Coding Quests
The Scroll Library
Tutorials

Godot 4 2D Enemy AI Tutorial (Idle, Chase, Attack)

June 16, 20266 min read

An enemy that stands still is a prop. An enemy that notices you, comes after you, and swings when it gets close is a game. The gap between those two is a small state machine, and in 2D it's friendlier to build than you'd think.

This is the 2D companion to my 3D enemy AI tutorial. Same brain, simpler setup.

Think in states

Don't try to write enemy behavior as one big tangle of if statements. Break it into states the enemy can be in, with clear rules for switching between them:

  • Idle: standing around, watching for the player.
  • Chase: the player is close, move toward them.
  • Attack: close enough to hit, swing.

That's a finite state machine, and it's the single most useful pattern in gameplay code. If the idea is new to you, read the state machine tutorial first, because everything below assumes it.

The enemy setup

Use a CharacterBody2D with a Sprite2D (or AnimatedSprite2D) and a CollisionShape2D. Give it a reference to the player and a couple of tuning values:

GDScript
extends CharacterBody2D
enum State { IDLE, CHASE, ATTACK }
@export var speed: float = 120.0
@export var detect_range: float = 200.0
@export var attack_range: float = 32.0
var state: State = State.IDLE
@onready var player: Node2D = get_tree().get_first_node_in_group("player")

Tag your player node with the group player (Node panel, Groups tab) so the enemy can find it without a hardcoded path.

Detection and the state switch

The simplest detection is distance. Each physics frame, check how far the player is and pick a state:

GDScript
func _physics_process(_delta: float) -> void:
if player == null:
return
var dist := global_position.distance_to(player.global_position)
match state:
State.IDLE:
velocity = Vector2.ZERO
if dist < detect_range:
state = State.CHASE
State.CHASE:
var dir := (player.global_position - global_position).normalized()
velocity = dir * speed
if dist <= attack_range:
state = State.ATTACK
elif dist > detect_range:
state = State.IDLE
State.ATTACK:
velocity = Vector2.ZERO
attack()
if dist > attack_range:
state = State.CHASE
move_and_slide()

That's a working enemy. It waits, it chases when you get close, it attacks in range, and it gives up if you escape. The match statement keeps each state's logic in its own clean block instead of a pile of nested conditions.

Chasing around walls

Distance-based chasing walks straight at the player, which means it gets stuck on walls. For anything with obstacles, swap the direct chase for a NavigationAgent2D. You bake a navigation region for your level, set the agent's target, and let it path around walls:

GDScript
@onready var agent: NavigationAgent2D = $NavigationAgent2D
func chase() -> void:
agent.target_position = player.global_position
var next := agent.get_next_path_position()
var dir := (next - global_position).normalized()
velocity = dir * speed

For a wide-open arena, distance chasing is fine. The moment you have a real level with walls, reach for the agent.

Telegraph the attack

The difference between a cheap enemy and a fair one is the windup. Before the attack lands, play a brief animation or flash so the player can react. A telegraphed leap or a quarter-second wind-up turns a frustrating enemy into a fun one. That fairness is most of what makes combat feel good.

Build the full enemy

This skeleton gets you idle, chase, and attack. A complete enemy wants roaming when idle, leashing so it returns home, hitstun when struck, and a death state with loot. Our 2D Enemy AI quest builds exactly that: a reusable state machine with NavigationAgent2D pathfinding, a telegraphed leap attack, combat spacing, and death drops, across ten lessons.

FAQ

How do I make an enemy chase the player in Godot 4?

Find the player (a group lookup is cleanest), get the direction to them with (player.global_position - global_position).normalized(), set velocity to that direction times speed, and call move_and_slide(). Wrap it in a chase state so the enemy only does it when the player is in range.

Should I use a state machine for enemy AI?

Yes, even for simple enemies. Splitting behavior into idle, chase, and attack states with a match statement keeps the logic readable and easy to extend. Trying to express the same behavior as nested if statements gets tangled fast.

When do I need NavigationAgent2D instead of just moving toward the player?

Use NavigationAgent2D when your level has walls or obstacles. Moving straight toward the player gets stuck on geometry, while a navigation agent paths around it. For an open arena with no obstacles, simple distance-based movement is enough.

godottutorialenemy-ai2d

Reading is the map. The quest is the territory.

Build this for real in the 2D Enemy AI Quest.

Start the Quest
Written by Coding Quests

We teach Godot 4 by making you build complete systems: inventories, save systems, souls-like controllers, enemy AI. The scrolls are free. The quests are where it sticks.