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:
- Avoid falling off edges and return back; aka patrolling.
- Kill the enemy when we jump on it (detecting if we jumped on it or we hit it from front).
- Following a fixed path (for enemies who fly in air and have to move between 2 points; birds, bats).
- 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.
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")
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:
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
).
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