Help with making a simple enemy ai for a 2d platformer

Godot Version

4.6-stable

Question

So, im trying to make a simple enemy ai for my 2d platformer game. How it works in a nutshell: the enemy has 2 states: patrolling, and pursuing. while patrolling, it walks in a direction for a bit and randomly switches directions, while it “scans” an area thanks to an area2d node (“detectionrange”). when the player enters said area, the enemy switches to the pursuing state. My question is: How do I get the enemy to travel to the players position? I already figured out how to get the players position, but I don’t know how to make it go to the player. Any help is appreciated. Thanks in advance!

func _ready() -> void: #randomly change directions during patrolling.
	randomize()
	while patrolling == true and pursuing == false:
		await get_tree().create_timer(randf_range(2.0 , 3.5)).timeout
		direction = -1
		animated_sprite_2d.flip_h = true
		animation_player.play("detectionrangeleft")
		await get_tree().create_timer(randf_range(2.0 , 3.5)).timeout
		direction = 1
		animated_sprite_2d.flip_h = false
		animation_player.play("detectionrangeright")

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
	if not ray_castdown.is_colliding(): # very rudimentary gravity code
		position.y += 150 * delta
	if patrolling == true and pursuing == false: # movement code while patrolling
		if ray_castright.is_colliding(): # wall detection code
			direction = -1
			animated_sprite_2d.flip_h = true
			animation_player.play("detectionrangeleft")
		if ray_castleft.is_colliding():
			direction = 1
			animated_sprite_2d.flip_h = false
			animation_player.play("detectionrangeright")
		if not ray_castdownleft.is_colliding(): #ledge detection code
			direction = 1
			animated_sprite_2d.flip_h = false
			animation_player.play("detectionrangeright")
		if not ray_castdownright.is_colliding():
			direction = -1
			animated_sprite_2d.flip_h = true
			animation_player.play("detectionrangeleft")
		spottedindicator.play("RESET")
		position.x += direction * SPEED * delta
	if patrolling == false and pursuing == true: # movement code while patrolling
		pass #this is the part that I need help for

func _on_detectionrange_body_entered(body: Node2D) -> void: #function for when player enters the detection range
	if (body.name == "player"):
		patrolling = false
		pursuing = true
		spottedindicator.play("spotted")
		targetedposition = body.global_position.x # "targetedposition" is well, the targeted position
		


Apologies for if the code is kinda bloated/ the question isnt well formated, this is my first post on the forum and im still kinda new to game dev and gdscript

1 Like

I think you just need a reference to the player, then the new direction will be the player’s position minus the enemy’s position. Eg: (10, 5) - (5, 2) = (5, 3). You’ll need the proper syntax and all that, i was just showing the math. This is a basic way of doing it because you’re just going in a straight line but it should work. Basically Enemy Position + Direction Of Movement = Player Position eventually so you just take the enemy position to the right hand side and get dir = player position - enemy position.

You might want to normalize the direction vector so that it behaves more predictably. Just get the direction and divide it by its own length. Or you can use the normalized method. Eg.

var direction: Vector2
direction = direction.normalized()

you just call this after doing the earlier subtracting.

I would use two raycasts for either direction, and simply make it move either way while it detects the player, i am not sure if that is the best way by any means, but it seems like a simple method for a Chargin’ Chuck-like enemy (the Football guys that charge at you in Super Mario World).

This is assuming you dont want the enemy to jump or anything.

So, you’re missing part of your script. I’m gonna work with what I’ve got. Also a few notes:

  • I refactored your code, added a bunch of helper functions and simplified everything.
  • Changed your code to use a simple Enum-based state machine.
  • I added an IDLE state in just to show you how you can leverage adding states, to add a little more variety, and to allow the player to get away when it leaves the detection range. (You can remove it if you like.)
  • randomize() is run when the game starts. You don’t need to explicitly call it.
  • Never use a while loop in your code. It can cause your game to hang. Instead, use Timers, the _process() or _physics_process() functions to keep repeating things. (I’ve done examples for both a Timer and _physics_process().)
  • I cleaned up some confusing variable names by adding underscores between words.
  • Take a look at this post I wrote today on how to use masks and layers so you don’t have to check the player’s name. Use the steps under the All Other Games header. Can't load player data to nodes in other scenes - #6 by dragonforge-dev
  • I guessed at your @onready variable names. You need to check these and correct anything I got wrong. In the future, include the entire script.
  • I guessed spottedindicator was an AnimationPlayer and renamed it spotted_indicator so it’s easier to read.
  • I assumed this is for a CharacterBody2D node. If it’s not, make it a CharacterBody2D node.
  • I got rid of your Magic Numbers. (They’re a bad coding habit.)
  • I replaced your gravity code with the current CharacterBody2D default code.
  • Added move_and_slide() back in.
  • Showed you how to do weighted random selection and random selection using Arrays. E.G., direction = [-1, 1].pick_random()
extends CharacterBody2D

enum State {
	PATROL,
	PURSUE,
	IDLE,
}

const SPEED = 300.0
const JUMP_VELOCITY = -400.0

@export var minimum_patrol_time: float = 2.0
@export var maximum_patrol_time: float = 3.5

var direction: float = 0.0
var player: CharacterBody2D
var state: State = State.PATROL
var patrol_timer: Timer

@onready var detection_range: Area2D = $detectionrange
@onready var spotted_indicator: AnimationPlayer = $spottedindicator
@onready var animated_sprite_2d: AnimatedSprite2D = $AnimatedSprite2D
@onready var animation_player: AnimationPlayer = $AnimationPlayer
@onready var ray_castright: RayCast2D = $RayCastright
@onready var ray_castdown: RayCast2D = $RayCastdown
@onready var ray_castleft: RayCast2D = $RayCastleft
@onready var ray_castdownleft: RayCast2D = $RayCastdownleft
@onready var ray_castdownright: RayCast2D = $RayCastdownright


func _ready() -> void: #randomly change directions during patrolling.
	detection_range.body_entered.connect(_on_player_detected)
	detection_range.body_exited.connect(_on_player_lost)
	patrol_timer = Timer.new()
	add_child(patrol_timer)
	patrol_timer.one_shot = true
	patrol_timer.timeout.connect(_on_patrol_timer_timeout)
	patrol_timer.start(randf_range(minimum_patrol_time, maximum_patrol_time))


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
	## Add the gravity.
	if not is_on_floor():
		velocity += get_gravity() * delta	
	# Add movement based on state
	match state:
		State.PATROL:
			if ray_castright.is_colliding(): # wall detection code
				direction = -1
			if ray_castleft.is_colliding():
				direction = 1
			if not ray_castdownleft.is_colliding(): #ledge detection code
				direction = 1
			if not ray_castdownright.is_colliding():
				direction = -1
		State.PURSUE: #Currently the enemy will chase the player right off a cliff.
			face(player)
			direction = (player.global_position - global_position).normalized().x
		State.IDLE:
			direction = 0.0
	
	if direction:
		velocity.x = direction * SPEED * delta
		_flip(direction)
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED)

	move_and_slide()


func face(node: Node2D) -> void:
	_flip(node.position.x - position.x)


func _flip(direction_float: float) -> void:
	animated_sprite_2d.flip_h = direction_float < 0
	if direction_float < 0:
		animation_player.play("detectionrangeleft")
	else:
		animation_player.play("detectionrangeright")


# You don't need to check the body name.
# Instead, put the Player on a separate physics layer,
# and have the Area2D only check that layer
func _on_player_detected(body: Node2D) -> void:
	player = body
	spotted_indicator.play("spotted")
	state = State.PURSUE
	patrol_timer.stop()


func _on_player_lost(_body: Node2D) -> void:
	player = null
	spotted_indicator.play("RESET")
	state = State.IDLE #When we lose the player, just stand still for a moment. This will prevent us from chasing the player forever by accident.
	patrol_timer.start()


func _on_patrol_timer_timeout() -> void:
	state = [State.PATROL, State.PATROL, State.IDLE].pick_random() #Simple weighted choice
	if state == State.PATROL: #If patrolling, pick a random direction -> left (-1) or right (1)
		direction = [-1, 1].pick_random()
	patrol_timer.start(randf_range(minimum_patrol_time, maximum_patrol_time)) #Kick off the timer again

It should work pretty well, once you get the variable names sorted. But I did not test it. If you have any questions, let me know.

Thank you! I’ll try it out later today and see how it goes! Thank you so much!

(Edit) Tested it. The pursue state works well. The patrol, however, doesnt (prob cuz i havent added the necessary timers) but ill figure it out. Thanks for the help!

1 Like

Glad it worked. Feel free to come back and post another question if you get stuck.

yeah about that, ive added a timer in the scenetree, but it still doesnt work

idk what I did wrong here.

You don’t need to add your own timer, the code does it for you. What is happening and what do you expect to happen?

The enemy doesnt move in the patrol state and these errors show up when there is no timer

Oops. My bad. Update the _ready() function:

func _ready() -> void: #randomly change directions during patrolling.
	detection_range.body_entered.connect(_on_player_detected)
	detection_range.body_exited.connect(_on_player_lost)
	patrol_timer = Timer.new()
	add_child(patrol_timer)
	patrol_timer.one_shot = true
	patrol_timer.timeout.connect(_on_patrol_timer_timeout)
	patrol_timer.start(randf_range(minimum_patrol_time, maximum_patrol_time))

Works nicely! tysm!

1 Like