Simple Enemy AI in Godot

Simple Enemy AI in Godot

Creating enemy AI in Godot 4 using the built-in Navigation System is incredibly efficient once you get the node structure down. Godot 4 uses a refined navigation server that handles pathfinding smoothly.

Here is a step-by-step Guide to creating an enemy that patrols or stands still, chases the player when they enter an Area3D, and gives up exactly 3 seconds after the player leaves.

1. Scene Setup

The Player Scene

For this Guide, assume your player is a CharacterBody3D assigned to a script.

  • Crucial Step: Select your Player root node, go to the Node tab (next to Inspector), click Groups, and add the player to a group named "player". This makes it easy for the enemy to Identify them.

The Enemy Scene

Create a new 3D scene for your enemy with the following node structure:

  • CharacterBody3D (Name: Enemy)
    • MeshInstance3D (A capsule or cylinder so you can see it)
    • CollisionShape3D (Matches the mesh)
    • NavigationAgent3D (Handles the pathfinding calculations)
    • Area3D (Name: DetectionArea – The trigger zone)
      • CollisionShape3D (A large sphere or box representing the detection Range)
    • Timer (Name: LoseTargetTimer – Set One Shot to On in the Inspector)

2. Setting Up the Environment (NavMesh)

For the navigation agent to move, it needs a walkability mesh.

  1. In your main world scene, add a NavigationRegion3D node.
  2. In the Inspector for the NavigationRegion3D, look for the Navigation Mesh property, click <empty>, and select New NavigationMesh.
  3. Make your floor/Environment meshes children of the NavigationRegion3D.
  4. Select the NavigationRegion3D node and click the Bake Navigation Mesh button at the top of the 3D viewport. You should see a transparent blue overlay appearing over your walkable surfaces.

3. The Enemy AI Script

Attach a script to your Enemy (CharacterBody3D) node. Paste the following code:

extends CharacterBody3D

@export var speed: float = 4.0
@export var accel: float = 10.0

@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D
@onready var lose_timer: Timer = $LoseTargetTimer

var target_player: CharacterBody3D = null
var is_chasing: bool = false

func _physics_process(delta: float) -> void:
	# If we don't have a target and aren't actively chasing, do nothing
	if not is_chasing or not target_player:
		return
		
	# 1. Tell the navigation agent where the player is
	nav_agent.target_position = target_player.global_position
	
	# 2. Check if path is finished (optional, but prevents jittering)
	if nav_agent.is_navigation_finished():
		return
		
	# 3. Calculate movement direction towards the next path point
	var current_pos = global_position
	var next_path_pos = nav_agent.get_next_path_position()
	
	# Calculate direction vector (horizontal plane only)
	var new_velocity = (next_path_pos - current_pos).normalized() * speed
	
	# 4. Smoothly interpolate current velocity to new velocity (Handling acceleration)
	velocity.x = move_toward(velocity.x, new_velocity.x, accel * delta)
	velocity.z = move_toward(velocity.z, new_velocity.z, accel * delta)
	
	# Simple gravity handling so the enemy doesn't Float if there are slopes
	if not is_on_floor():
		velocity.y -= 9.8 * delta
	else:
		velocity.y = 0

	# 5. Move the Character
	move_and_slide()

4. Connecting the Signals

To make the detection area and the timer work together, we Need to wire up Godot’s signals.

Step A: Player Enters Detection Area

  1. Select the DetectionArea node.
  2. Go to the Node tab (next to Inspector) -> Signals.
  3. Double-click body_entered(body: Node3D) and connect it to your Enemy script.
  4. Add this code inside the generated Function:
func _on_detection_area_body_entered(body: Node3D) -> void:
	if body.is_in_group("player"):
		target_player = body
		is_chasing = true
		lose_timer.stop() # Stop the countdown if the player re-enters before 3 seconds

Step B: Player Leaves Detection Area

  1. With DetectionArea selected, double-click body_exited(body: Node3D) in the Signals tab and connect it to your Enemy script.
  2. Add this code inside the generated Function:
func _on_detection_area_body_exited(body: Node3D) -> void:
	if body == target_player:
		# Start the 3-second countdown
		lose_timer.start(3.0)

Step C: Timer Runs Out

  1. Select your LoseTargetTimer node.
  2. Go to the Signals tab, double-click timeout() and connect it to your Enemy script.
  3. Add this code inside the generated Function:
func _on_lose_target_timer_timeout() -> void:
	# 3 seconds have passed without the player returning
	is_chasing = false
	target_player = null

Pro-Tips for Fine-Tuning

  • Fixing Floating/Sinking: If your enemy clips into the ground or floats slightly above it while moving, select the NavigationAgent3D and tweak the Path Post Processing or adjust its Target Desired Distance.
  • Avoid Walls: Under the NavigationAgent3D properties, look at the Pathfinding section. You can adjust the agent radius to Ensure the enemy doesn’t hug walls too tightly and get stuck on corners.
  • Y-Axis Locking: The pathfinding returns a 3D coordinate vector. If your map has hills, move_and_slide() handles the slopes smoothly using the gravity snippet provided above.