Let’s Learn Godot 4 by Making an RPG — Part 10: Animating AI Movement🤠
What good is an enemy if it just floats around our map as a static image? That won’t do, so let’s get to work on implementing our animations so that our enemy can come to life. I also have a surprise for you: we already wrote most of our enemy’s animation code. Well, not really, I mean it’s still in the Player code, but that means we can go ahead and copy and paste over some code which speeds up our development time tremendously.
WHAT YOU WILL LEARN IN THIS PART:
· How to add animations to non-controllable nodes.
· Further practice with Vectors.
In your Player script, we want to copy two entire functions over to our Enemy script. The first one you should copy is your func player_animations(direction: Vector2) function, and the second one is your func returned_direction(direction: Vector2) function. Rename your player_animations() function in your Enemy script to enemy_animations().
### Enemy.gd
extends CharacterBody2D
# Node refs
@onready var player = get_tree().root.get_node("Main/Player")
@onready var animation_sprite = $AnimatedSprite2D
# Enemy stats
@export var speed = 50
var direction : Vector2 # current direction
var new_direction = Vector2(0,1) # next direction
var animation
# older code
# Animation Direction
func returned_direction(direction : Vector2):
#it normalizes the direction vector to make sure it has length 1 (1, or -1 up, down, left, and right)
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
# Animations
func enemy_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)
To activate these animations for our enemy’s movement, we’ll have to first check if any other animations are playing (such as attack or death animations), and if not, we play them in our physics_process() function. Once again we did the same for our Player character, so this should not be too confusing for you to understand.
Copy in the is_attacking variable from your Player’s code.
### Enemy.gd
extends CharacterBody2D
# Node refs
@onready var player = get_tree().root.get_node("Main/Player")
@onready var animation_sprite = $AnimatedSprite2D
# Enemy stats
@export var speed = 50
var direction : Vector2 # current direction
var new_direction = Vector2(0,1) # next direction
var animation
var is_attacking = false
Then in your physics_process() function, let’s call our enemy_animations() function to play our enemy’s animations if they are not attacking.
### Enemy.gd
# older code
# ------------------------- Movement & Direction ---------------------
# Apply movement to the enemy
func _physics_process(delta):
var movement = speed * direction * delta
var collision = move_and_collide(movement)
#if the enemy collides with other objects, turn them around and re-randomize the timer countdown
if collision != null and collision.get_collider().name != "Player":
#direction rotation
direction = direction.rotated(rng.randf_range(PI/4, PI/2))
#timer countdown random range
timer = rng.randf_range(2, 5)
#if they collide with the player
#trigger the timer's timeout() so that they can chase/move towards our player
else:
timer = 0
#plays animations only if the enemy is not attacking
if !is_attacking:
enemy_animations(direction)If you were to run your enemy scene from here, you might notice that the enemy will try and attack your player — but the problem is that they will try and attack your player even when they are turned away from our player.
If you were to run your enemy scene from here, you might notice that the enemy will try and attack your player — but the problem is that they will try and attack your player even when they are turned away from our player.
In our timeout() function, we are setting the new_direction for animation, but this is not updating our new_direction accurately. To fix this, we need to make sure that our new_direction is set accurately during an attack and that it is in sync with the direction the enemy is facing.
For that, we will create a new function that syncs our new_direction with the actual movement direction of our enemy. We will then call this function whenever the enemy moves or rotates. This will make sure that our new_direction is accurately representing the direction the enemy is facing when it’s attacking.
Let’s create a new function that will sync our new_direction. You can do this above your timeout() function.
### Enemy.gd
#older code
#syncs new_direction with the actual movement direction and is called whenever the enemy moves or rotates
func sync_new_direction():
if direction != Vector2.ZERO:
new_direction = direction.normalized()
Then, we’ll call this function in the func _on_timer_timeout(): function, which is the place where the enemy decides its behavior (whether it should attack, chase, or roam randomly). This ensures that whenever the enemy updates its behavior, it also updates its direction for animations accordingly.
### Enemy.gd
# older code
# ------------------------- Movement & Direction ---------------------
func _on_timer_timeout():
# Calculate the distance of the player's relative position to the enemy's position
var player_distance = player.position - position
#turn towards player so that it can attack
if player_distance.length() <= 20:
new_direction = player_distance.normalized()
sync_new_direction()
direction = Vector2.ZERO
#chase/move towards player to attack them
elif player_distance.length() <= 100 and timer == 0:
direction = player_distance.normalized()
sync_new_direction()
#random roam radius
elif timer == 0:
#this will generate a random direction value
var random_direction = rng.randf()
#This direction is obtained by rotating Vector2.DOWN by a random angle between 0 and 2π radians (0 to 360°).
if random_direction < 0.05:
#enemy stops
direction = Vector2.ZERO
elif random_direction < 0.1:
#enemy moves
direction = Vector2.DOWN.rotated(rng.randf() * 2 * PI)
sync_new_direction()
Remember, your enemy will attack in the direction of your player’s last known location when they started attacking, so it is natural if there is a delay in them turning towards your player’s new location when attacking. For example, if you were on their left when they started attacking and then you suddenly ran to the right, they will finish their attack animation towards the left first before redirecting to the right.
If you run your scene now you will see that your enemy character idles and animates according to its current direction.
You might notice another issue here: our side animation never plays. This is because our existing returned_direction() function checks the y-direction first and then the x-direction. This means that if there’s any y-movement, it will always prioritize the up/down animations over the side animations. To fix this, we should prioritize the x-direction when it’s dominant:
### Enemy.gd
# older code
# ------------------------- Movement & Direction ---------------------
# Animation Direction
func returned_direction(direction : Vector2):
var normalized_direction = direction.normalized()
var default_return = "side"
if abs(normalized_direction.x) > abs(normalized_direction.y):
if normalized_direction.x > 0:
#(right)
$AnimatedSprite2D.flip_h = false
return "side"
else:
#flip the animation for reusability (left)
$AnimatedSprite2D.flip_h = true
return "side"
elif normalized_direction.y > 0:
return "down"
elif normalized_direction.y < 0:
return "up"
#default value is empty
return default_return
Now our enemy will play their side animations.
This is it for now, as we will implement the attacking animations in the next few parts. In Part 11, we will add our Enemy Spawner scene so that we don’t have to manually instance x amount of enemies in our Scene. 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 $3 on Ko-fi!😊