Coding the player
In this lesson, we’ll add player movement, animation, and set it up to detect collisions.
Player node and click the “Attach Script” button:
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.
Note
Scripting languages before continuing.
Start by declaring the member variables this object will need:
GDScript C#C++
extends Area2Dexport var speed = 400 # How fast the player will move (pixels/sec).var screen_size # Size of the game window.
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.}
// 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 <AnimatedSprite.hpp>#include <Area2D.hpp>#include <CollisionShape2D.hpp>#include <Godot.hpp>#include <Input.hpp>class Player : public godot::Area2D { GODOT_CLASS(Player, godot::Area2D) godot::AnimatedSprite *_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
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.
_ready() function is called when a node enters the scene tree, which is a good time to find the size of the game window:
GDScript C#C++
func _ready(): screen_size = get_viewport_rect().size
public override void _Ready(){ ScreenSize = GetViewportRect().Size;}
// This code goes in `player.cpp`.#include "player.hpp"void Player::_ready() { _animated_sprite = get_node<godot::AnimatedSprite>("AnimatedSprite"); _collision_shape = get_node<godot::CollisionShape2D>("CollisionShape2D"); _input = godot::Input::get_singleton(); _screen_size = get_viewport_rect().size;}
_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.
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_rightaction. We need to assign a key to this action. Click the “+” icon on the right, then click the “Key” option in the drop-down menu. A dialog asks you to type in the desired key. Press the right arrow on your keyboard and click “Ok”. Repeat these steps to add three more mappings:
move_leftmapped to the left arrow key.move_upmapped to the up arrow key.move_downmapped to the down arrow key. Your input map tab should look like this: 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.Input.is_action_pressed(), which returnstrueif it’s pressed orfalseif it isn’t. GDScript C#C++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 $AnimatedSprite.play() else: $AnimatedSprite.stop()
public override void _Process(float 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 animatedSprite = GetNode<AnimatedSprite>("AnimatedSprite"); if (velocity.Length() > 0) { velocity = velocity.Normalized() * Speed; animatedSprite.Play(); } else { animatedSprite.Stop(); }}
// 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(); }}
velocityto(0, 0)- by default, the player should not be moving. Then we check each input and add/subtract from thevelocityto obtain a total direction. For example, if you holdrightanddownat the same time, the resultingvelocityvector 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. normalize the velocity, which means we set its length to1, then multiply by the desired speed. This means no more fast diagonal movement. Tip Vector math. It’s good to know but won’t be necessary for the rest of this tutorial.play()orstop()on the AnimatedSprite. Tip$is shorthand forget_node(). So in the code above,$AnimatedSprite.play()is the same asget_node("AnimatedSprite").play().$returns the node at the relative path from the current node, or returnsnullif the node is not found. Since AnimatedSprite is a child of the current node, we can use$AnimatedSprite.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_processfunction (make sure it’s not indented under the else): GDScript C#C++position += velocity * deltaposition.x = clamp(position.x, 0, screen_size.x)position.y = clamp(position.y, 0, screen_size.y)
Position += velocity * delta;Position = new Vector2( x: Mathf.Clamp(Position.x, 0, ScreenSize.x), y: Mathf.Clamp(Position.y, 0, ScreenSize.y));
Tip 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” (F6, 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 saysgodot::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);
Attempt to call function 'play' in base 'null instance' on a null instance$NodeNamemust match the name you see in the scene tree.Choosing animations
flip_hproperty for left movement. We also have the “up” animation, which should be flipped vertically withflip_vfor downward movement. Let’s place this code at the end of the_process()function: GDScript C#C++if velocity.x != 0: $AnimatedSprite.animation = "walk" $AnimatedSprite.flip_v = false # See the note below about boolean assignment. $AnimatedSprite.flip_h = velocity.x < 0elif velocity.y != 0: $AnimatedSprite.animation = "up" $AnimatedSprite.flip_v = velocity.y > 0
if (velocity.x != 0){ animatedSprite.Animation = "walk"; animatedSprite.FlipV = false; // See the note below about boolean assignment. animatedSprite.FlipH = velocity.x < 0;}else if (velocity.y != 0){ animatedSprite.Animation = "up"; animatedSprite.FlipV = velocity.y > 0;}
Note assigning a boolean value, we can do both at the same time. Consider this code versus the one-line boolean assignment above: GDScript C#if (velocity.x != 0) { _animated_sprite->set_animation("walk"); _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);}
if velocity.x < 0: $AnimatedSprite.flip_h = trueelse: $AnimatedSprite.flip_h = false
Play the scene again and check that the animations are correct in each of the directions. Tipif (velocity.x < 0){ animatedSprite.FlipH = true;}else{ animatedSprite.FlipH = false;}
"Walk", you must also use a capital “W” in the code._ready(), so the player will be hidden when the game starts: GDScript C#C++hide()
Hide();
hide();
Preparing for collisions
Playerto 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.extends Area2D: GDScript C#C++signal hit
// Don't forget to rebuild the project so the editor knows about the new signal.[Signal]public delegate void Hit();
// 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());}
Area2Dto detect the collision. Select thePlayernode and click the “Node” tab next to the Inspector tab to see the list of signals the player can emit:RigidBody2Dnodes, we want thebody_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. Note the green icon indicating that a signal is connected to this function. Add this code to the function: GDScript C#C++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)
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);}
// 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);}
hitsignal more than once. Noteset_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. GDScript C#C++func start(pos): position = pos show() $CollisionShape2D.disabled = false
public void Start(Vector2 pos){ Position = pos; Show(); GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;}
With the player working, we’ll work on the enemy in the next lesson.// This code goes in `player.cpp`.void Player::start(const godot::Vector2 p_position) { set_position(p_position); show(); _collision_shape->set_disabled(false);}
