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:

  • Patrol

    The “Patrol” state moves along a pre-defined path (or stands still if there’s no path assigned).

  • Chase

    The “Chase” state moves the enemy towards the player. See 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.

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 CharacterBody2D

@export var patrol_path : Path2D

var run_speed = 25.0
var attacks = ["attack1", "attack2"]

enum states {PATROL, CHASE, ATTACK, DEAD}
var state = states.PATROL
var target = null
var player = null
var current_patrol_point = 0
var patrol_points = []
func _ready():
    if patrol_path:
        patrol_points = patrol_path.curve.get_baked_points()

func _physics_process(delta):
    $Label.text = str(states.keys()[state])
    velocity = Vector2.ZERO
    choose_action()
    if target:
        if target.x > position.x:
            $Sprite2D.scale.x = 1
        elif target.x < position.x:
            $Sprite2D.scale.x = -1
        if state != states.ATTACK:
            velocity = position.direction_to(target) * run_speed

    if velocity.length() > 0:
        anim_state.travel("run")
    move_and_slide()

func choose_action():
    var current_anim = anim_state.get_current_node()
    if current_anim in attacks:
        return
    match state:
        states.DEAD:
            set_physics_process(false)
        states.PATROL:
            if !patrol_path:
                anim_state.travel("idle")
                target = null
                return
            target = patrol_points[current_patrol_point]
            if position.distance_to(target) < 5:
                current_patrol_point = wrapi(current_patrol_point + 1, 0, patrol_points.size())
        states.CHASE:
            target = player.position
        states.ATTACK:
            target = player.position
            anim_state.travel(attacks.pick_random())

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

func _on_attack_radius_body_entered(body):
    state = states.ATTACK

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

func _on_attack_radius_body_exited(body):
    state = states.CHASE