Building 2D Enemy AI in Godot

Updated June 2026
Building 2D enemy AI in Godot combines CharacterBody2D physics, Area2D detection zones, RayCast2D line-of-sight checks, and NavigationAgent2D pathfinding into a state machine that handles patrol, chase, attack, and retreat behaviors. This guide walks through each component and shows how they connect to produce enemies that feel responsive and intelligent in a 2D game.

Enemy AI is one of the systems where Godot's node composition model really shines. Instead of writing monolithic AI classes, you build an enemy from specialized child nodes that each handle one concern: physics, visuals, detection, navigation, and animation. The AI script on the root node reads data from these children and makes decisions, keeping the logic clean and the components reusable across different enemy types.

Set Up the Enemy Scene

Create a new scene with CharacterBody2D as the root node. This node type provides physics-based movement with built-in collision handling through the move_and_slide() method. Add a Sprite2D child for the enemy's visual appearance and a CollisionShape2D with a shape that matches the sprite's bounds, typically a RectangleShape2D or CapsuleShape2D.

Add a NavigationAgent2D as a child node. This agent communicates with the NavigationServer to request and follow paths through the level's navigation mesh. Configure its path_desired_distance (how close the agent needs to get to a path point before moving to the next one) and target_desired_distance (how close is "arrived" at the final destination). For most 2D enemies, values between 4.0 and 16.0 pixels work well for both settings.

Add an AnimationPlayer or AnimationTree for managing visual states. If your enemy has separate animations for idle, walk, chase, and attack, an AnimationTree with a state machine gives you smooth blending between states. Connect the animation states to your AI states so the enemy always looks like it is doing what its logic says it is doing.

Build the Detection System

Detection in 2D Godot games relies on Area2D nodes with collision shapes that define sensory ranges. Create a child Area2D named "DetectionZone" with a CircleShape2D collision child. Set the radius to the distance at which you want the enemy to notice the player, typically 150 to 300 pixels depending on your game's scale. Create a second Area2D named "AttackZone" with a smaller radius representing melee or close attack range.

Connect the body_entered and body_exited signals of DetectionZone to your enemy script. When the player's CharacterBody2D enters the detection zone, the enemy knows a potential target is nearby. When the player exits, the enemy can decide whether to give up pursuit or continue investigating the last known position. Use collision layers and masks to ensure these areas only detect the player and not other enemies, projectiles, or world geometry.

For more realistic detection, separate the detection zone from the reaction. When a body enters the detection area, do not immediately switch to chase. Instead, set a flag that a potential target exists, then verify with a raycast (covered in a later step) that the enemy has line of sight. This prevents enemies from "seeing through walls" just because the player is within the circular detection radius.

Implement the State Machine

Define an enum at the top of your script for all possible enemy states: IDLE, PATROL, CHASE, ATTACK, and HURT or RETREAT if needed. Add a var current_state: int = State.IDLE variable to track which state is active. In _physics_process(delta), use a match statement on current_state to dispatch to the appropriate behavior function for each state.

Each state function handles its own logic and transition conditions. The patrol function moves the enemy along waypoints and checks if the player has been detected. The chase function updates the NavigationAgent target to the player's position and moves toward it, checking if the player is within attack range or has escaped. The attack function plays the attack animation and applies damage, then transitions back to chase or idle when the attack completes.

State transitions should go through a dedicated function rather than being scattered throughout the code. A change_state(new_state) function can handle exit logic for the current state (stopping movement, canceling timers) and entry logic for the new state (starting animations, setting navigation targets). This prevents bugs where the enemy is in a chase state but still playing the idle animation because the transition code only updated the state variable.

Keep a reference to the player node when it enters the detection zone, and clear it when the player leaves or the enemy gives up. Check is_instance_valid(target) before accessing the player reference, because the player might be freed (respawning, changing scenes) while the enemy is in chase or attack state. This defensive check prevents null reference crashes that are common in AI code.

Create Patrol Behavior

Patrol behavior moves the enemy between a set of predefined positions. The simplest approach is an array of Vector2 waypoints defined as an exported variable so you can set them in the editor. Each waypoint represents a position the enemy walks to, pauses briefly, then continues to the next point in the array, looping back to the start when it reaches the end.

Use the NavigationAgent2D to path to each waypoint. Set navigation_agent.target_position = waypoints[current_waypoint_index], then in each physics frame call navigation_agent.get_next_path_position() to get the next point along the path. Calculate the direction from the enemy's current position to that point, multiply by the patrol speed, and assign to the velocity property before calling move_and_slide().

Check navigation_agent.is_navigation_finished() each frame. When it returns true, the enemy has arrived at the current waypoint. Start a timer for the pause duration, and when the timer fires, increment the waypoint index (wrapping with modulo) and set the new target position. This produces natural-looking patrol behavior where the enemy walks, pauses to "look around," then continues its route.

For variety, you can randomize the pause duration, choose a random next waypoint instead of sequential order, or add slight positional offsets to each waypoint so the patrol does not follow an identical path every loop. These small variations make the AI feel less mechanical without adding significant complexity to the code.

Add Chase and Attack Logic

Chase behavior reuses the NavigationAgent2D but sets the player's position as the target instead of a waypoint. Update the target position every few physics frames rather than every single frame, which reduces NavigationServer load and produces smoother paths. A common pattern is updating the target every 0.25 seconds using a timer, which is frequent enough for responsive chasing without unnecessary computation.

Move toward the next path position at a chase speed that is typically faster than patrol speed. The speed difference makes the transition from patrol to chase visually obvious to the player and communicates danger. Flip the sprite horizontally based on movement direction so the enemy faces the player during the chase. Access the velocity's x component after move_and_slide() and set sprite.flip_h accordingly.

Attack behavior activates when the player is within the attack zone. Transition to the attack state, play the attack animation, and use a timer or animation callback to apply damage at the right moment in the animation. After the attack completes, check if the player is still in range. If so, attack again after a cooldown. If the player has moved out of range, transition back to chase. If the player has left the detection zone entirely, transition to a "return to patrol" state where the enemy walks back to its nearest waypoint.

For ranged enemies, the attack state spawns a projectile scene instead of applying direct damage. Instantiate the projectile, set its direction toward the player's position, and add it to the scene tree. The projectile handles its own movement, collision detection, and damage application. This separation keeps the enemy's AI script focused on decision-making rather than projectile physics.

Add Line of Sight with Raycasting

RayCast2D nodes project an invisible line from the enemy toward a target and report whether anything blocks the path. Add a RayCast2D child to the enemy and configure its collision mask to detect world geometry (walls, platforms) but not other enemies or the player's hitbox. Each physics frame when a potential target exists in the detection zone, point the raycast at the player's position and check its results.

Set the raycast's target_position to the local-space offset between the enemy and the player: raycast.target_position = to_local(player.global_position). If raycast.is_colliding() returns true, something is between the enemy and the player, meaning no line of sight. If it returns false, the path is clear and the enemy should react.

Combine the Area2D detection with the raycast verification for a two-stage detection system. The area tells you the player is nearby, and the raycast confirms the enemy can actually see them. This prevents frustrating situations where enemies chase the player through walls or detect them from behind solid obstacles. For stealth games, you can narrow the raycast check to a cone by testing the angle between the enemy's facing direction and the direction to the player, only checking the raycast if the player is within the enemy's field of view.

Polish with Animations and Feedback

An AnimationTree with a state machine node type maps directly to your AI states. Create animation states for idle, patrol_walk, chase_run, attack, hurt, and death. In your change_state() function, call animation_tree.get("parameters/playback").travel("chase_run") to transition the animation whenever the AI state changes. The AnimationTree handles blending between states smoothly.

Add visual feedback beyond animations. When the enemy detects the player, briefly flash a detection indicator, change the enemy's color tint, or display an exclamation mark sprite. When the enemy takes damage, flash white for one or two frames using a shader or modulate property. These visual cues help the player understand what the AI is doing and make combat feel more responsive and fair.

Sound effects tied to AI states improve the game feel significantly. Footstep sounds during patrol, a growl or alert sound when detection triggers, weapon swing sounds during attack, and hit reaction sounds during damage all communicate the AI's state through audio. Use AudioStreamPlayer2D nodes so the sounds have spatial positioning, getting louder as the player approaches an enemy.

Key Takeaway

Effective 2D enemy AI in Godot comes from composing specialized nodes, detection areas for awareness, raycasts for line of sight, navigation agents for pathfinding, and animation trees for visual feedback, into a clean state machine that handles each behavior as a separate, testable function.