Let’s Learn Godot 4 by Making an RPG — Part 3: Player Animations🤠

Christine Coomans
12 min readJun 21, 2023

Our player has been set up, and it moves around the map when we run the game. There is one problem though, and that is that it moves around statically. It has no animations configured! In this section, we are going to focus on adding animations to our player, so that it can walk around and start coming to life.

WHAT YOU WILL LEARN IN THIS PART:

· How to work with sprite sheets for animation.

· How to add animations using the AnimatedSprite2D node.

· How to connect animations to input actions.

· How to add custom input actions.

· How to work with the input() function and Input singleton.

· How to work with built-in signals.

For our player, we need animations to move it up, down, left, and right. For each of those directions, we will set up animations for idling, walking, attacking, damage, and death. Since we have so many animations to tackle, I will take you through the first direction, and then give you a table to complete the rest.

In your Player scene, with your AnimatedSprite2D selected, we can see that we already have an animation added for walking downwards. Let’s rename this default animation as walk_down.

Click on the file icon to add a new animation and call this idle_down. Just like before, let’s select the “Add Frames from Sprite Sheet” option, and navigate to the player’s front-sheet animation.

Change the horizontal value to 14, and the vertical value to 5, and select the first row of frames for our idle animation.

Change its FPS value to 6.

Add a new animation, and call it attack_down. For this one, you want to select the third row of animation frames.

Change its FPS value to 6 and turn off looping because we only want this animation to fire off once and not continuously.

Next, let’s add a new animation, and call it damage. Select the singular frame from row 4 for this animation.

Change its FPS value to 1 and turn off looping.

Finally, create a new animation and call it death. Add the final row of frames to this animation.

Change its FPS value to 14 and turn off looping.

Now we have our downward animations created, as well as our death and damage animations. Do you think you can handle adding the rest on your own? If not, reach out to me, and I will amend this section with the instructions for those as well.

For the more daring ones, here’s a table of animations you need to add:

At the end, your complete animations list should look like this:

Now that we’ve added all of our player’s animations, we need to go and update our script so that we can link these animations to our input actions. We want our “up” animations to play if we press W or UP, our “down” animations to play if the press S or DOWN, and “left” and “right” animations to play if we press A and LEFT, or D and RIGHT.

Let’s open up our script by clicking on the scroll icon next to your Player node. Because we already have our movement functionality added in our _physics_process() function, we can go ahead and create a custom function that will play the animations based on our player’s direction. This function needs to take a Vector2 as a parameter, which is the floating-point coordinates that represent our player’s position or direction in a particular space and time (refer to the vector images in the previous sections as a refresher on this).

Underneath your existing code, let’s create our player_animations() function.

### Player.gd

extends CharacterBody2D

# Player movement speed

@export var speed = 50

func _physics_process(delta):

# older code

# Animations

func player_animations(direction : Vector2):

pass

To determine the direction that the player is facing, we need to create two new variables. The first variable is the variable that we will compare against a zero vector. If the direction is not equal to Vector(0,0), it means the player is moving, which means the direction of the player. The second variable will be to store the value of this direction so that we can play its animation.

Let’s create these new variables on top of our script, underneath our speed variable.

### Player.gd

extends CharacterBody2D

# Player movement speed
@export var speed = 50

func _physics_process(delta):
# older code

# Animations
func player_animations(direction : Vector2):
pass

To make our code more organized, we will use the @onready annotation to create an instance of a reference to our AnimatedSprite2D node. This way we can reuse the variable name instead of saying $AnimatedSprite2D.play() each time we want to change the animation. Take note that we also flip our sprite horizontally when playing the side_ animation. This is so that we can reuse the same animation for both left and right directions.

### Player.gd
extends CharacterBody2D
# Node references
@onready var animation_sprite = $AnimatedSprite2D

Now, in our newly created function, we need to compare our new_direction variable to zero, and then assign the animation to it. If the direction is not equal to zero, we are moving, and thus our walk animation should play. If it is equal to zero, we are still, so the idle animation should play.

### Player.gd

extends CharacterBody2D

# Node references
@onready var animation_sprite = $AnimatedSprite2D

# older code

# Animations
func player_animations(direction : Vector2):
#Vector2.ZERO is the shorthand for writing Vector2(0, 0).
if direction != Vector2.ZERO:
#update our direction with the new_direction
new_direction = direction
#play walk animation because we are moving
animation = #todo
animation_sprite.play(animation)
else:
#play idle animation because we are still
animation = #todo
animation_sprite.play(animation)

But how do we determine the animation? We can do a long conditional check, or we can create a new function that will determine our direction (left, right, up, down) based on our plane directions (x, y) after the player presses an input action. If our player is pressing up, then we should return “up”, and the same goes for down, side, and left. If our player presses both up and down, we will return side.

Remember when we had to put _up, _down, and _side after each animation? Well, there was also a reason for that. Let’s create a new function underneath our player_animations() function to see what I mean by this.

### Player.gd

# older code

# Animation Direction
func returned_direction(direction : Vector2):
#it normalizes the direction vector
var normalized_direction = direction.normalized()
var default_return = "side"

if normalized_direction.y > 0:
return "down"
elif normalized_direction.y < 0:
return "up"
elif normalized_direction.x > 0:
#(right)
$AnimatedSprite2D.flip_h = false
return "side"
elif normalized_direction.x < 0:
#flip the animation for reusability (left)
$AnimatedSprite2D.flip_h = true
return "side"

#default value is empty
return default_return

In this function, we check the values of our player’s x and y coordinates, and based on that, we return the animation suffix, which is the end of the word (_up, _down, _side). We will then append this suffix to the animation to play, which is walk or idle. Let’s go back to our player_animations() function and replace the #todo code with this functionality.

### Player.gd

# older code

# Animations
func player_animations(direction : Vector2):
#Vector2.ZERO is the shorthand for writing Vector2(0, 0).
if direction != Vector2.ZERO:
#update our direction with the new_direction
new_direction = direction
#play walk animation, because we are moving
animation = "walk_" + returned_direction(new_direction)
animation_sprite.play(animation)
else:
#play idle animation, because we are still
animation = "idle_" + returned_direction(new_direction)
animation_sprite.play(animation)

Now all we have to do is actually call this function in the function where we added our movement, which is at the end of our _physics_process() function.

### Player.gd

# older code

func _physics_process(delta):
# Get player input (left, right, up/down)
var direction: Vector2
direction.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
direction.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
# If input is digital, normalize it for diagonal movement
if abs(direction.x) == 1 and abs(direction.y) == 1:
direction = direction.normalized()
# Apply movement
var movement = speed * direction * delta
# moves our player around, whilst enforcing collisions so that they come to a stop when colliding with another object.
move_and_collide(movement)
#plays animations
player_animations(direction)

If you run your scene now, you will see that your player plays the idle animation if it’s still, and the walk animation if it’s moving, and he also changes direction!

While we are at it, let’s also add the animation functionality for our players attacking and sprinting. For these animations, we will require new input actions, so in your project settings underneath Input Map, let’s add a new input.

Call the first one ui_sprint and the second one ui_attack.

Assign the Shift key to the ui_sprint, and the CTRL key to ui_attack. You can also assign joystick keys to this if you want. We will add the sprinting input in our _physics_process() function because we will use this later to track our stamina values at a constant rate, but we will add the attacking input in our _input(event) function because we want the input to be captured, but not tracked during the game loop.

You can either add inputs in Godot in its built-in _input function, or via the Input Singleton class. You will use the Input singleton if you want the state of the input actions stored all throughout the game loop, because it is independent of any node or scene, for example, you might use it in a scenario where you want to be able to press two buttons at once.

The _input function can capture inputs `is_action_pressed()’ and `is_action_released()’ functions, whilst the Input singleton captures inputs via the `is_action_just_pressed()’ / ‘is_action_just_released()’ functions. I made some images to simply explain the different types of input functions.

When to use the Input Singleton vs. input() function?

In Godot you can cause input events in two ways: the input() function, and the Input Singleton. The main difference between the two is that inputs that are captured in the input() function can only be fired off once, whereas the Input singleton can be called in any other method.

For example, we’re using the Input singleton in our physics_process() function because we want the input to be captured continuously to trigger an event, whereas if we were to pause the game or shoot our gun we would capture these inputs in the input() function because we only want the input to be captured once — which is when we press the button.

Figure 6: Input methods. View the online version here.

Let’s start with the code for our attack input. We only want to play our other animations only if our player isn’t attacking, and vice versa. To do this, we need to create a new variable which we will use to check if the player is currently attacking or not.

### Player.gd
extends CharacterBody2D
# Node references
@onready var animation_sprite = $AnimatedSprite2D
# Player states
@export var speed = 50
var is_attacking = false

Now we can amend our _physics_process() function to only play our returned animations and to only process our movement if the player is not attacking.

### Player.gd

extends CharacterBody2D

# older code

func _physics_process(delta):
# Get player input (left, right, up/down)
var direction: Vector2
direction.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
direction.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
# If input is digital, normalize it for diagonal movement
if abs(direction.x) == 1 and abs(direction.y) == 1:
direction = direction.normalized()
# Apply movement if the player is not attacking
var movement = speed * direction * delta

if is_attacking == false:
move_and_collide(movement)
player_animations(direction)

We now need to call on our built-in func _input(event) function so that we can play the animation for our attack_ prefix. Let’s add this function underneath our _physics_process() function.


### Player.gd

extends CharacterBody2D

# older code

func _input(event):
#input event for our attacking, i.e. our shooting
if event.is_action_pressed("ui_attack"):
#attacking/shooting anim
is_attacking = true
var animation = "attack_" + returned_direction(new_direction)
animation_sprite.play(animation)

You will notice now that if you run your scene and press CTRL to fire off your attack animation, the animation plays but our character does not return to its previous idle or walk animations. This is because our character is stuck on the last animation frame of our attack function, and to fix this, we need to use a signal to notify our game that the animation has finished playing so that it can set our is_attacking variable back to false.

To do this, we need to click on our AnimatedSprite2D node, and in the Node panel we need to hook up the animation_finished signal to our player script. This signal will trigger when our animation has reached the end of its frame, and thus if our attack_ animation has finished playing, this signal will trigger the function to reset our is_attacking variable back to false. Double-click on the signal and select the script to do so.

What is a signal?

A signal is an emitter that provides a way for nodes to communicate without needing to have a direct reference to each other. This makes our code more flexible and maintainable. Signals emit after certain events occur, for example, if our enemy count changes after an enemy is killed, we can use signals to notify the UI that it needs to change the value of the enemy count from 10 to 9.

You will now see that a new method has been created at the end of your player script. We can now simply reset our is_attacking variable back to false.


### Player.gd

extends CharacterBody2D

# older code

# Reset Animation states
func _on_animated_sprite_2d_animation_finished():
is_attacking = false

Next, we can go ahead and add in our sprinting functionality for when the player holds down Shift. Since sprinting is just movement, we can add the code for it in our _physics_process() function, which handles our player’s movement and physics (not animations).

### Player.gd

extends CharacterBody2D

# --------------------------------- Movement & Animations -----------------------
func _physics_process(delta):
# Get player input (left, right, up/down)
var direction: Vector2
direction.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
direction.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
# Normalize movement
if abs(direction.x) == 1 and abs(direction.y) == 1:
direction = direction.normalized()
# Sprinting
if Input.is_action_pressed("ui_sprint"):
speed = 100
elif Input.is_action_just_released("ui_sprint"):
speed = 50
# Apply movement if the player is not attacking
var movement = speed * direction * delta
if is_attacking == false:
move_and_collide(movement)
player_animations(direction)
# If no input is pressed, idle
if !Input.is_anything_pressed():
if is_attacking == false:
animation = "idle_" + returned_direction(new_direction)

Finally, if the player is not pressing any input, then the idle animation should play. This will prevent our player from being stuck in a running state even if our inputs are released.

### Player.gd

extends CharacterBody2D

# --------------------------------- Movement & Animations -----------------------
func _physics_process(delta):
# Get player input (left, right, up/down)
var direction: Vector2
direction.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
direction.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
# Normalize movement
if abs(direction.x) == 1 and abs(direction.y) == 1:
direction = direction.normalized()
# Sprinting
if Input.is_action_pressed("ui_sprint"):
speed = 100
elif Input.is_action_just_released("ui_sprint"):
speed = 50
# Apply movement if the player is not attacking
var movement = speed * direction * delta
if is_attacking == false:
move_and_collide(movement)
player_animations(direction)
# If no input is pressed, idle
if !Input.is_anything_pressed():
if is_attacking == false:
animation = "idle_" + returned_direction(new_direction)

If we run our scene, we can see that the player is moving around the map with animations, and they can also sprint and attack!

With our player character set up, we can move on to animations. In the next part we will be creating the map for our game, i.e., the world, as well as set up our player’s camera. Remember to save your game project, and I’ll see you in the next part.

Your final code for this part should look like this.

TUTORIAL RELEASE

LINKS TO OTHER PARTS

The tutorial series has 23 chapters. I’ll be releasing all of the chapters in sectional daily parts over the next couple of weeks.

You can find the updated list of the tutorial links for all 23 parts in this series here.

FULL TUTORIAL

If you like this series or want to skip the wait and access the offline, full version of the tutorial series, you can support me by buying the offline booklet for just $4 on Ko-fi!😊

--

--