123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449 |
- .. _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.webp
- 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.webp
- .. 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;
- public partial class Player : Area2D
- {
- [Export]
- public int Speed { get; set; } = 400; // How fast the player will move (pixels/sec).
- public Vector2 ScreenSize; // Size of the game window.
- }
- 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 Inspector in a new section
- with the name of the script. 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 **Build** button at
- the top right of the editor.
- .. image:: img/build_dotnet.webp
- .. image:: img/export_variable.webp
- Your ``player.gd`` script should already contain
- a ``_ready()`` and a ``_process()`` function.
- If you didn't select the default template shown above,
- create these functions while following the lesson.
- 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;
- }
- 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
- map the arrow keys to the four directions.
- Click on *Project -> Project Settings* to open the project settings window and
- click on the *Input Map* tab at the top. Type "move_right" in the top bar and
- click the "Add" button to add the ``move_right`` action.
- .. image:: img/input-mapping-add-action.webp
- We need to assign a key to this action. Click the "+" icon on the right, to
- open the event manager window.
- .. image:: img/input-mapping-add-key.webp
- The "Listening for Input..." field should automatically be selected.
- Press the "right" key on your keyboard, and the menu should look like this now.
- .. image:: img/input-mapping-event-configuration.webp
- Select the "ok" button. The "right" key is now associated with the ``move_right`` action.
- Repeat these steps to add three more mappings:
- 1. ``move_left`` mapped to the left arrow key.
- 2. ``move_up`` mapped to the up arrow key.
- 3. And ``move_down`` mapped to the down arrow key.
- Your input map tab should look like this:
- .. image:: img/input-mapping-completed.webp
- Click the "Close" button to close the project settings.
- .. note::
- We only mapped one key to each input action, but you can map multiple keys,
- joystick buttons, or mouse buttons to the same input action.
- 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("move_right"):
- velocity.x += 1
- if Input.is_action_pressed("move_left"):
- velocity.x -= 1
- if Input.is_action_pressed("move_down"):
- velocity.y += 1
- if Input.is_action_pressed("move_up"):
- velocity.y -= 1
- if velocity.length() > 0:
- velocity = velocity.normalized() * speed
- $AnimatedSprite2D.play()
- else:
- $AnimatedSprite2D.stop()
- .. code-tab:: csharp
- public override void _Process(double delta)
- {
- var velocity = Vector2.Zero; // The player's movement vector.
- if (Input.IsActionPressed("move_right"))
- {
- velocity.X += 1;
- }
- if (Input.IsActionPressed("move_left"))
- {
- velocity.X -= 1;
- }
- if (Input.IsActionPressed("move_down"))
- {
- velocity.Y += 1;
- }
- if (Input.IsActionPressed("move_up"))
- {
- velocity.Y -= 1;
- }
- var animatedSprite2D = GetNode<AnimatedSprite2D>("AnimatedSprite2D");
- if (velocity.Length() > 0)
- {
- velocity = velocity.Normalized() * Speed;
- animatedSprite2D.Play();
- }
- else
- {
- animatedSprite2D.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.
- .. tip:: ``$`` is shorthand for ``get_node()``. So in the code above,
- ``$AnimatedSprite2D.play()`` is the same as
- ``get_node("AnimatedSprite2D").play()``.
- 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 = position.clamp(Vector2.ZERO, screen_size)
- .. code-tab:: csharp
- Position += velocity * (float)delta;
- Position = new Vector2(
- x: Mathf.Clamp(Position.X, 0, ScreenSize.X),
- y: Mathf.Clamp(Position.Y, 0, ScreenSize.Y)
- );
- .. 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 "Run Current 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 the following 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 the following boolean assignment.
- animatedSprite2D.FlipH = velocity.X < 0;
- }
- else if (velocity.Y != 0)
- {
- animatedSprite2D.Animation = "up";
- animatedSprite2D.FlipV = 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();
- 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. If you're using GDScript, add it after
- ``extends Area2D``. If you're using C#, add it after ``public partial class Player : 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 HitEventHandler();
- 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.webp
- Notice our custom "hit" signal is there as well! Since our enemies are going to
- be ``RigidBody2D`` nodes, we want the ``body_entered(body: Node2D)`` signal. This
- signal will be emitted when a body contacts the player. Click "Connect.." and
- the "Connect a Signal" window appears.
- Godot will create a function with that exact name directly in script
- for you. You don't need to change the default settings right now.
- .. warning::
- .. The issue for this bug is #41283
- If you're using an external text editor (for example, Visual Studio Code),
- a bug currently prevents Godot from doing so. You'll be sent to your external
- editor, but the new function won't be there.
- In this case, you'll need to write the function yourself into the Player's
- script file.
- .. image:: img/player_signal_connection.webp
- Note the green icon indicating that a signal is connected to this function; this does
- not mean the function exists, only that the signal will attempt to connect to a function
- with that name, so double-check that the spelling of the function matches exactly!
- Next, add this code to the function:
- .. tabs::
- .. code-tab:: gdscript GDScript
- func _on_body_entered(body):
- hide() # Player disappears after being hit.
- hit.emit()
- # Must be deferred as we can't change physics properties on a physics callback.
- $CollisionShape2D.set_deferred("disabled", true)
- .. code-tab:: csharp
- // We also specified this function name in PascalCase in the editor's connection window.
- private void OnBodyEntered(Node2D body)
- {
- Hide(); // Player disappears after being hit.
- EmitSignal(SignalName.Hit);
- // Must be deferred as we can't change physics properties on a physics callback.
- GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred(CollisionShape2D.PropertyName.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 position)
- {
- Position = position;
- Show();
- GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;
- }
- With the player working, we'll work on the enemy in the next lesson.
|