Skip to main content
Coding Quests
The Scroll Library
Tutorials

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

June 16, 2026Updated July 19, 20267 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.

The short version: a 2D enemy in Godot 4 is a CharacterBody2D that runs a small state machine (idle/patrol, chase, attack). Detection is usually a plain distance check to the player, chasing is either straight-line velocity or a NavigationAgent2D when there are walls, and move_and_slide() does the moving. That is the whole loop.

How this differs from the 3D guide (read this if you know the 3D one): the pattern is identical but the moving parts are not. Here you use CharacterBody2D not CharacterBody3D, a NavigationAgent2D baked from a tilemap-based navigation region instead of a NavigationRegion3D navmesh, flip_h on a sprite instead of look_at rotation, and 2D distance with no Y-axis gotcha. This page also leans on a lightweight enum + match state machine rather than the node-per-state setup, because a top-down enemy rarely needs the extra structure.

Before you start: you need GDScript basics and a player node. Everything here assumes you understand the state-machine idea itself; if not, read the state machine tutorial first. Written for Godot 4.x, where NavigationAgent2D and CharacterBody2D are stable across the 4.x line.

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 It Yourself

Type it from scratch and the gaps in your understanding show up fast. In a small level with a player in the player group:

  1. Get the match-based idle/chase/attack enemy working with pure distance detection, no navigation yet. Prove it chases when you approach and gives up when you leave detect_range.
  2. Add a wall between you and the enemy. Watch it grind straight into the wall. That failure is the exact reason NavigationAgent2D exists.
  3. Swap in the agent. Bake a navigation region over your tilemap, point the agent at the player, and confirm it paths around the wall. If it does not move, the region is not baked or the agent has no target_position.
  4. Telegraph the attack. Add a quarter-second flash or windup before the swing lands, then playtest it against yourself. Fairness is the difference between a fun enemy and a cheap one.

If you can add a patrol/roam state (pick a random nearby point, path to it, repeat) without breaking chase and attack, you own the pattern.

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. The level side of this, laying out walls and baking a navigation region, is its own skill; the Godot 4 TileMapLayer tutorial covers building the world your enemies path around.

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

Stop reading. Start building.

Beat the demo boss by writing real Godot code, then build this for real in the 2D Enemy AI Quest.

Free, and no card needed. Built by a real person, with new quests every month.

Get the next Godot build in your inbox

New quests, project breakdowns, and game-dev tips. Free, no spam, unsubscribe anytime.

Written by Omar

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