Learn Godot 4 by Making a 2D Platformer — Part 3: Player Animations
*You can find the links to the previous parts at the bottom of this tutorial.
With our Player scene created, we can now go ahead and add animations to our player which we will then connect to our inputs in our script. We want our player to have animations to run when pressing ui_left and ui_right, idle when standing still, climb when pressing ui_up, jump when pressing ui_jump, and attack when pressing ui_attack (which we still need to create). We also want animations for our player’s damage and death.
WHAT YOU WILL LEARN IN THIS PART:
- How to create multiple AnimatedSprite2D animations.
- How to connect node Signals to scripts.
Let’s start by adding our ui_attack input action. We will assign the SHIFT key for our attack. Our player will need this attack animation and input for when it picks up an attack boost pickup, which will allow it to destroy boxes and bombs for a short amount of time.
Now, let’s create our animations, starting with our run animation. We will use this run animation for both our left and right running, so we don’t need to create a run_left and run_right animation. Let’s create a new animation (click on the paper icon in your Animations panel) and name it “run”. Navigate to “res://Assets/Kings and Pigs/Sprites/01-King Human/Run (78x58).png” in your Assets folder and crop out your animation frames as indicated in the image below.
Change its FPS to 10 and leave its looping on.
Create a new animation and name it “jump”. Navigate to “res://Assets/Kings and Pigs/Sprites/01-King Human/Jump (78x58).png” in your Assets folder and crop out your animation frames as indicated in the image below.
Change its FPS to 5 and leave its looping on.
Create a new animation and name it “climb”. Navigate to “res://Assets/Kings and Pigs/Sprites/01-King Human/Door In (78x58).png” in your Assets folder and crop out your animation frames as indicated in the image below.
Change its FPS to 10 and leave its looping on.
Create a new animation and name it “attack”. Navigate to “res://Assets/Kings and Pigs/Sprites/01-King Human/Attack (78x58).png” in your Assets folder and crop out your animation frames as indicated in the image below.
Change its FPS to 7 and turn its looping off.
Create a new animation and name it “damage”. Navigate to “res://Assets/Kings and Pigs/Sprites/01-King Human/Hit (78x58).png” in your Assets folder and crop out your animation frames as indicated in the image below.
Change its FPS to 2 and turn its looping off.
Finally, create a new animation and name it “death”. Navigate to “res://Assets/Kings and Pigs/Sprites/01-King Human/Dead (78x58).png” in your Assets folder and crop out your animation frames as indicated in the image below.
Change its FPS to 5 and turn its looping off.
With our animations created, we can now go ahead and add a function to our Player script that will change our animation based on our input actions captured. Let’s start with our run animation. If our player presses ui_left or ui_right, our run animation should play. We will flip our sprite horizontally so that we can reuse the “run” animation for both left and right movements. We also need to ensure that we are not pressing our ui_jump input, otherwise, we will run whilst jumping! We will use our Input singleton to check for these inputs because we want to call this player_animations() function in our physics_process function so that our animations can be re-captured at each second.
### Player.gd
#older code
#animations
func player_animations():
#on left (add is_action_just_released so you continue running after jumping)
if Input.is_action_pressed("ui_left") || Input.is_action_just_released("ui_jump"):
$AnimatedSprite2D.flip_h = true
$AnimatedSprite2D.play("run")
#on right (add is_action_just_released so you continue running after jumping)
if Input.is_action_pressed("ui_right") || Input.is_action_just_released("ui_jump"):
$AnimatedSprite2D.flip_h = false
$AnimatedSprite2D.play("run")
Now if you comment out your move_and_slide to temporarily stop your player from falling, and we call our player_animations() function in our physics_process() function, we can run our scene and our left and right running animations should play when we press A or D on our keyboards. We will see that the sharp animation flipping is extremely obvious, but that is because of the wide gap in our animation frames (which I explained in the previous part). I apologize for that as it looks ugly — but it will have to do for now.
#older code
#movement and physics
func _physics_process(delta):
# vertical movement velocity (down)
velocity.y += gravity * delta
# horizontal movement processing (left, right)
horizontal_movement()
#applies movement
#move_and_slide()
#applies animations
player_animations()
Next, let’s play our idle animation if our player is not pressing any input action. If you run your scene, your player should idle if they are not running.
### Player.gd
#older code
#animations
func player_animations():
#on left (add is_action_just_released so you continue running after jumping)
if Input.is_action_pressed("ui_left") || Input.is_action_just_released("ui_jump"):
$AnimatedSprite2D.flip_h = true
$AnimatedSprite2D.play("run")
#on right (add is_action_just_released so you continue running after jumping)
if Input.is_action_pressed("ui_right") || Input.is_action_just_released("ui_jump"):
$AnimatedSprite2D.flip_h = false
$AnimatedSprite2D.play("run")
#on idle if nothing is being pressed
if !Input.is_anything_pressed():
$AnimatedSprite2D.play("idle")
Let’s also add our attack input for our attack animation. We will disable this animation from playing later on when we’ve added our attack boosts. We will capture this input in our input() function because we do not want it to process at each second. This will cause the player to have to press it each time they want to attack.
We’ll also create a variable that will check if we are attacking, so that we can stop our other animations from processing if our player is attacking. This will enable us to run then stop to attack and continue running again. In the code below, we see that it only plays our running and idling animations if we are not pressing our attack input which sets our is_attacking variable to true.
### Player.gd
extends CharacterBody2D
#player movement variables
@export var speed = 100
@export var gravity = 200
@export var jump_height = -100
var is_attacking = false
#movement and physics
func _physics_process(delta):
# vertical movement velocity (down)
velocity.y += gravity * delta
# horizontal movement processing (left, right)
horizontal_movement()
#applies movement
#move_and_slide()
#applies animations
if !is_attacking:
player_animations()
#older code
#singular input captures
func _input(event):
#on attack
if event.is_action_pressed("ui_attack"):
is_attacking = true
$AnimatedSprite2D.play("attack")
We also need to reset this variable back to false, otherwise, our player could get stuck in this attack animation. We can do this via the AnimatedSprite2D node’s animation_finished() signal. Signals are a delegation mechanism built into Godot that allows one game object to react to a change in another without them referencing one another. This signal will fire off, or emit, whenever an animation frame has finished playing. When our attack animation has finished playing, we will then reset our is_attacking value back to false so that our other animations can play.
To attach a signal, click on the AnimatedSprite2D node, and in the Node panel underneath Signal, double-click the animation_finished option and connect it to the script where you want to emit this signal. We want it to fire off in our Player script since we are going to change a variable in our Player script.
You’ll see in your script that it created a func on_animated_sprite_2d_animation_finished(). It is here where we will reset our is_attacking variable.
### Player.gd
#older code
#reset our animation variables
func _on_animated_sprite_2d_animation_finished():
is_attacking = false
Now, we can go ahead and add our jump animation. If they are jumping, we want to move the sprite upwards on our y axis according to a certain height. Let’s create a variable that we will call jump_height, and this will contain the max height that our player will move upwards if they are jumping.
### Player.gd
#player movement variables
@export var speed = 100
@export var gravity = 200
@export var jump_height = -100
Now, we can go ahead and tell the code to make the player jump. For this, we will check if the player is pressing the ui_jump input as well as if they are on the floor. We don’t want them to jump endlessly, so we will use the built-in method is_on_floor to check if our player is currently grounded, and if they are, they will be allowed to jump. This method returns true if the body collided with the floor on the last call of move_and_slide. We do not have a floor yet, so we won’t be able to test this out. In the next part, we will add a floor — or a collision — which we will then get to see our players’ jumping animation.
Then, when we are jumping we want to move the player in accordance with our jump_height variable. We can do this by changing the y velocity of our player to be equal to our jump_height value. This will push our player 100 pixels upwards, simulating a jumping effect.
### Player.gd
#older code
#singular input captures
func _input(event):
#on attack
if event.is_action_pressed("ui_attack"):
is_attacking = true
$AnimatedSprite2D.play("attack")
#on jump
if event.is_action_pressed("ui_jump") and is_on_floor():
velocity.y = jump_height
$AnimatedSprite2D.play("jump")
We will add our death and damage functionality for their animations later on. For now, we will add our last animation which is for our climbing. For this, we also need to create a variable that will check if our player is climbing — because we don’t want to play this animation if they aren’t climbing ladders.
### Player.gd
#player movement variables
@export var speed = 100
@export var gravity = 200
@export var jump_height = -100
#movement states
var is_attacking = false
var is_climbing = false
In our input() function, we will check if the player is climbing. This climbing variable will be triggered to return true in another scene later on when we interact with a ladder. If they are climbing, we need to check if they are pressing the ui_up input, along with checking if they are on the floor. If all of this returns true, we play the climbing animation.
We also change the force of gravity to be lower since we want to reduce the effect of gravity, making the character move more slowly while climbing the ladder. After we’ve changed the gravity value, we need to move the character upwards on their x-axis to simulate them “climbing”. We do this by adjusting the vertical velocity of the character, making it move upwards (-200 pixels per second) while “climbing” the ladder.
We also need to reset their climbing value back to false as well as reset their vertical velocity, since they should not be climbing anymore when the player is no longer on a ladder.
### Player.gd
#older code
#singular input captures
func _input(event):
#on attack
if event.is_action_pressed("ui_attack"):
is_attacking = true
$AnimatedSprite2D.play("attack")
#on jump
if event.is_action_pressed("ui_jump") and is_on_floor():
$AnimatedSprite2D.play("jump")
#on climbing ladders
if is_climbing == true:
if Input.is_action_pressed("ui_up"):
$AnimatedSprite2D.play("climb")
gravity = 100
velocity.y = -200
#reset gravity
else:
gravity = 200
is_climbing = false
Your code should now look like this.
If you run your scene now, your player should be able to run left and right (in one place, since you can only test this if you comment out your move_and_slide() method), as well as attack. We can’t yet see their climbing or jumping, but we’ll get onto fixing that issue in the next part.
Congratulations on creating the base for your player’s movement animations! In the next part, we will create our first level, which we will also be able to test our climbing and jumping animations out on. Now would be a good time to save your project and make a backup of your project so that you can revert to this part if any game-breaking errors occur. Go back and revise what you’ve learned before you continue with the series, and once you’re ready, I’ll see you in the next part!
Next Part to the Tutorial Series
The tutorial series has 24 chapters. I’ll be posting 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 24 parts of this series on my GitBook. If you don’t see a link added to a part yet, then that means that it hasn’t been posted yet. Also, if there are any future updates to the series, my GitBook would be the place where you can keep up-to-date with everything!
Support the Series & Gain Early Access!
If you like this series and would like to support me, you could donate any amount to my KoFi shop or you could purchase the offline PDF that has the entire series in one on-the-go booklet!
The booklet gives you lifelong access to the full, offline version of the “Learn Godot 4 by Making a 2D Platformer” PDF booklet. This is a 451-page document that contains all the tutorials of this series in a sequenced format, plus you get dedicated help from me if you ever get stuck or need advice. This means you don’t have to wait for me to release the next part of the tutorial series on Dev.to or Medium. You can just move on and continue the tutorial at your own pace — anytime and anywhere!
This book will be updated continuously to fix newly discovered bugs, or to fix compatibility issues with newer versions of Godot 4.