DEV Community

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

Posted on • Updated on

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

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.

Godot Learning RPG

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.

Godot Learning RPG

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

Godot Learning RPG

Change its FPS value to 6.

Godot Learning RPG

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

Godot Learning RPG

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

Godot Learning RPG

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

Godot Learning RPG

Change its FPS value to 1 and turn off looping.

Godot Learning RPG

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

Godot Learning RPG

Change its FPS value to 14 and turn off looping.

Godot Learning RPG

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:

Godot Learning RPG

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

Godot Learning RPG

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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!

Godot Learning RPG

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.

Godot Learning RPG

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.

Godot Learning RPG

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](https://app.mural.co/t/godot5664/m/godot5664/1680361240017/5d99128c3275a0c6ff47397bb4ce560b08de26a5?sender=christinecoomans304568).

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.


Godot Learning RPG

Godot Learning RPG

Godot Learning RPG

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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!

Godot Learning RPG

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.

Buy Me a Coffee at ko-fi.com


FULL TUTORIAL

Godot RPG

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

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!😊

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

Top comments (13)

Collapse
 
jknech profile image
jknech

Thank you for this great tutorial Christine - it’s very well organized and detailed… very much appreciated by this beginner!

I am having an issue (perhaps) after completing lesson 4 (animating the player). Everything runs fine, but I do get a message in the debugger anytime two direction keys are held or pressed at the same time (so moving diagonally) - it repeats the error message that in the player_animations() function there is no animation named walk (it references line 49 from your finished code). As I said, it doesn’t appear to impact functionality (although perhaps it may be and I’m not yet aware) but I wonder if I’ve missed something and what I might do to troubleshoot and eliminate the error.

Thank you
J

Collapse
 
christinec_dev profile image
christine • Edited

Hey there! Yes, don't worry about this issue as it doens't break the game - but if it bothers you I do have a fix for you :)

In your player_animations() function, you create an animation name like this:

animation = "walk_" + returned_direction(new_direction)
Enter fullscreen mode Exit fullscreen mode

The returned_direction(new_direction) function can return "up", "down", "side", or "" - but never "walk". It seems the latter case "walk_" is causing the error, because there's no animation direction defined with that name. This causes the returned_direction(new_direction) function to return an empty string, which causes "walk_" to be passed into the play function. This happens when the direction is not perfectly up, down, left, or right (when you're pressing two buttons together).

To fix this, we can set replace our "" with a direction instead in our returned_direction() function. Do this:

#returns the animation direction
func returned_direction(direction : Vector2):
    var normalized_direction  = direction.normalized()

    if normalized_direction.y >= 1:
        return "down"
    elif normalized_direction.y <= -1:
        return "up"
    elif normalized_direction.x >= 1:
        $AnimatedSprite2D.flip_h = false
        return "side"
    elif normalized_direction.x <= -1:
        $AnimatedSprite2D.flip_h = true
        return "side"

    #default value is a direction from above
    return "side"
Enter fullscreen mode Exit fullscreen mode

Now when you press two buttons together, your side animation should always play and the walk error should be fixed. If you don't want the side animation to always play, you could always return "walk".

#returns the animation direction
func returned_direction(direction : Vector2):
    var normalized_direction  = direction.normalized()

    if normalized_direction.y >= 1:
        return "down"
    elif normalized_direction.y <= -1:
        return "up"
    elif normalized_direction.x >= 1:
        $AnimatedSprite2D.flip_h = false
        return "side"
    elif normalized_direction.x <= -1:
        $AnimatedSprite2D.flip_h = true
        return "side"

    #default value
    return "walk"
Enter fullscreen mode Exit fullscreen mode

Which will loop your walk animation if you press two inputs. To stop it from looping even after you've released your inputs, you can add this code to your physics_process() function:

func _physics_process(delta):
       #older code  
       if !Input.is_anything_pressed():
        animation  = "idle_" + returned_direction(new_direction)    
Enter fullscreen mode Exit fullscreen mode

Using any of the two fixes above should resolve your error. Give it a try, and let me know if it works. Happy coding! 😊

Collapse
 
jknech profile image
jknech

Thanks for the quick reply Christine - very much appreciated!

I tried the first of the two solutions you recommended and it seems to work like a charm - the debugger has stopped spitting out errors :)

I plan on continuing through the tutorial (planning on paying for the PDF guide as well). Looking forward to the journey and any future content/support you provide!

Best
J

Thread Thread
 
christinec_dev profile image
christine

I'm glad it worked for you. Have a happy week of coding ahead! 😁

Thread Thread
 
sirneij profile image
John Owolabi Idogun

Welcome to the community ☺️. Hope you are having a great time.

Thread Thread
 
christinec_dev profile image
christine

Thanks for welcoming people John. 😅

 
sirneij profile image
John Owolabi Idogun

Welcome to the community. Hope you get to enjoy being here.

Collapse
 
fauxpas8008 profile image
Paul

I followed ( I think!) the guide and when I added the default "side" option at the end of func returned_direction I noticed that if I moved left, then pressed up or down + right, the flip_h value was still set to left causing me to moonwalk.
I did the below and no more moonwalking

func returned_direction(direction : Vector2):
    var normalized_direction = direction.normalized()

    if normalized_direction.y >= 1:
        return "down"
    elif normalized_direction.y <= -1:
        return "up"
    elif normalized_direction.x >= 1:
        #right
        $AnimatedSprite2D.flip_h = false
        return "side"
    elif normalized_direction.x <= -1:
        #left
        $AnimatedSprite2D.flip_h = true
        return "side"

    else:
        if Input.is_action_pressed("ui_left"):
            $AnimatedSprite2D.flip_h = true 
        elif Input.is_action_pressed("ui_right"):
            $AnimatedSprite2D.flip_h = false    
        return diagonal_default_player_direction
Enter fullscreen mode Exit fullscreen mode
Collapse
 
fauxpas8008 profile image
Paul

Thank you for this, really helpful and detailed.
I love that this isn't skin deep.

Correction:
attack_up animation has 6 frames not 8

Collapse
 
christinec_dev profile image
christine

Thanks for all of your suggestions Paul! I'll amend it. 😊

Collapse
 
alex2025 profile image
Alex

Hi Christine! Thanks for the tutorial...I learn by building complete projects vs small tutorials, so I was excited when I came across this series.

I ran into an error when adding "func _input(event)". It said that new_direction was not defined...and it certainly is not defined in the scope of this function. I took a look at your script and saw that you had it defined at the top of the script, but there is no mention of that in the tutorial itself. It may confuse those who are just following along and may not know much about programming yet. Just an FYI!

Collapse
 
tavernsagas profile image
James

Aka me. lmao im yelling IT IS DEFINED IM LOOKING RIGHT AT IT! xD

Collapse
 
cosmicgraveyard215 profile image
Tanner West

Thank you for this! You resolved my issue!