Simple Enemy AI with Finite State Machines in Godot
The blank stare of a lifeless enemy, pacing back and forth without purpose, is a game design sin. Let’s banish that forever! We’re diving headfirst into crafting compelling enemy AI using Finite State Machines (FSMs) in Godot. Forget complex behavior trees for now; FSMs are your gateway to responsive, engaging, and surprisingly simple enemy logic. My strong belief is that mastering this foundational concept will allow game developers to create more immersive and enjoyable gaming experiences. This guide takes you from zero to a basic patrol-chase-attack loop.
Setting Up the Enemy Scene
The most-downloaded assets on Wayline this month — sorted by what real developers actually use.
First, create a new scene in Godot. This will be our enemy. I recommend starting with a KinematicBody2D node as the root. Add a Sprite for the visual representation and a CollisionShape2D for collision detection. Rename these nodes for clarity, such as Enemy, EnemySprite, and EnemyCollision.
Here’s a crucial pitfall to avoid: Don’t use RigidBody2D unless you need realistic physics simulations. KinematicBody2D gives you direct control over movement, essential for precise AI behavior.
Implementing the Finite State Machine
The core of our AI is the FSM. Create a new GDScript file (e.g., enemy_ai.gd) and attach it to the Enemy node. This script will manage the enemy’s states and transitions.
We’ll define three states: Patrol, Chase, and Attack. Each state represents a distinct behavior.
extends KinematicBody2D
enum {
PATROL,
CHASE,
ATTACK
}
var state = PATROL
var speed = 50
var chase_speed = 100
var attack_range = 50
var patrol_points = [Vector2(-50,0), Vector2(50,0)] #Example patrol points, relative to the starting position
var current_patrol_point = 0
var player = null #Reference to the player node
func _ready():
player = get_tree().get_nodes_in_group("Player")[0] #Assuming you have a "Player" group
patrol_points = patrol_points.map(func(point): return point + position) #Absolute positions
func _physics_process(delta):
match state:
PATROL:
patrol(delta)
CHASE:
chase(delta)
ATTACK:
attack(delta)
#State Transition Logic
if player_in_attack_range():
state = ATTACK
elif player_in_chase_range() and can_see_player():
state = CHASE
else:
state = PATROL
func patrol(delta):
var direction = (patrol_points[current_patrol_point] - position).normalized()
var collision = move_and_collide(direction * speed * delta)
if position.distance_to(patrol_points[current_patrol_point]) < 5: #Reached patrol point
current_patrol_point = (current_patrol_point + 1) % patrol_points.size()
func chase(delta):
var direction = (player.position - position).normalized()
move_and_collide(direction * chase_speed * delta)
func attack(delta):
#Replace with your attack logic
print("Attacking!")
func player_in_attack_range():
return position.distance_to(player.position) <= attack_range
func player_in_chase_range():
return position.distance_to(player.position) <= 200 #Example chase range
func can_see_player():
var space_state = get_world_2d().direct_space_state
var result = space_state.intersect_ray(position, player.position, [self]) #Raycast
return result == null #If null, no collision occurred, meaning the enemy can "see" the player
This code defines the states and the transition logic. The _physics_process function is the heart of the FSM, handling state execution and transitions.
Common Mistake: Forgetting to normalize the direction vector in patrol and chase can lead to erratic movement!
Implementing State Behaviors: Patrol, Chase, and Attack
The patrol, chase, and attack functions define the enemy’s actions in each state.
- Patrol: The enemy moves between predefined points. This uses the
move_and_collidefunction for collision avoidance. - Chase: The enemy moves towards the player. Here, we use a slightly higher
chase_speedto increase the urgency. - Attack: This is a placeholder for your actual attack logic. Consider implementing different attack types or animations.
For the patrolling behavior, make sure to define patrol points. It’s best to have these patrol points be relative to the enemy’s original position for easier re-positioning in the editor.
Transitioning Between States
The transition logic is crucial for a reactive AI. The code checks for two conditions:
- Proximity: Is the player within
attack_rangeorchase_range? - Line of Sight: Can the enemy “see” the player? This is implemented using a raycast. If the raycast doesn’t detect any collisions, the enemy has a clear line of sight.
Pitfall Alert: Using a simple distance check without line of sight leads to enemies “seeing” through walls! Raycasts are essential for realistic behavior.
Debugging the FSM
Debugging FSMs can be tricky. Godot’s debugger is your friend. Here’s a structured approach:
Print Statements: Add
print("State:", state)in the_physics_processfunction to track state changes. I know print statements seem archaic, but their simplicity is invaluable.Visual Debugging: Use Godot’s drawing functions (e.g.,
draw_line,draw_circle) in the_drawfunction to visualize the enemy’s state. For instance, draw a green circle when patrolling, yellow when chasing, and red when attacking.
func _draw():
match state:
PATROL:
draw_circle(Vector2(0,0), 10, Color(0,1,0,1)) #Green
CHASE:
draw_circle(Vector2(0,0), 10, Color(1,1,0,1)) #Yellow
ATTACK:
draw_circle(Vector2(0,0), 10, Color(1,0,0,1)) #Red
- Remote Debugger: Use Godot’s remote debugger to step through the code and inspect variables in real-time. This is crucial for understanding complex interactions.
Crucial Tip: Ensure the “Player” group exists and contains the player node. This is a common source of errors that can be easily overlooked.
Real-World Applications and Enhancements
This patrol-chase-attack loop is a foundation. Expand on it:
- Add Animation: Integrate animations for each state to enhance visual feedback.
- Implement Different Attack Types: Ranged attacks, melee attacks, special abilities.
- Improve Patrol Behavior: Implement more sophisticated patrol routes or random wandering.
- Introduce a “Dead” State: When the enemy’s health reaches zero, transition to a “Dead” state with appropriate animations and cleanup.
- Group AI: Implement ways for your enemies to interact with each other.
Finite State Machines are powerful tools, and mastering them is a key step towards creating engaging and believable game worlds. Don’t be afraid to experiment and iterate. The most compelling AI behaviors often emerge from unexpected interactions and careful refinement.