Changing behaviors

Problem

You want your AI-controlled entity to switch between different behaviors.

Solution

For this example, we’ll assume an enemy with the following behaviors. See the individual recipes for how to make each behavior work.

  • Patrol

    The “Patrol” state moves along a pre-defined path (or stands still if there’s no path assigned). See Recipe: Path following for details.

  • Chase

    The “Chase” state moves the enemy towards the player. See Recipe: Chasing the player for how to make this behavior.

  • Attack

    In this state, the player is in range of a melee attack, so the enemy stops moving and executes its attack. See Recipe: Melee attacks for how to make melee attacks.

These behaviors are states - the enemy can only be in one of these states at a time, and certain events, such as the player getting near, will cause a transition to another state.

To determine the state transitions, we have two Area2D nodes on the enemy: an outer one called “DetectRadius” and an inner called “AttackRadius”. The player entering or exiting these areas will trigger the related behavior.

alt alt

We’ve chosen a rectangular shape for AttackRadius in this example due to the shape of the enemy’s attack. Any shape is fine as long as it’s smaller than the DetectRadius.

Connect the body_entered and body_exited signals of both these areas. If you’re using collision layers (and you should be), set them so that they can only detect the player (or any other body you want to be chased/attacked).

Now let’s examine the enemy’s script:

extends KinematicBody2D

enum states {PATROL, CHASE, ATTACK, DEAD}
var state = states.PATROL

We start with an enum to give us a way to reference our states by name, and a variable to hold the current state.

# For setting animations.
var anim_state
var run_speed = 25
var attacks = ["attack1", "attack2"]

# For path following.
export (NodePath) var patrol_path
var patrol_points
var patrol_index = 0

# Target for chase mode.
var player = null

var velocity = Vector2(run_speed, 0)

The other variables needed for the individual behaviors and animations. See the referenced behavior links above for details.

func _physics_process(delta):
    choose_action()
    # Changing the x scale flips the sprite and its attack area.
    if velocity.x > 0:
        $Sprite.scale.x = 1
    elif velocity.x < 0:
        $Sprite.scale.x = -1

    # If we're moving, show the run animation.
    if velocity.length() > 0:
        anim_state.travel("run"
        )
    # Show the idle animation when coming to a stop (but not attacking).
    if anim_state.get_current_node() == "run" and velocity.length() == 0:
        anim_state.travel("idle")

    velocity = move_and_slide(velocity)

We’ll handle movement as normal in _physics_process(), calling choose_action() (see below) to decide what the resulting movement will be.

func choose_action():
    velocity = Vector2.ZERO
    var current = anim_state.get_current_node()
    # If we're currently attacking, don't move or change state.
    if current in attacks:
        return

    # Depending on the current state, choose a movement target.
    var target
    match state:
        states.DEAD:
            set_physics_process(false)

        # Move along assigned path.
        states.PATROL:
            if !patrol_path:
                return
            target = patrol_points[patrol_index]
            if position.distance_to(target) < 1:
                patrol_index = wrapi(patrol_index + 1, 0, patrol_points.size())
                target = patrol_points[patrol_index]
            velocity = (target - position).normalized() * run_speed

        # Move towards player.
        states.CHASE:
            target = player.position
            velocity = (target - position).normalized() * run_speed

        # Make an attack.
        states.ATTACK:
            target = player.position
            if target.x > position.x:
                $Sprite.scale.x = 1
            elif target.x < position.x:
                $Sprite.scale.x = -1
            anim_state.travel("attack")

In choose_action() we determine the target and move toward it.

func _on_DetectRadius_body_entered(body):
    state = states.CHASE
    player = body

func _on_DetectRadius_body_exited(body):
    state = states.PATROL
    player = null

func _on_AttackRadius_body_entered(body):
    state = states.ATTACK

func _on_AttackRadius_body_exited(body):
    state = states.CHASE

Finally, the functions connected to the area signals change the state accordingly.

Expanding

This example is intentionally kept as simplified as possible, while still demonstrating complete behaviors. In a larger project, there would likely be a greater number of behaviors, as well as more complex conditions for deciding which one to apply.