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:
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 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.