Hi! I’m making a quest in my game where the player character has to follow an NPC. There are tutorials on how to get an NPC to follow a path, and that’s not my problem- rather, my problem is I can’t find a single tutorial on how to make the NPC stop walking if the player character gets too far away from them. I want the NPC to stop and wait; I just can’t figure out how. Any tips?
You could use a timer with a countdown and check the npcs distance to the player every x seconds. There are many ways to check distance. Maybe an area node would be simplest? You can tie the timeout-function of the timer node to a function that checks overlapping areas, and if none of them are the player, stop walking.
Area.get_overlapping_areas() or something like that is the name of the function to get overlapping areas.
if player.global_position.distance_to(global_position) > max_distance:
stop_following()
run this on every tick of the NPC. I will guess you are running pathfinding on the physics process. then every physics frame it checks if it’s a distance away from the next point or if it has completed its path.
well, just add a expression evaluating the distance of the NPC to the player, and change the state if that is the case.
you should be using a state machine to manage the behaviour of the NPC AI.
Its a way to make separate different behaviours. Like “sleeping”, “angered”, “fleeing” etc. It is probably overkill for your NPC unless you plan to introduce additional states.
I would also try to avoid checking the distance every frame as the start and stop movement will be sudden and jerky. I would rather use a timer as it introduces a bit of randomness (sometimes you will get in/out of range when 0.12 sec remains, sometimes when 0.3 sec remains) and will be a lot smoother (not start/stop moving the exact frame you get in/out of range).
state machine is a programming design pattern. It is used in all traditional AIs.
your NPC has a “state” variable, then, depending on the current state, it performs different behaviours.
enum State {
WAITING,
FOLLOW,
CHASE,
ATTACK,
DEAD,
#ETC
}
var state : State = State.WAITING
func _physics_process(delta) -> void:
match state:
State.FOLLOW:
#follow code
follow(player)
State.WAITING:
#look for something to do.
# in enemies this state waits for the player to get into range, or it can be nothing and the state change is triggered from a collision.
pass
State.ATTACK:
#look at enemy and attack. if the enemy gets too far away, change to CHASE state.
#when the enemy dies, change state.
attack()
State.CHASE:
#like follow but follow the current enemy.
follow(enemy)
#we always execute move_and_slide(), regardless of state.
move_and_slide()
all NPCs and enemies must have a state machine. there is no such thing as overkill, only messy code.
even just a DEAD state is a reliable and tidy way of making sure an enemy stays down.
always write code thinking of the future.
the other advantage is inheritance, a base class can use a similar script for inherited classes and share functionality, and this allows you to do a lot of things: since both Enemy and NPC would inherit from a base class, they can both have a damage() method, or a heal() method, they can be put into an Array together, etc.
that is not a problem, and the solution you suggest is very messy.
CharacterBody3D doesn’t “suddenly stop”, it is handled by velocity and move_and_slide(), and animations have blend time. a “stopping” state can be used for the transition, or a tween can execute the changes over time and in order.
but I don’t think this is even a thing. stop_following() would change state, and that would change what the NPC is trying to do.
func stop_following() -> void:
state = State.WAITING
velocity = Vector3.ZERO
Okay, so i implemended the code, and its giving me three different errors
extends Node2D
var Player = "../CharacterBody2D"
enum State {
WAITING,
FOLLOW
#ETC
}
var state : State = State.WAITING
func _physics_process(delta) -> void:
match state:
State.FOLLOW:
#follow code
follow(Player)
State.WAITING:
#look for something to do.
# in enemies this state waits for the player to get into range, or it can be nothing and the state change is triggered from a collision.
pass
#we always execute move_and_slide(), regardless of state.
move_and_slide()
func stop_following() -> void:
state = State.WAITING
velocity = Vector3.ZERO
Line 20:Function “follow()” not found in base self.
Line 26:Function “move_and_slide()” not found in base self.
Line 30:Identifier “velocity” not declared in the current scope.
Your first bug is because you didn’t define follow() in your NPC code that you are showing, the second two are because you’re making it a Node2D and I am assuming/hoping it is actually a CharacterBody2D. So some updates to your code:
extends CharacterBody2D #This has to be the actual type the object
enum State { #Enums always go at the top of the file.
WAITING,
FOLLOW, #Every line in a declared Array or Dictionary should be followed with a comma
}
var state: State = State.WAITING
@onready var area_2d: Area2D = $Area2D # See how to set this up below
func _ready() -> void:
area_2d.body_entered(_on_body_entered)
area_2d.body_exited(_on_body_exited)
func _physics_process(delta: float) -> void:
match state:
State.FOLLOW:
follow(delta)
State.WAITING:
velocity = Vector2.ZERO
move_and_slide()
#Have the NPC follow the path in this code, use delta to make the movement smooth
func follow(delta: float) -> void:
#Put your code here and delete this comment when you do.
pass #Also remove the pass here when you add your code.
# This is our new stop_following() function.
func _on_body_exited(body: Node2D) -> void:
state = State.WAITING
print("In WAITING mode.")
# This switches state back to following.
func _on_body_entered(body: Node2D) -> void:
state = State.FOLLOW
print("In FOLLOW mode.")
To make this work, you need to:
Add an Area2D node to the NPC.
Add a CollisionShape2D node to the Area2D node.
In the Inspector add a Circle Shape to the CollisionShape2D.
Define the radius of the Circle Shape to be how far away you want the NPC to get before it stops and waits for the player.
Select the Area2D and open the Collision section.
Clear Layer 1 and Mask 1, and set Mask 2. (You can right-click on Mask 2 to and rename it Player. If you already have a collision layer representing the Player, obviously use that instead.)
When you run your code, the NPC will detect if the player is in range and print out whether the player is close enough or too far anytime that changes. Then you just have to add your follow code.
NOTE:This method is different than the two recommended before because based on seeing your code, I deduce that this method will be easiest for your level of coding experience in the long run, and will give you exposure to Area2D nodes and how they work.
extends CharacterBody2D #This has to be the actual type the object
enum State { #Enums always go at the top of the file.
WAITING,
FOLLOW, #Every line in a declared Array or Dictionary should be followed with a comma
}
@onready var Player = $CharacterBody2D
var state: State = State.WAITING
@onready var area_2d: Area2D = $Area2D # See how to set this up below
func _ready() -> void:
area_2d.body_entered.connect(_on_body_entered)
area_2d.body_exited.connect(_on_body_exited)
func _physics_process(delta: float) -> void:
match state:
State.FOLLOW:
follow(delta)
State.WAITING:
velocity = Vector2.ZERO
move_and_slide()
func _on_body_exited(Player) -> void:
state = State.WAITING
print("In WAITING mode.")
# This switches state back to following.
func _on_body_entered(Player) -> void:
state = State.FOLLOW
print("In FOLLOW mode.")
#Have the NPC follow the path in this code, use delta to make the movement smooth
func follow(delta: float) -> void:
#Put your code here and delete this comment when you do.
pass #Also remove the pass here when you add your code.
(I did copy and paste some areas which is why its a little jumbled, but i get no errors )
But when i hit play, theres nothing in the debug window and the debug window gives me the warnings:
[Ignore]Line 29 (UNUSED_PARAMETER):The parameter “Player” is never used in the function “_on_body_exited()”. If this is intended, prefix it with an underscore: “_Player”.
[Ignore]Line 29 (SHADOWED_VARIABLE):The local function parameter “Player” is shadowing an already-declared variable at line 8 in the current class.
[Ignore]Line 35 (UNUSED_PARAMETER):The parameter “Player” is never used in the function “_on_body_entered()”. If this is intended, prefix it with an underscore: “_Player”.
[Ignore]Line 35 (SHADOWED_VARIABLE):The local function parameter “Player” is shadowing an already-declared variable at line 8 in the current class.
[Ignore]Line 41 (UNUSED_PARAMETER):The parameter “delta” is never used in the function “follow()”. If this is intended, prefix it with an underscore: “_delta”.
(i also did the collision as well so thank you for that :D)
Alrighty I copy and pasted the code but its still not printing in the debug menu, its giving me a warning and an error
(Tessa’s the name i gave the npc scene, for continuity’s sake)
W 0:00:00:484 GDScript::reload: The parameter “delta” is never used in the function “follow()”. If this is intended, prefix it with an underscore: “_delta”.
UNUSED_PARAMETER
tessa.gd:39 @