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:
extends CharacterBody2Denum State { IDLE, CHASE, ATTACK }@export var speed: float = 120.0@export var detect_range: float = 200.0@export var attack_range: float = 32.0var 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:
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:
@onready var agent: NavigationAgent2D = $NavigationAgent2Dfunc chase() -> void: agent.target_position = player.global_position var next := agent.get_next_path_position() var dir := (next - global_position).normalized() velocity = dir * speedFor 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:
- 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 leavedetect_range. - Add a wall between you and the enemy. Watch it grind straight into the wall. That failure is the exact reason
NavigationAgent2Dexists. - 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. - 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.


