DEV Community

Cover image for Let’s Learn Godot 4 by Making an RPG — Part 10: Animating AI Movement🤠
christine
christine

Posted on • Edited on

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().

Godot RPG



    ### 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)


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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)


Enter fullscreen mode Exit fullscreen mode

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()


Enter fullscreen mode Exit fullscreen mode

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()


Enter fullscreen mode Exit fullscreen mode

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.

Godot RPG

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


Enter fullscreen mode Exit fullscreen mode

Now our enemy will play their side animations.

Godot RPG

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.

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 (3)

Collapse
 
moopet profile image
Ben Sinclair

This looks like a good series to follow, but how come the first part is part 10 and the second is part 1? Was that something you did or is there a bug in how DEV sorts numbers I wonder?

Collapse
 
christinec_dev profile image
christine

It's a bug with the Dev sorts! Start from part 1 and make your way up😊

Collapse
 
alex2025 profile image
Alex

This paragraph is duplicated: If you were to run your enemy scene from here,