|
@@ -0,0 +1,515 @@
|
|
|
+.. _doc_your_first_2d_game_coding_the_player:
|
|
|
+
|
|
|
+Coding the player
|
|
|
+=================
|
|
|
+
|
|
|
+In this lesson, we'll add player movement, animation, and set it up to detect
|
|
|
+collisions.
|
|
|
+
|
|
|
+To do so, we need to add some functionality that we can't get from a built-in
|
|
|
+node, so we'll add a script. Click the ``Player`` node and click the "Attach
|
|
|
+Script" button:
|
|
|
+
|
|
|
+.. image:: img/add_script_button.png
|
|
|
+
|
|
|
+In the script settings window, you can leave the default settings alone. Just
|
|
|
+click "Create":
|
|
|
+
|
|
|
+.. note:: If you're creating a C# script or other languages, select the language
|
|
|
+ from the `language` drop down menu before hitting create.
|
|
|
+
|
|
|
+.. image:: img/attach_node_window.png
|
|
|
+
|
|
|
+.. note:: If this is your first time encountering GDScript, please read
|
|
|
+ :ref:`doc_scripting` before continuing.
|
|
|
+
|
|
|
+Start by declaring the member variables this object will need:
|
|
|
+
|
|
|
+.. tabs::
|
|
|
+ .. code-tab:: gdscript GDScript
|
|
|
+
|
|
|
+ extends Area2D
|
|
|
+
|
|
|
+ export var speed = 400 # How fast the player will move (pixels/sec).
|
|
|
+ var screen_size # Size of the game window.
|
|
|
+
|
|
|
+ .. code-tab:: csharp
|
|
|
+
|
|
|
+ using Godot;
|
|
|
+ using System;
|
|
|
+
|
|
|
+ public class Player : Area2D
|
|
|
+ {
|
|
|
+ [Export]
|
|
|
+ public int Speed = 400; // How fast the player will move (pixels/sec).
|
|
|
+
|
|
|
+ public Vector2 ScreenSize; // Size of the game window.
|
|
|
+ }
|
|
|
+
|
|
|
+ .. code-tab:: cpp
|
|
|
+
|
|
|
+ // A `player.gdns` file has already been created for you. Attach it to the Player node.
|
|
|
+
|
|
|
+ // Create two files `player.cpp` and `player.hpp` next to `entry.cpp` in `src`.
|
|
|
+ // This code goes in `player.hpp`. We also define the methods we'll be using here.
|
|
|
+ #ifndef PLAYER_H
|
|
|
+ #define PLAYER_H
|
|
|
+
|
|
|
+ #include <AnimatedSprite2D.hpp>
|
|
|
+ #include <Area2D.hpp>
|
|
|
+ #include <CollisionShape2D.hpp>
|
|
|
+ #include <Godot.hpp>
|
|
|
+ #include <Input.hpp>
|
|
|
+
|
|
|
+ class Player : public godot::Area2D {
|
|
|
+ GODOT_CLASS(Player, godot::Area2D)
|
|
|
+
|
|
|
+ godot::AnimatedSprite2D *_animated_sprite;
|
|
|
+ godot::CollisionShape2D *_collision_shape;
|
|
|
+ godot::Input *_input;
|
|
|
+ godot::Vector2 _screen_size; // Size of the game window.
|
|
|
+
|
|
|
+ public:
|
|
|
+ real_t speed = 400; // How fast the player will move (pixels/sec).
|
|
|
+
|
|
|
+ void _init() {}
|
|
|
+ void _ready();
|
|
|
+ void _process(const double p_delta);
|
|
|
+ void start(const godot::Vector2 p_position);
|
|
|
+ void _on_Player_body_entered(godot::Node2D *_body);
|
|
|
+
|
|
|
+ static void _register_methods();
|
|
|
+ };
|
|
|
+
|
|
|
+ #endif // PLAYER_H
|
|
|
+
|
|
|
+Using the ``export`` keyword on the first variable ``speed`` allows us to set
|
|
|
+its value in the Inspector. This can be handy for values that you want to be
|
|
|
+able to adjust just like a node's built-in properties. Click on the ``Player``
|
|
|
+node and you'll see the property now appears in the "Script Variables" section
|
|
|
+of the Inspector. Remember, if you change the value here, it will override the
|
|
|
+value written in the script.
|
|
|
+
|
|
|
+.. warning:: If you're using C#, you need to (re)build the project assemblies
|
|
|
+ whenever you want to see new export variables or signals. This
|
|
|
+ build can be manually triggered by clicking the word "Mono" at the
|
|
|
+ bottom of the editor window to reveal the Mono Panel, then clicking
|
|
|
+ the "Build Project" button.
|
|
|
+
|
|
|
+.. image:: img/export_variable.png
|
|
|
+
|
|
|
+The ``_ready()`` function is called when a node enters the scene tree, which is
|
|
|
+a good time to find the size of the game window:
|
|
|
+
|
|
|
+.. tabs::
|
|
|
+ .. code-tab:: gdscript GDScript
|
|
|
+
|
|
|
+ func _ready():
|
|
|
+ screen_size = get_viewport_rect().size
|
|
|
+
|
|
|
+ .. code-tab:: csharp
|
|
|
+
|
|
|
+ public override void _Ready()
|
|
|
+ {
|
|
|
+ ScreenSize = GetViewportRect().Size;
|
|
|
+ }
|
|
|
+
|
|
|
+ .. code-tab:: cpp
|
|
|
+
|
|
|
+ // This code goes in `player.cpp`.
|
|
|
+ #include "player.hpp"
|
|
|
+
|
|
|
+ void Player::_ready() {
|
|
|
+ _animated_sprite = get_node<godot::AnimatedSprite2D>("AnimatedSprite2D");
|
|
|
+ _collision_shape = get_node<godot::CollisionShape2D>("CollisionShape2D");
|
|
|
+ _input = godot::Input::get_singleton();
|
|
|
+ _screen_size = get_viewport_rect().size;
|
|
|
+ }
|
|
|
+
|
|
|
+Now we can use the ``_process()`` function to define what the player will do.
|
|
|
+``_process()`` is called every frame, so we'll use it to update elements of our
|
|
|
+game, which we expect will change often. For the player, we need to do the
|
|
|
+following:
|
|
|
+
|
|
|
+- Check for input.
|
|
|
+- Move in the given direction.
|
|
|
+- Play the appropriate animation.
|
|
|
+
|
|
|
+First, we need to check for input - is the player pressing a key? For this game,
|
|
|
+we have 4 direction inputs to check. Input actions are defined in the Project
|
|
|
+Settings under "Input Map". Here, you can define custom events and assign
|
|
|
+different keys, mouse events, or other inputs to them. For this game, we will
|
|
|
+just use the default events called "ui_right" etc that are assigned to the arrow
|
|
|
+keys on the keyboard.
|
|
|
+
|
|
|
+You can detect whether a key is pressed using ``Input.is_action_pressed()``,
|
|
|
+which returns ``true`` if it's pressed or ``false`` if it isn't.
|
|
|
+
|
|
|
+.. tabs::
|
|
|
+ .. code-tab:: gdscript GDScript
|
|
|
+
|
|
|
+ func _process(delta):
|
|
|
+ var velocity = Vector2.ZERO # The player's movement vector.
|
|
|
+ if Input.is_action_pressed("ui_right"):
|
|
|
+ velocity.x += 1
|
|
|
+ if Input.is_action_pressed("ui_left"):
|
|
|
+ velocity.x -= 1
|
|
|
+ if Input.is_action_pressed("ui_down"):
|
|
|
+ velocity.y += 1
|
|
|
+ if Input.is_action_pressed("ui_up"):
|
|
|
+ velocity.y -= 1
|
|
|
+
|
|
|
+ if velocity.length() > 0:
|
|
|
+ velocity = velocity.normalized() * speed
|
|
|
+ $AnimatedSprite2D.play()
|
|
|
+ else:
|
|
|
+ $AnimatedSprite2D.stop()
|
|
|
+
|
|
|
+ .. code-tab:: csharp
|
|
|
+
|
|
|
+ public override void _Process(float delta)
|
|
|
+ {
|
|
|
+ var velocity = Vector2.Zero; // The player's movement vector.
|
|
|
+
|
|
|
+ if (Input.IsActionPressed("ui_right"))
|
|
|
+ {
|
|
|
+ velocity.x += 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Input.IsActionPressed("ui_left"))
|
|
|
+ {
|
|
|
+ velocity.x -= 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Input.IsActionPressed("ui_down"))
|
|
|
+ {
|
|
|
+ velocity.y += 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Input.IsActionPressed("ui_up"))
|
|
|
+ {
|
|
|
+ velocity.y -= 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ var animatedSprite = GetNode<AnimatedSprite2D>("AnimatedSprite2D");
|
|
|
+
|
|
|
+ if (velocity.Length() > 0)
|
|
|
+ {
|
|
|
+ velocity = velocity.Normalized() * Speed;
|
|
|
+ animatedSprite.Play();
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ animatedSprite.Stop();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .. code-tab:: cpp
|
|
|
+
|
|
|
+ // This code goes in `player.cpp`.
|
|
|
+ void Player::_process(const double p_delta) {
|
|
|
+ godot::Vector2 velocity(0, 0);
|
|
|
+
|
|
|
+ velocity.x = _input->get_action_strength("move_right") - _input->get_action_strength("move_left");
|
|
|
+ velocity.y = _input->get_action_strength("move_down") - _input->get_action_strength("move_up");
|
|
|
+
|
|
|
+ if (velocity.length() > 0) {
|
|
|
+ velocity = velocity.normalized() * speed;
|
|
|
+ _animated_sprite->play();
|
|
|
+ } else {
|
|
|
+ _animated_sprite->stop();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+We start by setting the ``velocity`` to ``(0, 0)`` - by default, the player
|
|
|
+should not be moving. Then we check each input and add/subtract from the
|
|
|
+``velocity`` to obtain a total direction. For example, if you hold ``right`` and
|
|
|
+``down`` at the same time, the resulting ``velocity`` vector will be ``(1, 1)``.
|
|
|
+In this case, since we're adding a horizontal and a vertical movement, the
|
|
|
+player would move *faster* diagonally than if it just moved horizontally.
|
|
|
+
|
|
|
+We can prevent that if we *normalize* the velocity, which means we set its
|
|
|
+*length* to ``1``, then multiply by the desired speed. This means no more fast
|
|
|
+diagonal movement.
|
|
|
+
|
|
|
+.. tip:: If you've never used vector math before, or need a refresher, you can
|
|
|
+ see an explanation of vector usage in Godot at :ref:`doc_vector_math`.
|
|
|
+ It's good to know but won't be necessary for the rest of this tutorial.
|
|
|
+
|
|
|
+We also check whether the player is moving so we can call ``play()`` or
|
|
|
+``stop()`` on the AnimatedSprite2D.
|
|
|
+
|
|
|
+ ``$`` is shorthand for ``get_node()``. So in the code above,
|
|
|
+ ``$AnimatedSprite2D.play()`` is the same as
|
|
|
+ ``get_node("AnimatedSprite2D").play()``.
|
|
|
+
|
|
|
+.. tip:: In GDScript, ``$`` returns the node at the relative path from the
|
|
|
+ current node, or returns ``null`` if the node is not found. Since
|
|
|
+ AnimatedSprite2D is a child of the current node, we can use
|
|
|
+ ``$AnimatedSprite2D``.
|
|
|
+
|
|
|
+Now that we have a movement direction, we can update the player's position. We
|
|
|
+can also use ``clamp()`` to prevent it from leaving the screen. *Clamping* a
|
|
|
+value means restricting it to a given range. Add the following to the bottom of
|
|
|
+the ``_process`` function (make sure it's not indented under the `else`):
|
|
|
+
|
|
|
+.. tabs::
|
|
|
+ .. code-tab:: gdscript GDScript
|
|
|
+
|
|
|
+ position += velocity * delta
|
|
|
+ position.x = clamp(position.x, 0, screen_size.x)
|
|
|
+ position.y = clamp(position.y, 0, screen_size.y)
|
|
|
+
|
|
|
+ .. code-tab:: csharp
|
|
|
+
|
|
|
+ Position += velocity * delta;
|
|
|
+ Position = new Vector2(
|
|
|
+ x: Mathf.Clamp(Position.x, 0, ScreenSize.x),
|
|
|
+ y: Mathf.Clamp(Position.y, 0, ScreenSize.y)
|
|
|
+ );
|
|
|
+
|
|
|
+ .. code-tab:: cpp
|
|
|
+
|
|
|
+ godot::Vector2 position = get_position();
|
|
|
+ position += velocity * (real_t)p_delta;
|
|
|
+ position.x = godot::Math::clamp(position.x, (real_t)0.0, _screen_size.x);
|
|
|
+ position.y = godot::Math::clamp(position.y, (real_t)0.0, _screen_size.y);
|
|
|
+ set_position(position);
|
|
|
+
|
|
|
+.. tip:: The `delta` parameter in the `_process()` function refers to the *frame
|
|
|
+ length* - the amount of time that the previous frame took to complete.
|
|
|
+ Using this value ensures that your movement will remain consistent even
|
|
|
+ if the frame rate changes.
|
|
|
+
|
|
|
+Click "Play Scene" (:kbd:`F6`, :kbd:`Cmd + R` on macOS) and confirm you can move
|
|
|
+the player around the screen in all directions.
|
|
|
+
|
|
|
+.. warning:: If you get an error in the "Debugger" panel that says
|
|
|
+
|
|
|
+ ``Attempt to call function 'play' in base 'null instance' on a null
|
|
|
+ instance``
|
|
|
+
|
|
|
+ this likely means you spelled the name of the AnimatedSprite2D node
|
|
|
+ wrong. Node names are case-sensitive and ``$NodeName`` must match
|
|
|
+ the name you see in the scene tree.
|
|
|
+
|
|
|
+Choosing animations
|
|
|
+~~~~~~~~~~~~~~~~~~~
|
|
|
+
|
|
|
+Now that the player can move, we need to change which animation the
|
|
|
+AnimatedSprite2D is playing based on its direction. We have the "walk" animation,
|
|
|
+which shows the player walking to the right. This animation should be flipped
|
|
|
+horizontally using the ``flip_h`` property for left movement. We also have the
|
|
|
+"up" animation, which should be flipped vertically with ``flip_v`` for downward
|
|
|
+movement. Let's place this code at the end of the ``_process()`` function:
|
|
|
+
|
|
|
+.. tabs::
|
|
|
+ .. code-tab:: gdscript GDScript
|
|
|
+
|
|
|
+ if velocity.x != 0:
|
|
|
+ $AnimatedSprite2D.animation = "walk"
|
|
|
+ $AnimatedSprite2D.flip_v = false
|
|
|
+ # See the note below about boolean assignment.
|
|
|
+ $AnimatedSprite2D.flip_h = velocity.x < 0
|
|
|
+ elif velocity.y != 0:
|
|
|
+ $AnimatedSprite2D.animation = "up"
|
|
|
+ $AnimatedSprite2D.flip_v = velocity.y > 0
|
|
|
+
|
|
|
+ .. code-tab:: csharp
|
|
|
+
|
|
|
+ if (velocity.x != 0)
|
|
|
+ {
|
|
|
+ animatedSprite2D.Animation = "walk";
|
|
|
+ animatedSprite2D.FlipV = false;
|
|
|
+ // See the note below about boolean assignment.
|
|
|
+ animatedSprite2D.FlipH = velocity.x < 0;
|
|
|
+ }
|
|
|
+ else if (velocity.y != 0)
|
|
|
+ {
|
|
|
+ animatedSprite2D.Animation = "up";
|
|
|
+ animatedSprite2D.FlipV = velocity.y > 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .. code-tab:: cpp
|
|
|
+
|
|
|
+ if (velocity.x != 0) {
|
|
|
+ _animated_sprite->set_animation("right");
|
|
|
+ _animated_sprite->set_flip_v(false);
|
|
|
+ // See the note below about boolean assignment.
|
|
|
+ _animated_sprite->set_flip_h(velocity.x < 0);
|
|
|
+ } else if (velocity.y != 0) {
|
|
|
+ _animated_sprite->set_animation("up");
|
|
|
+ _animated_sprite->set_flip_v(velocity.y > 0);
|
|
|
+ }
|
|
|
+
|
|
|
+.. Note:: The boolean assignments in the code above are a common shorthand for
|
|
|
+ programmers. Since we're doing a comparison test (boolean) and also
|
|
|
+ *assigning* a boolean value, we can do both at the same time. Consider
|
|
|
+ this code versus the one-line boolean assignment above:
|
|
|
+
|
|
|
+ .. tabs::
|
|
|
+ .. code-tab :: gdscript GDScript
|
|
|
+
|
|
|
+ if velocity.x < 0:
|
|
|
+ $AnimatedSprite2D.flip_h = true
|
|
|
+ else:
|
|
|
+ $AnimatedSprite2D.flip_h = false
|
|
|
+
|
|
|
+ .. code-tab:: csharp
|
|
|
+
|
|
|
+ if (velocity.x < 0)
|
|
|
+ {
|
|
|
+ animatedSprite2D.FlipH = true;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ animatedSprite2D.FlipH = false;
|
|
|
+ }
|
|
|
+
|
|
|
+Play the scene again and check that the animations are correct in each of the
|
|
|
+directions.
|
|
|
+
|
|
|
+.. tip:: A common mistake here is to type the names of the animations wrong. The
|
|
|
+ animation names in the SpriteFrames panel must match what you type in
|
|
|
+ the code. If you named the animation ``"Walk"``, you must also use a
|
|
|
+ capital "W" in the code.
|
|
|
+
|
|
|
+When you're sure the movement is working correctly, add this line to
|
|
|
+``_ready()``, so the player will be hidden when the game starts:
|
|
|
+
|
|
|
+.. tabs::
|
|
|
+ .. code-tab:: gdscript GDScript
|
|
|
+
|
|
|
+ hide()
|
|
|
+
|
|
|
+ .. code-tab:: csharp
|
|
|
+
|
|
|
+ Hide();
|
|
|
+
|
|
|
+ .. code-tab:: cpp
|
|
|
+
|
|
|
+ hide();
|
|
|
+
|
|
|
+Preparing for collisions
|
|
|
+~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
+
|
|
|
+We want ``Player`` to detect when it's hit by an enemy, but we haven't made any
|
|
|
+enemies yet! That's OK, because we're going to use Godot's *signal*
|
|
|
+functionality to make it work.
|
|
|
+
|
|
|
+Add the following at the top of the script, after ``extends Area2D``:
|
|
|
+
|
|
|
+.. tabs::
|
|
|
+ .. code-tab:: gdscript GDScript
|
|
|
+
|
|
|
+ signal hit
|
|
|
+
|
|
|
+ .. code-tab:: csharp
|
|
|
+
|
|
|
+ // Don't forget to rebuild the project so the editor knows about the new signal.
|
|
|
+
|
|
|
+ [Signal]
|
|
|
+ public delegate void Hit();
|
|
|
+
|
|
|
+ .. code-tab:: cpp
|
|
|
+
|
|
|
+ // This code goes in `player.cpp`.
|
|
|
+ // We need to register the signal here, and while we're here, we can also
|
|
|
+ // register the other methods and register the speed property.
|
|
|
+ void Player::_register_methods() {
|
|
|
+ godot::register_method("_ready", &Player::_ready);
|
|
|
+ godot::register_method("_process", &Player::_process);
|
|
|
+ godot::register_method("start", &Player::start);
|
|
|
+ godot::register_method("_on_Player_body_entered", &Player::_on_Player_body_entered);
|
|
|
+ godot::register_property("speed", &Player::speed, (real_t)400.0);
|
|
|
+ // This below line is the signal.
|
|
|
+ godot::register_signal<Player>("hit", godot::Dictionary());
|
|
|
+ }
|
|
|
+
|
|
|
+This defines a custom signal called "hit" that we will have our player emit
|
|
|
+(send out) when it collides with an enemy. We will use ``Area2D`` to detect the
|
|
|
+collision. Select the ``Player`` node and click the "Node" tab next to the
|
|
|
+Inspector tab to see the list of signals the player can emit:
|
|
|
+
|
|
|
+.. image:: img/player_signals.png
|
|
|
+
|
|
|
+Notice our custom "hit" signal is there as well! Since our enemies are going to
|
|
|
+be ``RigidBody2D`` nodes, we want the ``body_entered(body: Node)`` signal. This
|
|
|
+signal will be emitted when a body contacts the player. Click "Connect.." and
|
|
|
+the "Connect a Signal" window appears. We don't need to change any of these
|
|
|
+settings so click "Connect" again. Godot will automatically create a function in
|
|
|
+your player's script.
|
|
|
+
|
|
|
+.. image:: img/player_signal_connection.png
|
|
|
+
|
|
|
+Note the green icon indicating that a signal is connected to this function. Add
|
|
|
+this code to the function:
|
|
|
+
|
|
|
+.. tabs::
|
|
|
+ .. code-tab:: gdscript GDScript
|
|
|
+
|
|
|
+ func _on_Player_body_entered(body):
|
|
|
+ hide() # Player disappears after being hit.
|
|
|
+ emit_signal("hit")
|
|
|
+ # Must be deferred as we can't change physics properties on a physics callback.
|
|
|
+ $CollisionShape2D.set_deferred("disabled", true)
|
|
|
+
|
|
|
+ .. code-tab:: csharp
|
|
|
+
|
|
|
+ public void OnPlayerBodyEntered(PhysicsBody2D body)
|
|
|
+ {
|
|
|
+ Hide(); // Player disappears after being hit.
|
|
|
+ EmitSignal(nameof(Hit));
|
|
|
+ // Must be deferred as we can't change physics properties on a physics callback.
|
|
|
+ GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred("disabled", true);
|
|
|
+ }
|
|
|
+
|
|
|
+ .. code-tab:: cpp
|
|
|
+
|
|
|
+ // This code goes in `player.cpp`.
|
|
|
+ void Player::_on_Player_body_entered(godot::Node2D *_body) {
|
|
|
+ hide(); // Player disappears after being hit.
|
|
|
+ emit_signal("hit");
|
|
|
+ // Must be deferred as we can't change physics properties on a physics callback.
|
|
|
+ _collision_shape->set_deferred("disabled", true);
|
|
|
+ }
|
|
|
+
|
|
|
+Each time an enemy hits the player, the signal is going to be emitted. We need
|
|
|
+to disable the player's collision so that we don't trigger the ``hit`` signal
|
|
|
+more than once.
|
|
|
+
|
|
|
+.. Note:: Disabling the area's collision shape can cause an error if it happens
|
|
|
+ in the middle of the engine's collision processing. Using
|
|
|
+ ``set_deferred()`` tells Godot to wait to disable the shape until it's
|
|
|
+ safe to do so.
|
|
|
+
|
|
|
+The last piece is to add a function we can call to reset the player when
|
|
|
+starting a new game.
|
|
|
+
|
|
|
+.. tabs::
|
|
|
+ .. code-tab:: gdscript GDScript
|
|
|
+
|
|
|
+ func start(pos):
|
|
|
+ position = pos
|
|
|
+ show()
|
|
|
+ $CollisionShape2D.disabled = false
|
|
|
+
|
|
|
+ .. code-tab:: csharp
|
|
|
+
|
|
|
+ public void Start(Vector2 pos)
|
|
|
+ {
|
|
|
+ Position = pos;
|
|
|
+ Show();
|
|
|
+ GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ .. code-tab:: cpp
|
|
|
+
|
|
|
+ // This code goes in `player.cpp`.
|
|
|
+ void Player::start(const godot::Vector2 p_position) {
|
|
|
+ set_position(p_position);
|
|
|
+ show();
|
|
|
+ _collision_shape->set_disabled(false);
|
|
|
+ }
|
|
|
+
|
|
|
+With the player working, we'll work on the enemy in the next lesson.
|