In this tutorial we will learn how to use the AnimatedSprite node to animate sprites. In particular, we will animate our player by adding idle animations, walk animations and two types of attacks (sword and fireball).

Preparing player node

Let’s open the SimpleRPG project and pick up where we left off in the last tutorial. To get started, we need to import all the animation frames into the project. Download player sprites by pressing the button below.

Download “SimpleRPG Player Frames” player_frames.zip – 35 KB

In the FileSystem panel, delete player.png, import all new images and put them in the Entities/Player folder.

Select all the images you just added, go to the Import panel, disable Filter and click on Reimport.

Back to the Scene panel, right-click on the Sprite node and choose Change Type.

Select AnimatedSprite and press the Change button.

Change node type to AnimatedSprite

Once the node type is changed, a yellow triangle appears next to Sprite. This warning tells us that we need to create a SpriteFrames resource to show something on screen.

SpriteFrames is a resource (i.e. a data container) that contains all the textures that we will use as frames, as well as the list of all animations with their properties (speed, loop playback, etc.)

To create a SpriteFrames resource, go to the Inspector, click on [empty] next to the Frames property and select New SpriteFrames.

Click on the newly created SpriteFrames resource. The SpriteFrames editor will open.

The SpriteFrames editor will open at the bottom of the window.

The SpriteFrames editor

The SpriteFrames editor panel is divided into two columns. The left column lists all the animations, while on the right there is the list of the frames that make up the selected animation.

The left column has a toolbar with two buttons, one to add a new animation and one to delete the selected animation. At the bottom of the column, you can set the playback speed (in frames per second) and decide whether the animation should be looped or not.

In the toolbar of the right column there are 8 buttons:

  • Load Resource: to add a resource to the animation frames. More simply, sprites can be dragged and dropped from the FileSystem panel.
  • Copy: copy the selected frame.
  • Paste: paste the previously copied frame.
  • Insert empty before: inserts an empty frame before the selected one.
  • Insert empty after: inserts an empty frame after the selected one.
  • Move before: move the selected frame back one position.
  • Move after: move the selected frame forward one position.
  • Delete: deletes the selected frame.

To start, rename the default animation by clicking on it and call it down_idle. Drag the player_down_idle_1.png and player_down_idle_2.png files from the FileSystem panel to the right side of the SpriteFrames editor and then set Speed (FPS) to 1.

Now, in the Inspector, enable the Playing property. You will see the sprite animate with the newly created idle animation.

The down_idle animation

With the same procedure, create all the other animations. The table below contains all the information you need to create them.

Animation nameFramesSpeed (FPS)Loop
down_attackplayer_down_idle_1.png
player_down_attack_1.png
player_down_attack_2.png
7Off
down_fireballplayer_down_idle_1.png
player_down_fireball_1.png
player_down_fireball_2.png
7Off
down_idleplayer_down_idle_1.png
player_down_idle_2.png
1On
down_walkplayer_down_idle_1.png
player_down_walk_1.png
player_down_idle_1.png
player_down_walk_2.png
10On
left_attackplayer_left_idle_1.png
player_left_attack_1.png
player_left_attack_2.png
7Off
left_fireballplayer_left_idle_1.png
player_left_fireball_1.png
player_left_fireball_2.png
7Off
left_idleplayer_left_idle_1.png
player_left_idle_2.png
1On
left_walkplayer_left_idle_1.png
player_left_walk_1.png
player_left_idle_1.png
player_left_walk_2.png
10On
right_attackplayer_right_idle_1.png
player_right_attack_1.png
player_right_attack_2.png
7Off
right_fireballplayer_right_idle_1.png
player_right_fireball_1.png
player_right_fireball_2.png
7Off
right_idleplayer_right_idle_1.png
player_right_idle_2.png
1On
right_walkplayer_right_idle_1.png
player_right_walk_1.png
player_right_idle_1.png
player_right_walk_2.png
10On
up_attackplayer_up_idle_1.png
player_up_attack_1.png
player_up_attack_2.png
7Off
up_fireballplayer_up_idle_1.png
player_up_fireball_1.png
player_up_fireball_2.png
7Off
up_idleplayer_up_idle_1.png
player_up_idle_2.png
1On
up_walkplayer_up_idle_1.png
player_up_walk_1.png
player_up_idle_1.png
player_up_walk_2.png
10On

In the Inspector, you can set the Animation property to the animation you want to preview. When you’ve finished creating the animations, set it to down_idle. It will be the default animation when the game starts.

Playing animations with script

Open the Player script. To choose which animation to play, we will write a new function named animates_player(). We will call this function at the end of _physics_process(), after the player’s movement has been calculated.

Let’s start by writing a simplified version of the animates_player() function to become familiar with how 2d sprite animation works.

Copy the following code into the script:

func animates_player(direction: Vector2):
	if direction != Vector2.ZERO:
		# Play walk animation
		$Sprite.play("down_walk")
	else:
		# Play idle animation
		$Sprite.play("down_idle")

The function animate_player() takes a Vector2 argument that represents the direction of the player movement.

The first line checks if the direction is different from zero (Vector2.ZERO is a constant equivalent to Vector2(0,0)). If true, the player is moving and the play() method of the Sprite node is called to play the down_walk animation. If false, it means that the player is still, and so the down_idle animation is performed. The $ operator, as we saw in Godot Tutorial – Part 3: First project, is used to get the reference to a child node.

Warning! Even if the node is still called Sprite, I remind you that now it’s of AnimatedSprite type. Do not confuse the name of the node with its type! play() is an AnimatedSprite method that is not available in Sprite-type nodes.

Now, at the bottom of the _physics_process() function, add this line to call animates_player():

# Animate player based on direction
animates_player(direction)

If you try to run the game, you will see that when the player is still, the idle animation is performed, while when it is moving, the walk animation is shown as expected.

Choosing the right animation

Obviously, we don’t want to show the player always facing down, but we want to choose which animation to play based on the direction in which the player is facing.

To do this, we first need to declare a new variable at the beginning of the script:

var last_direction = Vector2(0, 1)

In this variable we will store the last direction in which the player moved before stopping. We set the initial value to Vector2 (0,1) to point the player down. This variable will help us decide which idle animation to show.

Now, we have to write the code that chooses whether to use the down, up, left or right animations, based on the direction vector. Since we will use this code on more than one occasion, we will write it in a new function, called get_animation_direction():

func get_animation_direction(direction: Vector2):
	var norm_direction = direction.normalized()
	if norm_direction.y >= 0.707:
		return "down"
	elif norm_direction.y <= -0.707:
		return "up"
	elif norm_direction.x <= -0.707:
		return "left"
	elif norm_direction.x >= 0.707:
		return "right"
	return "down"

How does this code work? First, it normalizes the direction vector to make sure it has length 1 (it can be less than 1 when using an analog stick). If we draw this normalized vector on a Cartesian plane, it will always be on a point of the unit circle (i.e. a circle with radius 1):

Let’s divide the plan into four quadrants as in the following figure:

Each area contains a quarter of the unit circle. Each of these arcs corresponds to one of the 4 possible directions of the animations.

How do we decide in which of these arcs is our vector? I won’t bore you with calculations, but it can be shown that all the points of the lower arc have the y coordinate greater than the square root of 2 over 2, or about 0.707, while those of the upper arc all have a value of y less than -0.707. Same thing for left and right, but in that case using the x coordinate.

Thus, the function simply checks the values of the x and y coordinates of the vector to decide in which of these arcs it’s located. Based on that, it returns the animation string prefix.

The return statement is used to end the execution of the function call and return the result value of the function. You can exit from a function at any point. If you don’t use the return keyword, the default return value is null.

Now, we can edit animates_player() to choose the correct animation direction:

func animates_player(direction: Vector2):
	if direction != Vector2.ZERO:
		# update last_direction
		last_direction = direction
		
		# Choose walk animation based on movement direction
		var animation = get_animation_direction(last_direction) + "_walk"
		
		# Play the walk animation
		$Sprite.play(animation)
	else:
		# Choose idle animation based on last movement direction and play it
		var animation = get_animation_direction(last_direction) + "_idle"
		$Sprite.play(animation)

As you can see, when the player is moving, we save in last_direction the current value of direction, so that it can be used later to choose the right idle animation.

We use get_animation_direction() to get the direction prefix, to which we append, with the + operator, the string indicating which animation we want (_walk or _idle). Then, as before, we use the play() method to play the animation.

If you try the game now, you will see that the player turns in the direction in which he walks. When the player stops, it remains oriented in the last direction of movement.

Walking speed

If you are using a joypad with analogue sticks, you will have noticed that when the player moves slowly, the walk animation is still performed at the speed of 10 FPS we initially set. This is visually unpleasant, because the speed of the animation is too high compared to the actual movement of the player on the map. We must therefore adjust the animation FPS based on player speed.

Doing it is quite simple, just add this new line to animates_player() before playing the walk animation:

$Sprite.frames.set_animation_speed(animation, 2 + 8 * direction.length())

This line accesses the SpriteFrame resource of the Sprite node and uses the set_animation_speed() method to set the FPS of the animation indicated by the first argument. The second argument is the number of FPS, which we calculate with a formula that returns a value from 2 to 10, depending on the length of the direction vector (which can range from 0 to 1).

Analog stick bounce problem

When using an analog stick, it can happen that when the player walk in one direction and suddenly release the stick, he end up facing the opposite direction. This can happen or not depending on the controller and the way the player handle the stick.

This is due to the fact that, whenever you release the stick, it returns back to its center position. But the spring that causes this movement is so strong, that the stick actually overshoots the center, going beyond for a short amount of time.

To solve this problem, we must ensure that the last_direction vector is not updated instantaneously with every change of direction, but that the change is gradual to filter out this “noise”. To do this, replace the line that updates last_direction with this:

# gradually update last_direction to counteract the bounce of the analog stick
last_direction = 0.5 * last_direction + 0.5 * direction

Attack and fireball animations

The player, in addition to moving around the map, can attack enemies with his sword or casting fireballs. Since we have already created the animations for these actions, we want to play them when the player presses the attack keys.

First of all, we need to create input actions in the Input Map panel, which you can access by clicking on Project → Project Settings → Input Map. We will create two actions:

  • attack: to attack with the sword; we will assign to this action the Space key and the Button 2 of the joypad (which corresponds to the X button of the XBox controller and to the Square button of the PlayStation controller)
  • fireball: to cast a fireball; we will assign to this action the Control button and the Button 3 of the joypad (which corresponds to the Y button of the XBox controller and to the Triangle button of the PlayStation controller).

To add a new action, enter the name in the Action text field at the top of the panel, and press Add. We have already seen the procedure for mapping an input to an action in the Godot Tutorial – Part 5: Player movement, so refer to that if you don’t remember how to do it. The result will be the following:

Close the Project Settings window, go back to the script and add a new variable that we will use to find out if there is an attack animation in progress:

var attack_playing = false

We need this variable because animates_player() is called continuously by _physics_process(), so the attack animation would not have the time to be executed because it would be immediately replaced by a walk or idle animation. Because of this, we need to edit the _physics_process() code to call animates_player() only if attack_playing is false:

# Animate player based on direction
if not attack_playing:
	animates_player(direction)

To handle attack input, we’ll use the _input method inherited from Node. This function is called whenever any input event occurs.

Add this code to the script:

func _input(event):
	if event.is_action_pressed("attack"):
		attack_playing = true
		var animation = get_animation_direction(last_direction) + "_attack"
		$Sprite.play(animation)
	elif event.is_action_pressed("fireball"):
		attack_playing = true
		var animation = get_animation_direction(last_direction) + "_fireball"
		$Sprite.play(animation)

If you followed Godot Tutorial – Part 5.1: Dragging the player with the mouse, then you already have this function in the script. You can add this code next to the existing one without any problems.

The function works like this: if one of the key/button assigned to the attack action is pressed, attack_playing is set to true, in order to bypass the animates_player() function. The correct animation is then chosen based on the last direction of the player and performed with the play() method. The same goes for the fireball action.

If you run the game now, you will see that the attack animations are performed correctly, but then the sprite remains on the last frame of the animation and the walk and idle animations are no longer played. This happens because attack_playing is not reset to false anywhere. We must then add some code that resets the variable when the animation ends.

Fortunately, AnimatedSprite provide the animation_finished() signal, which is sent each time the last frame of an animation is played. We will link this signal to a function in our script that resets attack_playing.

Open the Node panel and, if it is not already selected, click on Signals to see the list of signals.

All signals of AnimatedSprite

Select animation_finished() and press Connect. A window will open to select the node to connect with the signal. Select Player and press Connect. The script will open with the cursor positioned on the function that was just created. Edit it like this:

func _on_Sprite_animation_finished():
	attack_playing = false

As a last thing, we want the player speed to decrease when it’s simultaneously moving and attacking. To do this, simply add these lines to _physics_process(), just before calling move_and_collide():

if attack_playing:
	movement = 0.3 * movement

Conclusions

In this tutorial we learned how to use AnimatedSprite and SpriteFrames to animate sprites and how to use scripts to run animations. Furthermore, we have deepened our knowledge of the Godot input system.

You can try what we’ve done so far by clicking here:

In the next tutorial we will start developing the game GUI.




31 Comments

Ben · November 3, 2019 at 5:24 pm

Thanks for all the work on these so far! I’ve noticed one interesting behavior after this chapter, and I can’t seem to fix it. When moving down and right (1, 1) or up and left (-1, -1), the attack animation will not play. The other six directions (up, up/right, right, down, down/left, left) work just fine, and, even more oddly, the fireball works in all eight directions. I did some investigating in my own code and the only guess I have is that somehow the call to _input() just isn’t happening when moving in those directions. I added print(event) as the first line inside _input() and it does not print anything when pressing space and moving in those directions, but it does with every other combination. I also tested the web version you uploaded and the behavior is the same. Is this a bug in Godot? or is something weird happening with the direction math?

Thanks!

    Davide Pesce · November 3, 2019 at 8:34 pm

    Hi Ben, I tried to replicate your problem but without success, both within Godot and playing the game in the browser.
    The only thing that comes to mind is that your keyboard suffers from ghosting, and cannot detect when you press space while pressing certain arrow key combinations. Go to this page and try testing the key combinations that don’t work. If this is the problem, you can only solve it by mapping the attack action to another key. Otherwise, I will try to investigate further.

      Ben · November 5, 2019 at 10:01 pm

      Wow. I didn’t even realize that could be a thing. According to that site, my space bar does indeed fail to register when pressed with certain combinations of arrow keys (and there appear to be a few other conditions that will trigger this as well). For good measure, I tested it on a different keyboard and it worked just fine. Thanks for the keyboard tip. I’ve never had other issues with it so that possibility never even crossed my mind.

Matthew · December 27, 2019 at 2:19 am

I for the the life of me cant get the attack animation to play ive stuck for days… I know the key is working because the mana bar is going down when i press fireball but the animation will not play

    Davide Pesce · January 1, 2020 at 4:43 pm

    To help you I need more details. If you want, write me in private on the Contact page.

    Anthony Uzmezler · February 3, 2020 at 12:40 am

    did you figure it out?

      Davide Pesce · February 3, 2020 at 10:28 am

      He never contacted me. Do you have the same issue? If so contact me privately, I have to see your project to understand the problem.

James · January 10, 2020 at 11:41 am

I’ve been following your tutorials since day one, but i’m having problems with where to paste some of your codes. I will be glad if you can take a screenshot of all the codes and how you have arranged them in godot. Right now when I try to play the game it is just loading and it says that Godot is having problems.

    Davide Pesce · January 10, 2020 at 2:03 pm

    You can find the complete code of the project on GitHub.

      James · January 13, 2020 at 12:06 pm

      Thank You !
      I checked GitHub and I actually found the codes updates, however i tried to copy them and paste them in godot . After doing that another error came up.
      On func _input_event(event):
      Godot is saying error parsing expression, misplaced: func

      I’ll be glad if you can check on that. So far I’m enjoying these tutorials and discovering a lot too.
      Thank You.

        Davide Pesce · January 13, 2020 at 2:49 pm

        I double-checked the code on GitHub and it works, so probably in the copy-paste you have lost some tabs (in GDScript indentation is essential to identify a block of code).

        If you download the entire project from GitHub and import it into Godot does it work?

          James · January 14, 2020 at 1:19 pm

          Hey Davide Pesce !

          I tried downloading the whole project and running it. It actually works fine. I’ll try following the tutorial carefully again and see where i’ve missed out. It was really great experiencing the end result of the project. How I wish if you could’ve added buttons such as “Play”, “Pause”, “Quit” and the like.

          Thank you for the information now I know it is not the game or my PC Probably I just missed some important information.

          Davide Pesce · January 14, 2020 at 2:56 pm

          How I wish if you could’ve added buttons such as “Play”, “Pause”, “Quit” and the like.

          Maybe in one of the next tutorials I will add a pause screen where you can quit the game. If you want to know how to pause the game, I have already talked about it in this tutorial.

James · January 13, 2020 at 4:13 pm

Thank you.
I’ll try to download the whole project and see what happens. Thanks for tips though. I’ll let you know as soon as u try it out.
Thanks

Anastasia · February 26, 2020 at 6:31 pm

Hello! Decided to go trough this tutorial after all and I encountered a problem. it’s suposed to work fine, but i get an error saying “cant play() based on null instances or something like that…

extends KinematicBody2D

# Player movement speed
export var speed = 75
var last_direction = Vector2(0, 1)

func get_animation_direction(direction: Vector2):
var norm_direction = direction.normalized()
if norm_direction.y >= 0.707:
return “down”
elif norm_direction.y <= -0.707:
return "up"
elif norm_direction.x = 0.707:
return “right”

func animates_player(direction: Vector2):
if direction != Vector2.ZERO:
# update last_direction
last_direction = direction

# Choose walk animation based on movement direction
var animation = get_animation_direction(last_direction) + “_walk”

# Play the walk animation
$Sprite.play(animation)
else:
# Choose idle animation based on last movement direction and play it
var animation = get_animation_direction(last_direction) + “_idle”
$Sprite.play(animation)

func _physics_process(delta):
# Get player input
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
move_and_collide(movement)
animates_player(direction)

Anastasia · February 26, 2020 at 6:32 pm

Also here it doesn’t show the tabs for some reason?

Anastasia · February 26, 2020 at 6:40 pm

OKOK nvm my sprite was named player whoops

Alex · February 27, 2020 at 11:20 pm

Hello David!
I’m trying to implement the “last_direction = 0.5*last_direction + 0.5*direction”, but keep getting this error: “Invalid operands ‘Nil’ and ‘String’ in operator ‘+’.” Checked your github to see if I entered the code in the right place, and I did, so I have no idea why this doesn’t work for me.

Oded · March 1, 2020 at 1:43 pm

I’ve been following and learning from your great tutorial. So first of all thanks a whole lot!

Both in your version (tutorial 8 conclusion) and in mine I see an animation problem when attacking repeatedly. Sometimes it gets stuck on the last frame of attack/fireball and only releases when the other attack action is pressed – like an edge case where _on_Sprite_animation_finished() isn’t called (movement still works but w/o animations).
Did you notice that? Do you know how it happens?

    Davide Pesce · March 1, 2020 at 8:13 pm

    Hi Oded, it has never happened to me, I will try to reproduce the problem and let you know.

    Davide Pesce · March 1, 2020 at 8:45 pm

    Sorry Oded, I didn’t realize we’re at tutorial 8! The problem will be solved in tutorial 11, when attack cooldown times are introduced. As you said, the problem is due to the attack animation that is not yet finished when the next attack starts.

      Oded · March 2, 2020 at 7:43 am

      Thanks for taking your time to reply, I’ll keep following and see if it’s solved for me as well later.

      I’d love to see more of your guides, your style and clarity are awesome

Anastasia · March 1, 2020 at 2:28 pm

Okkkkk I have another problem :/. Im a biiit stuck with this thing:
if i press the mana button or the attack button: nothing happens! Exeeept the attack_playing turns true and:
https://vimeo.com/394757529
It slows down permanently??

Cant find the problem: maybe you can?
btw yes i did conect the bars

extends KinematicBody2D

var health = 100
var health_max = 100
var health_regeneration = 1
var mana = 100
var mana_max = 100
var mana_regeneration = 2
var attack_playing = false

signal player_stats_changed

# Player movement speed
export var speed = 75
var last_direction = Vector2(0, 1)

func _ready():
emit_signal(“player_stats_changed”, self)

func _input(event):
if event.is_action_pressed(“attack”):
attack_playing = true
var animation = get_animation_direction(last_direction) + “_attack”
$Sprite.play(animation)
elif event.is_action_pressed(“fireball”):
if mana >= 25:
mana = mana – 25
emit_signal(“player_stats_changed”, self)
attack_playing = true
var animation = get_animation_direction(last_direction) + “_fireball”
$Sprite.play(animation)

func _on_Sprite_animation_finished():
attack_playing = false

func _process(delta):
# Regenerates mana
var new_mana = min(mana + mana_regeneration * delta, mana_max)
if new_mana != mana:
mana = new_mana
emit_signal(“player_stats_changed”, self)

# Regenerates health
var new_health = min(health + health_regeneration * delta, health_max)
if new_health != health:
health = new_health
emit_signal(“player_stats_changed”, self)

func get_animation_direction(direction: Vector2):
var norm_direction = direction.normalized()
if norm_direction.y >= 0.707:
return “down”
elif norm_direction.y <= -0.707:
return "up"
elif norm_direction.x = 0.707:
return “right”

func animates_player(direction: Vector2):
if direction != Vector2.ZERO:
# update last_direction
last_direction = direction

# Choose walk animation based on movement direction
var animation = get_animation_direction(last_direction) + “_walk”

# Play the walk animation
$Sprite.play(animation)
else:
# Choose idle animation based on last movement direction and play it
var animation = get_animation_direction(last_direction) + “_idle”
$Sprite.play(animation)

func _physics_process(delta):
# Get player input
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
if attack_playing:
movement = 0.3 * movement
move_and_collide(movement)
animates_player(direction)

    Davide Pesce · March 1, 2020 at 9:19 pm

    Hi Anastasia, an if statement is missing at the end of your _phyisics_process function. It must end in this way:

    if not attack_playing:
    	animates_player(direction)
    

      Anastasia · March 7, 2020 at 5:32 pm

      Hellooo! No idea how i missed that, thank you!
      Now it keeps on looping… is that normal or did i make another silly mistake?

        Anastasia · March 7, 2020 at 5:35 pm

        Indeed a silly mistake, I forgot to conect the animated sprite to my player!

Cr0 · March 3, 2020 at 9:38 am

Hello David
I keep getting this error “Invalid operands ‘Nil’ and ‘String’ in operator ‘+’.” when I move up using the arrow key, but if I start moving down, and then left or right, then I can move freely, without errors. I have copied your code fro github, to see if it was my code that was wrong, but I still get the error. Is there a potential fix to this problem?

    Davide Pesce · March 3, 2020 at 8:10 pm

    Surely there is a fix! To find it, however, I need to see your project. If you can upload your project to Dropbox or Google Drive and send me the link (here or privately via the Contact page) I take a look at it and let you know!

    Davide Pesce · March 4, 2020 at 1:51 pm

    Ok, I found the problem: at the end of the get_animation_direction function the return “down” line is missing. This line is executed only in the (rare) case where its argument is zero. I corrected this problem in tutorial 9 but forgot to update the code here.
    So, the updated function code is this:

    func get_animation_direction(direction: Vector2):
    	var norm_direction = direction.normalized()
    	if norm_direction.y >= 0.707:
    		return "down"
    	elif norm_direction.y <= -0.707:
    		return "up"
    	elif norm_direction.x <= -0.707:
    		return "left"
    	elif norm_direction.x >= 0.707:
    		return "right"
    	return "down"
    

      Cr0 · March 5, 2020 at 8:19 am

      Thanks for the solution, it worked. Keep up the good work, I really like these tutorials.

Leave a Reply

Your email address will not be published. Required fields are marked *

By continuing to browse this Website, you consent to the use of cookies. More information

This Website uses:

By continuing to browse this Website without changing the cookies settings of your browser or by clicking "Accept" below, you consent to the use of cookies.

Close