Making AI Characters for 2D Platformer

godot platformer character AI

You have already created a simple platformer level & player character movement. You want to add AI characters similar to Mario-like games.

This Godot 4 recipe is the solution to problems such as:

  1. Avoid falling off edges and return back; aka patrolling.
  2. Kill the enemy when we jump on it (detecting if we jumped on it or we hit it from front).
  3. Following a fixed path (for enemies who fly in air and have to move between 2 points; birds, bats).
  4. Random movers, who move randomly within a given area (useful for flies, bees and so on).

Avoid falling off edge

How do I make enemies not fall from a platform and make them patrol an area and change direction when they hit a wall?

We use a raycast in front of the character to detect if there still is land in front of them. If there is no land, then reverse direction. In the case of a cliff, we check if player is on the wall (has touched a cliff) & reverse direction if it did.

This is the scene setup with a raycast node.

platformer character AI in godot

And the code for moving the snail and changing direction when on edges is following (Enemy.gd in this case):

extends CharacterBody2D


@export var floor_raycast: RayCast2D
@export var gravity:float = 100

var vel: Vector2
var speed = 40

func _ready():
	vel.x = -speed

func _physics_process(delta:float) -> void:
	if not is_on_floor():
	  vel.y += gravity * delta
	else:
	  vel.y = 0
	if is_on_wall() or (is_on_floor() and not floor_raycast.is_colliding()):
		vel.x = vel.x * -1 # Reverse velocity
		floor_raycast.position.x *= -1.0 # Reverse raycast position
		$AnimatedSprite2D.flip_h = not $AnimatedSprite2D.flip_h # Reverse sprite direction
	
	velocity = vel # Assign velocity
	move_and_slide()
	
	$AnimatedSprite2D.play("walk")
godot platformer tutorial

Detect if player has jumped on enemy

so we can reduce its health or kill it.

We add Area2D nodes to detect if player has touched the enemy horizontally or vertically. If if did vertically (by jumping), then kill the enemy. But if we hit enemy from front or back, then reduce our own health.

Add these areas to enemy:

godot platformer AI tutorial

Connect the body_entered signals for them and write this code:

func _on_verticle_sensors_body_entered(body):
	if body in get_tree().get_nodes_in_group("players"):
		self.queue_free()
		# Give player a jump boost (optional)
		body.velocity.y = body.jump_speed # Replace with any value


func _on_horizontal_sensors_body_entered(body):
	if body in get_tree().get_nodes_in_group("players"):
		if "health" in body:
			body.health -= 1

Add player to players group to access it from anywhere in code. Alternatively, you can pass the player reference via @export variable or any other approach. The above code reduces health on player if it touched the enemy horizontally. But if it jumped on the enemy, enemy gets killed (queue_freed).

killing enemy godot platformer tutorial

To-and-fro motion of bees or birds

The patrolling movement of some NPCs between two points (as in the 1st image) can be achieved by moving towards one direction for some time & then reversing. It can be done using a timer.

Following is code for this kind of movement:

extends CharacterBody2D

# Variables to control movement
var base_speed = 64
var patrol_distance = 200
var direction = 1
var start_position
@export var horizontal_motion: bool = true

# Timer to control direction change
var time_to_change = 1.0 # Time in seconds to change direction
var timer = 0.0

func _ready():
	# Store the starting position of the bird
	start_position = global_position

func _process(delta):
	# Move the bird left and right
	patrol(delta)

func patrol(delta):
	# Increment the timer
	timer += delta
	
	# Change direction if timer exceeds the time to change direction
	if timer >= time_to_change:
		direction *= -1 # Reverse direction
		timer = 0.0 # Reset timer

	# Calculate movement direction and move the bird
	if horizontal_motion:
		velocity = Vector2(direction * base_speed, 0)
	else:
		velocity = Vector2(0, direction * base_speed)
	move_and_slide()

	# Limit the movement range based on patrol_distance
	if abs(global_position.x - start_position.x) > patrol_distance:
		# Snap back to the patrol distance limit and reverse direction
		global_position.x = start_position.x + sign(direction) * patrol_distance
		direction *= -1  # Reverse direction immediately when hitting the limit
		timer = 0.0      # Reset timer

Other NPCs

Most of the platformer NPCs are developed by above methods. If you don’t want an enemy to patrol, you can just remove the patrolling part in above code.

If you want to make enemy not killed by jumping on it, remove the part dealing with Area2D sensors.

Killing enemies by throwing a dagger

In previous post, I talked about throwing daggers or stones or any other throwable thing. For this, we can just add a bit more code to the dagger (or stone) script that should look a bit like this:

func _on_dagger_body_entered(body):
	if body in get_tree().get_nodes_in_group("enemies"):
		body.health -= 1

Assuming that all enemies are added to group enemies.

Moving enemies randomly

We can assign a random direction after every 0.2 or 0.3 seconds to make it move randomly.

For more controlled cases, we can use Path2D nodes to follow a specific path by the AI characters. But depending on the use-case, it can get more and more complex.

Thank you for reading; you can ask anything in the comments. <3

Leave a Reply

Your email address will not be published. Required fields are marked *