Issues with AI move_and_slide()

Godot Version

Godot 4.6.2

Question

I’m currently trying to get an AI NPC to move towards a specific point, but the usual move_and_slide() and use of navigation agent isn’t working this time for a reason I can’t figure out. This is the current code I have, reformatted to try and go towards the player for debug purposes. It exists within the physics process function.

var player = $“../Player”
nav_agent.set_target_position(player.global_transform.origin)
var next_nav_point = nav_agent.get_next_path_position()
velocity = (next_nav_point - global_transform.origin).normalized() * baseSpeed
look_at(Vector3(global_position.x + velocity.x, global_position.y, global_position.z + velocity.z), Vector3.UP)
move_and_slide()

This causes the NPC to immediately shoot off in the positive Z direction, no matter what I change. This is the only instance that changes the velocity of the NPC, so I have no idea what else could be causing it, as the same block of code works on enemies within the same project.

Thanks for any help you can give!

Please format your code correctly so it is readable.

Also tell us how it is different from the code that works.

extends CharacterBody3D

#CONSTANTS

#Alert States
const ST_PASSIVE = 0
const ST_SUSPICIOUS = 1
const ST_ALERT = 2
const ST_SEARCH = 3
const ST_AGRO = 4
const ST_HOLD = 5
const ST_HOLD_AGRO = 6
const ST_FLEE = 7
const ST_COWER = 8

#Passive Behaviour
const PB_IDLE = 1
const PB_WANDER = 3
const PB_PATROL = 4

#Actions
const ACT_INVESTIGATE = 0
const ACT_RETURN = 1
const ACT_INTERACT = 2
const ACT_STAND = 3
const ACT_MOVE = 4

#Agression Caps
const AC_LOW = 3
const AC_MED = 7
const AC_HIGH = 10

#Variables

#Core Stats
@onready var health = 10
@onready var baseSpeed = 2

#Appearance

#Behaviour
@onready var agression = 5
@onready var alert = 0
@onready var state = ST_PASSIVE
@onready var passiveBehaviour = PB_WANDER
@export var wanderArea: NodePath
@onready var faction = null
@onready var alertDistance = 10
@onready var suspiciousThreshold = 100
@onready var alertThreshold = 200
@onready var agroThreshold = 300
@onready var alertGroups = Array()
@onready var currentAlerts = Array()
@onready var sightTarget = null
@onready var overlapAreas = Array()

#Passive
@onready var idleTarget = null
@onready var idleTimer = 0
@onready var idleAnim = null
@onready var action = null

#Nodes
@onready var animPlayer = $AnimationPlayer
@onready var sightRay = $SightRay
@onready var areaNode = $Area3D
@onready var nav_agent = $NavigationAgent3D

func _ready():
	alertGroups.append(StimulusGlobal.SG_GUNSHOT)
	alertGroups.append(StimulusGlobal.SG_PLAYER)

func _physics_process(delta: float) -> void:

	if state == ST_PASSIVE:
		if passiveBehaviour == PB_WANDER:
			wander_behaviour()
		if passiveBehaviour == PB_IDLE:
			idle_behaviour()
	
	if agression <= AC_LOW:
		if alert_check() == ST_SUSPICIOUS or alert_check() == ST_ALERT or alert_check() == ST_AGRO:
			state_set(ST_FLEE)
	elif agression > AC_LOW and agression <= AC_MED:
		if alert_check() == ST_SUSPICIOUS or alert_check() == ST_ALERT:
			state_set(ST_HOLD)
		elif alert_check() == ST_AGRO:
			state_set(ST_HOLD_AGRO)
	elif agression > AC_MED and agression <= AC_HIGH:
		if alert_check() == ST_SUSPICIOUS:
			state_set(ST_SUSPICIOUS)
		elif alert_check() == ST_ALERT:
			state_set(ST_ALERT)
		elif alert_check() == ST_AGRO:
			state_set(ST_AGRO)

	#Check Stimulus
	overlapAreas = areaNode.get_overlapping_areas()
	for i in overlapAreas.size():
		if overlapAreas[i].is_in_group("Stimulus"):
			var stimulus = overlapAreas[i].get_parent()
			if stimulus_group_check(stimulus.group) == true and stimulus_sight_check(stimulus.type, stimulus) == true and stimulus_one_time_check(stimulus) == true:
				#print(overlapAreas[i])
				alert += stimulus.intensity
				
	var player = $"../Player"
	nav_agent.set_target_position(player.global_position)
	var next_nav_point = nav_agent.get_next_path_position()
	velocity = global_position.direction_to(next_nav_point) * 0.0001
	look_at(Vector3(global_position.x + velocity.x, global_position.y, global_position.z + velocity.z), Vector3.UP)
	move_and_slide()
				
	#move_and_slide()
				
func passive_behaviour():
	pass
	#var idleTarget
	#var idleTimer: int
	#var animTimer: int
	#var idleAnim
	#var action: int
	#if action != ACT_MOVE and action != ACT_IDLE:
	#	action = ACT_IDLE
	#	print(action)
	#if idleTarget == null:
	#	#print("YOOOOOOOO")
	#	idleTarget = get_node(wanderArea).get_child(1)
	#	#print(idleTarget)
	
	#if idleTimer <= 0 and action == ACT_IDLE:
	#	idleTarget = get_node(wanderArea).get_child(randi_range(0,get_node(wanderArea).get_child_count(false)))
	#	action = ACT_MOVE
	#if idleTarget != null and action == ACT_MOVE:
	#	if global_position.distance_to(idleTarget.global_position) < 1:
	#		action = ACT_IDLE
	#		idleTimer = 3
		
	#if action == ACT_IDLE:
	#	idleTimer -= get_process_delta_time()
	#if action == ACT_MOVE and idleTarget != null:
	#	velocity = Vector3.ZERO
	#	nav_agent.set_target_position(idleTarget.global_transform.origin)
	#	var next_nav_point = nav_agent.get_next_path_position()
	#	velocity = (next_nav_point - global_transform.origin).normalized() * baseSpeed

func idle_behaviour():
	pass

func wander_behaviour():
	#print(action)
	if action == null:
		action = ACT_STAND
		idleTimer = 5
	if action == ACT_STAND:
		idleTimer -= get_process_delta_time()
		if idleTimer <= 0:
			idleTarget = get_node(wanderArea).get_child(randi_range(0,get_node(wanderArea).get_child_count(false) - 1))
			print(idleTarget)
			action = ACT_MOVE
	#if action == ACT_MOVE:
	#	velocity = Vector3.ZERO
	#	nav_agent.set_target_position(idleTarget.global_transform.origin)
	#	var next_nav_point = nav_agent.get_next_path_position()
	#	velocity = (next_nav_point - global_transform.origin).normalize() * baseSpeed
	#	move_and_slide()

func suspicious_behaviour():
	pass

func alert_behaviour():
	pass

func agro_behaviour():
	pass

func hold_behaviour():
	pass

func hold_agro_behaviour():
	pass

func flee_behaviour():
	pass

func cower_behaviour():
	pass

func state_set(stateCheck):
	if stateCheck != state:
		state = stateCheck

func state_check(stateCheck):
	if state == stateCheck:
		return true
	else:
		return false

func alert_check():
	if alert < suspiciousThreshold:
		return ST_PASSIVE
	elif alert >= suspiciousThreshold and alert < alertThreshold:
		return ST_SUSPICIOUS
	elif alert >= alertThreshold and alert < agroThreshold:
		return ST_ALERT
	elif alert >= agroThreshold:
		return ST_AGRO

func stimulus_group_check(group):
	#print("STIMGROUPCHECK")
	if alertGroups.has(group) == true:
		#print("IS IN GROUP")
		return true
	else:
		#print("IS NOT IN GROUP")
		return false

func stimulus_sight_check(type, target):
	#print("STIMSIGHTCHECK")
	if type == StimulusGlobal.ST_HEAR:
		#print("CAN HEAR")
		return true
	elif type == StimulusGlobal.ST_SIGHT:
		sightRay.target_position = sightRay.to_local(target.global_position)
		sightRay.force_raycast_update()
		if sightRay.is_colliding() and sightRay.get_collider() == target.caller:
				#print("CAN SEE")
				return true
		else:
			#print(sightRay.get_collider())
			#print(target)
			#print("CANT SEE")
			return false

func stimulus_one_time_check(target):
	#print("STIMONETIMECHECK")
	#print(target.seenTargets.size())
	#print(target.oneTime)
	if target.oneTime == false:
		#print("NOT ONE TIME")
		return true
	elif target.oneTime == true:
		#print("IS ONE TIME")
		if target.seenTargets.has(self) == true:
			#print("ONE TIME FAIL")
			return false
		else:
			#print("ONE TIME PASS AND ADD")
			target.seenTargets.append(self)
			return true

Above is the code that doesn’t work, and below is an instance of the code that does work:

extends CharacterBody3D


var player = null
var state_machine
var health = 3
var rng = RandomNumberGenerator.new()
var dead = false

const SPEED = 3.0
const ATTACK_RANGE = 6
const AGRO_RANGE = 15

@export var player_path: NodePath

@onready var nav_agent = $NavigationAgent3D
@onready var anim_tree = $AnimationTree
@onready var audio = $Audio

@onready var pistolsound = preload("res://Sounds/PistolShot.wav")

# Called when the node enters the scene tree for the first time.
func _ready():
	player = get_node(player_path)
	state_machine = anim_tree.get("parameters/playback")
	

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	velocity = Vector3.ZERO
	#AI PROTOTYPE
	
	#Idle
	if global_position.distance_to(player.global_position) > AGRO_RANGE and health > 0:
		state_machine.travel("idle")
		velocity = Vector3.ZERO
	
	#Chase
	elif global_position.distance_to(player.global_position) <= AGRO_RANGE \
	and global_position.distance_to(player.global_position) > ATTACK_RANGE and health > 0:
		#anim_tree.set("parameters/conditions/run", true)
		state_machine.travel("run")
		nav_agent.set_target_position(player.global_transform.origin)
		var next_nav_point = nav_agent.get_next_path_position()
		velocity = (next_nav_point - global_transform.origin).normalized() * SPEED
		look_at(Vector3(global_position.x + velocity.x, global_position.y, global_position.z + velocity.z), Vector3.UP)
	
	#Aim
	elif global_position.distance_to(player.global_position) <= ATTACK_RANGE and health > 0: 
		#anim_tree.set("parameters/conditions/aim", true)
		if state_machine.get_current_node() != "aiming" and state_machine.get_current_node() != "shoot":
			state_machine.travel("aim")
		look_at(Vector3(player.global_position.x, global_position.y, player.global_position.z), Vector3.UP)
		var shootroll: int = rng.randf_range(0,50)
		if shootroll == 1 and state_machine.get_current_node() != "shoot": 
			state_machine.travel("shoot")
			audio.stream = pistolsound
			audio.play()
			#print("enemyshot")
			player.damage(5)
	
	
	move_and_slide()
	


func _target_in_range():
	return global_position.distance_to(player.global_position) < ATTACK_RANGE
	
func _hit_finished():
	player.hit()


func _on_area_3d_body_part_hit(dam):
	health -= dam
	if health <= 0 and dead == false:
		state_machine.travel("die")
		set_collision_layer_value(1, false)
		set_collision_mask_value(1, false)
		dead = true

What’s the code for StimulusGlobal look like?

extends Node

#Stimulus Groups
const SG_GUNSHOT = 0
const SG_BODY = 1
const SG_PLAYER = 2

#Stimulus Types
const ST_SIGHT = 0
const ST_HEAR = 1

#Stimulus Scene
@onready var stimulusScene = preload("res://Scenes/NPCs/stimulus.tscn")

#Stimulus Array
@onready var stimulusArray = Array()

# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	pass # Replace with function body.


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
	pass

func create_stimulus(caller,type,range,group,oneTime,intensity,time):
	var stimulusInstance = stimulusScene.instantiate()
	caller.add_child(stimulusInstance)
	stimulusInstance.caller = caller
	stimulusInstance.type = type
	stimulusInstance.range = range
	stimulusInstance.group = group
	stimulusInstance.oneTime = oneTime
	stimulusInstance.intensity = intensity
	stimulusInstance.time = time
	

This is it. It’s meant to be be how various things such as gunshots, NPCs, items etc. alert NPCs.

I’m assuming since you didn’t give it a class_name it is an Autoload.

Yes it is

Ok. Give me a few.

StimulusGlobal

I’m going to start with StimulusGlobal.

  • Use const instead of @onready variables for preloading files that won’t change. You can do this by dragging the file into your script, pressing and holding Ctrl after you drag but before you drop, and dropping the file into your code. It will also use a UID, which means if you move the file later your code won’t break.
  • Follow the GDScript Style Guide for naming variables. The recommendation is to use snake_case. You are currently using camelCase, which means you’re coming from another language that did that, or followed a tutorial from someone who taught you that. Either way, it’s going to increase the confusion in you reading code in the future, and other people reading your code.
  • Make sure all your functions have a return value, even if it’s just void. This habit will help you debug problems when you are returning values.
  • Delete code you aren’t using. Especially the default _ready() and _process() functions. Unused code clutters things up and makes problems harder to debug.
  • You should type variables like Arrays. You do not need to initialize empty Arrays or Dictionaries however.
  • Be careful not to use reserved words as variable names. In this case: range. It can cause you really weird debugging problems later. The compiler is literally telling you not to do this. Pay attention to those warnings.

    Ignoring them causes what’s known as Error Blindness, where you have so many warnings and errors popping up, you just ignore them.
  • Spaces between function arguments make them easier to read.
  • Function arguments should be typed. This makes sure you aren’t assigning the wrong value to something. (I made my best guesses.)
  • When you instantiate an object, typing it makes sure that the values assigned to it are the correct ones (in combination with the tip above). You will also get autocompletion in the editor. However to do this, your Scene needs an attached script with a class_name. (I created a stand-in one with a dummy Type Enum.) Then you’ll know when you’re passing the wrong thing.
  • I added in Enums for Stimulus.Type and Stimulus.Group to show you how you can use that anywhere in the code to pass the type instead of using const values over and over in files. These will also still match your current const values when evaluated as ints. I change "SIGHT to “VISUAL” and “HEAR” to “AUDIO”. You can change them back if you like.
  • Rename variables like stimulusInstance to just stimulus. Yes, it’s an instance, but when you use a const it becomes clear that the uppercase STIMULUS is the scene, and stimulus is the instance. Which makes the rest of the code shorter and easier to read.

Example stimulus.gd

class_name Stimulus extends Node

enum Group {
	GUNSHOT,
	BODY,
	PLAYER,
}
enum Type {
	VISUAL,
	AUDIO,
}

var type: Type
var detection_range: float
var group: Group
var one_time: bool
var intensity: float
var time: float

New stimulus_global.gd

extends Node

#Stimulus Scene
const STIMULUS = preload("uid://cwh2l0p6n1bdn")

#Stimulus Array
@onready var stimulus_array: Array


func create_stimulus(caller: Node, type: Stimulus.Type, detection_range: float, group: Stimulus.Group, one_time: bool, intensity: float, time: float) -> void:
	var stimulus: Stimulus = STIMULUS.instantiate()
	caller.add_child(stimulus)
	stimulus.caller = caller
	stimulus.type = type
	stimulus.detection_range = detection_range # You should change this variable name in your scene.
	stimulus.group = group
	stimulus.one_time = one_time # You should change this variable name in your scene.
	stimulus.intensity = intensity
	stimulus.time = time

You’ll notice that the code is easier to read. Now we can take a look at your working CharacterBody3D.

Working CharacterBody3D

I did some cleanup like in the previous script, but I’m only calling out new things.

  • Follow the GDScript Style Guide for variable order at the top of the file.
  • You do not need to initialize variables as null. If they are not assigned, they will evaluate to null anyway. What is more important is giving them a Type.
  • You do not need to initialize the RandomNumberGenerator, despite what a tutorial might have told you. Godot already does that for you. Seeding should only be done if you plan on tracking the seeds and giving the player some control over them to get deterministic results. (Like in Minecraft where if you give it the same seed you get the exact same terrain every time.)
  • As above, when preloading a scene, preloading a sound can be done as a const. However in this case you only use your AudioStreamPlayer to play the one sound. So a better thing to do is put the sound in the player and name it appropriately. Since this is a stealth game, I put it in an AudioStreamPlayer3D. In this way it will sound farther away the farther the NPC is from the Player. But to hear it, you’ll need to put an AudioListenered3D on either the Player or the Camera3D.
  • state_machine is a common name in video games. Declaring it as type AnimationNodeStateMachinePlayback makes it clear what it is.
  • When making @onready variables, you can do it the same as a const. Click and drag the nide you want to make a variable for. Click and hold the Ctrl key after dragging and before dropping. Drop the node in the Script Editor. This will add a Type declaration which will enable autocomplete when typing your code.
  • You are using randf_range(0, 50) and assigning it to an int. Just use randi_range(0, 50). Also, you don’t need to call it from rng anymore.
  • Put only one line between sections of code in your functions. Put two between functions. It makes the code easier to read.
  • When a function returns a bool value, it is common to prepend the name with is. E.G. _is_target_in_range()
  • When a function happens after something is completed or when a signal is sent, it is common to prepend the name with on. E.G. _on_hit_finished()
  • Don’t abbreviate variables just for the sake of abbreviation. damage is much clearer, for example, than dam.
  • If you don’t use an argument like delta in physics_process(), prepend it with an underscore.
  • I don’t knw what your project looks like, so I’m not sure why you’re exporting a NodePath to find the Player when you could just do @export player: CharacterBody3D. So I left it. But you shouldn’t use a NodePath if you can export the actual object, because actually exporting the object creates a reference which is just a memory address and is smaller.

New CharacterBody3D

extends CharacterBody3D

const SPEED = 3.0
const ATTACK_RANGE = 6
const AGRO_RANGE = 15

@export var player_path: NodePath

var player: CharacterBody3D
var state_machine: AnimationNodeStateMachinePlayback
var health: int = 3
var dead: bool = false

@onready var navigation_agent_3d: NavigationAgent3D = $NavigationAgent3D
@onready var animation_tree: AnimationTree = $AnimationTree
@onready var pistol_shot: AudioStreamPlayer3D = $PistolShot


func _ready() -> void:
	player = get_node(player_path)
	state_machine = animation_tree.get("parameters/playback")


func _process(_delta: float) -> void:
	velocity = Vector3.ZERO
	#AI PROTOTYPE
	
	#Idle
	if global_position.distance_to(player.global_position) > AGRO_RANGE and health > 0:
		state_machine.travel("idle")
		velocity = Vector3.ZERO
	
	#Chase
	elif global_position.distance_to(player.global_position) <= AGRO_RANGE \
	and global_position.distance_to(player.global_position) > ATTACK_RANGE and health > 0:
		state_machine.travel("run")
		navigation_agent_3d.set_target_position(player.global_transform.origin)
		var next_nav_point: Vector3 = navigation_agent_3d.get_next_path_position()
		velocity = (next_nav_point - global_transform.origin).normalized() * SPEED
		look_at(Vector3(global_position.x + velocity.x, global_position.y, global_position.z + velocity.z), Vector3.UP)
	
	#Aim
	elif global_position.distance_to(player.global_position) <= ATTACK_RANGE and health > 0: 
		if state_machine.get_current_node() != "aiming" and state_machine.get_current_node() != "shoot":
			state_machine.travel("aim")
		look_at(Vector3(player.global_position.x, global_position.y, player.global_position.z), Vector3.UP)
		var shootroll: int = randi_range(0, 50)
		if shootroll == 1 and state_machine.get_current_node() != "shoot": 
			state_machine.travel("shoot")
			pistol_shot.play()
			player.damage(5)
	
	move_and_slide()


func _is_target_in_range() -> bool:
	return global_position.distance_to(player.global_position) < ATTACK_RANGE


func _on_hit_finished() -> void:
	player.hit()


func _on_area_3d_body_part_hit(damage: int) -> void:
	health -= damage
	if health <= 0 and dead == false:
		state_machine.travel("die")
		set_collision_layer_value(1, false)
		set_collision_mask_value(1, false)
		dead = true

None of the functionality is changed, but now the code is much clearer. Which makes it easier to debug. Which brings us to the problem file.

Broken CharacterBody3D

  • Your core stats are just variables. They do not need to be @onready variables. Same with your behavior and passive variables. They don’t do anything in the ready phase.
  • Change suspiciousDistance to suspicion_threshold because it’s better grammar.
  • overlap_areas does not need to be a class variable.
  • You have declared player using a hardcoded node path inside physics_process(). I moved it to an @export variable so you can assign it, but also so it doesn’t get lost. I’m assuming this is just for testing. Ultimately, you’ll want to detect the player and then go after it.
  • move_and_slide() was commented out in your code. This is likely your original problem.

Fixed New characterBody3D script

extends CharacterBody3D

enum AlertState {
	PASSIVE,
	SUSPICIOUS,
	ALERT,
	SEARCH,
	AGRO,
	HOLD,
	HOLD_AGRO,
	FLEE,
	COWER
}
#Set these as the specific values in case they have a reason to be that value.
enum PassiveBehaviour {
	IDLE = 1,
	WANDER = 3,
	PATROL = 4,
}
#Consider adding a NONE action state
enum Action {
	INVESTIGATE,
	RETURN,
	INTERACT,
	STAND,
	MOVE,
}

#Agression Caps
const AC_LOW = 3
const AC_MED = 7
const AC_HIGH = 10

@export var wanderArea: NodePath #Could this be a direct object export?
@export var player: CharacterBody3D

#Core Stats
var health: int = 10
var base_speed: float = 2.0

#Appearance

#Behaviour
var agression: int = 5
var alert: int = 0
var state: AlertState = AlertState.PASSIVE
var passive_behaviour: PassiveBehaviour = PassiveBehaviour.WANDER
#var faction: int # Guessed this was an int because you like ints.
#var alert_distance: float = 10.0
var suspicion_threshold: int = 100
var alert_threshold: int = 200
var agro_threshold: int = 300
var alertGroups: Array[Stimulus.Group]
#var current_alerts: Array
#var sight_target: CharacterBody3D

#Passive
var idle_target: Node3D
var idle_timer: float = 0.0
#var idle_anim: String
var action: Action

#@onready var animation_player: AnimationPlayer = $AnimationPlayer
@onready var sight_ray: RayCast3D = $SightRay
@onready var area_3d: Area3D = $Area3D
@onready var navigation_agent_3d: NavigationAgent3D = $NavigationAgent3D


func _ready() -> void:
	alertGroups.append(Stimulus.Group.GUNSHOT)
	alertGroups.append(Stimulus.Group.PLAYER)


func _physics_process(_delta: float) -> void:
	if state == AlertState.PASSIVE:
		if passive_behaviour == PassiveBehaviour.WANDER:
			wander_behaviour()
		if passive_behaviour == PassiveBehaviour.IDLE:
			idle_behaviour()
	
	if agression <= AC_LOW:
		if alert_check() == AlertState.SUSPICIOUS or alert_check() == AlertState.ALERT or alert_check() == AlertState.AGRO:
			set_state(AlertState.FLEE)
	elif agression > AC_LOW and agression <= AC_MED:
		if alert_check() == AlertState.SUSPICIOUS or alert_check() == AlertState.ALERT:
			set_state(AlertState.HOLD)
		elif alert_check() == AlertState.AGRO:
			set_state(AlertState.HOLD_AGRO)
	elif agression > AC_MED and agression <= AC_HIGH:
		if alert_check() == AlertState.SUSPICIOUS:
			set_state(AlertState.SUSPICIOUS)
		elif alert_check() == AlertState.ALERT:
			set_state(AlertState.AGRO)

	#Check Stimulus
	var overlap_areas: Array = area_3d.get_overlapping_areas()
	for i in overlap_areas.size():
		if overlap_areas[i].is_in_group("Stimulus"):
			var stimulus: Node3D = overlap_areas[i].get_parent()
			if stimulus_group_check(stimulus.group) == true and stimulus_sight_check(stimulus.type, stimulus) == true and stimulus_one_time_check(stimulus) == true:
				#print(overlap_areas[i])
				alert += stimulus.intensity
				
	navigation_agent_3d.set_target_position(player.global_position)
	var next_nav_point: Vector3 = navigation_agent_3d.get_next_path_position()
	velocity = global_position.direction_to(next_nav_point) * 0.0001
	look_at(Vector3(global_position.x + velocity.x, global_position.y, global_position.z + velocity.z), Vector3.UP)
	move_and_slide()


func execute_passive_behaviour() -> void:
	pass
	#var idle_target
	#var idle_time: floatr: int.0
	#var animTimer: int
	#var: String
	#var action: int
	#if action != ACT_MOVE and action != ACT_IDLE:
	#	action = ACT_IDLE
	#	print(action)
	#if idle_target == null:
	#	#print("YOOOOOOOO")
	#	idle_target = get_node(wanderArea).get_child(1)
	#	#print(idle_target)
	
	#if idle_timer <= 0 and action : float== ACT_IDLE:.0
	#	idle_target = get_node(wanderArea).get_child(randi_range(0,get_node(wanderArea).get_child_count(false)))
	#	action = ACT_MOVE
	#if idle_target != null and action == ACT_MOVE:
	#	if global_position.distance_to(idle_target.global_position) < 1:
	#		action = ACT_IDLE
	#		idle_timer: float = 3.0
		
	#if action == ACT_IDLE:
	#	idle_timer : float-= get_process_delta_time().0
	#	nav_agent.set_target_position(idle_target.global_transform.origin)
	#	var next_nav_point = nav_agent.get_next_path_position()
	#	velocity = (next_nav_point - global_transform.origin: float).normalized() * base_speed


func idle_behaviour() -> void:
	pass


func wander_behaviour() -> void:
	#print(action)
	if action == null:
		action = Action.STAND
		idle_timer = 5.0
	if action == Action.STAND:
		idle_timer -= get_process_delta_time()
		if idle_timer <= 0:
			idle_target = get_node(wanderArea).get_child(randi_range(0,get_node(wanderArea).get_child_count(false) - 1))
			print(idle_target)
			action = Action.MOVE
	#	nav_agent.set_target_position(idle_target.global_transform.origin)
	#	var next_nav_point = nav_agent.get_next_path_position()
	#	velocity = (next_nav_point - global_transform.origin: float).normalize() * base_speed
	#	move_and_slide()


func suspicious_behaviour() -> void:
	pass


func alert_behaviour() -> void:
	pass


func agro_behaviour() -> void:
	pass


func hold_behaviour() -> void:
	pass


func hold_agro_behaviour() -> void:
	pass


func flee_behaviour() -> void:
	pass


func cower_behaviour() -> void:
	pass


func set_state(new_state: AlertState) -> void:
	if new_state != state:
		state = new_state


func state_check(state_to_check: AlertState) -> bool:
	if state == state_to_check:
		return true
	else:
		return false


func alert_check() -> AlertState:
	if alert < suspicion_threshold:
		return AlertState.PASSIVE
	elif alert >= suspicion_threshold and alert < alert_threshold:
		return AlertState.SUSPICIOUS
	elif alert >= alert_threshold and alert < agro_threshold:
		return AlertState.ALERT
	elif alert >= agro_threshold:
		return AlertState.AGRO
	
	return AlertState.PASSIVE ##Added default return state


func stimulus_group_check(group: Stimulus.Group) -> bool:
	if alertGroups.has(group) == true:
		return true
	else:
		return false


func stimulus_sight_check(type: Stimulus.Type, target: Node3D) -> bool:
	if type == Stimulus.Type.AUDIO:
		return true
	elif type == Stimulus.Type.VISUAL:
		sight_ray.target_position = sight_ray.to_local(target.global_position)
		sight_ray.force_raycast_update()
		if sight_ray.is_colliding() and sight_ray.get_collider() == target.caller:
				return true
	
	return false #Default return value


func stimulus_one_time_check(target: Node3D) -> bool:
	#print("STIMONETIMECHECK")
	#print(target.seenTargets.size())
	#print(target.oneTime)
	if target.one_time == false:
		#print("NOT ONE TIME")
		return true
	elif target.oneTime == true:
		#print("IS ONE TIME")
		if target.seenTargets.has(self) == true:
			#print("ONE TIME FAIL")
			return false
		else:
			#print("ONE TIME PASS AND ADD")
			target.seenTargets.append(self)
			return true
	
	return false #Default return value

Conclusion

You had a lot going on in your scripts. Going line-by-line I was able to find what I think was the problem - which was you had move_and_slide() commented out. Hence, no movement.

I commented out a number of unused variables. This file is too big. It’s doing too many things - which means it’s hard to figure out what is going on. Simplifying your file through refactoring would help with debugging. It would also help to follow the GDScript Style Guide. Learning to follow it will help you find things in your own code.

Naming is also very important. When thinking about naming variables and functions, do what you can to make using them make grammatical sense. This will also make your coding easier. Finally, using Enums will make your life a lot easier because it will prevent typos, and it allows you to use the editor’s autocomplete functionality.

Hopefully this helps solve your problem and teaches you more about GDScript. If the solution doesn’t work, or you have questions, let me know.

Hi,

Thanks for the extensive write up, however the character not moving wasn’t the orginal problem. The issue was that the character was immediately shooting off into the positive z direction upon starting up. Right above the commented out move_and_slide() you’re referencing is another move_and_slide() that isn’t commented out, so the original problem still persists.

I did not see a second move_and_slide(). wander_behaviour() wasn’t being called that I could see. Since I don’t have your full project, I can’t really test to see what’s going on. So here is what I would suggest you do.

  1. Comment everything out.
  2. Uncomment the pieces that you need to get it working.
  3. Continue uncommenting lines until you find the break.

Then you’ll know what code is causing it.

This isn’t in wander_behaviour(). I thought that might be the issue so I moved the navigation code to the physics process, and it’s still causing the issue. I was wondering if there were common reasons why objects with navigation agents would just shoot off in a direction instead of navigating towards the target.

No. There are not.

This often happens when you accidentally instantiate your CharacterBody3D twice / more than once in exactly the same place.

Or when you instantiate it clipped entirely through another physics body.

To check for multiple instantiations just check with a print message inside _ready

Hi, I checked this, and it’s only returned the print message once. The scene is placed directly in the game world scene so I didn’t think it would happen more than once. I did a bunch of debugging, and have found that it appears to be the armature that was imported via blender that’s causing the issue. I’ve noticed that the heirachy of the NPC that is having these physics problems is different to the one that’s working correctly, here are pictures to show.

Below is the one that doesn’t work, it has a Skeleton3D, then a physicalBoneSimulator3D as a child, which has all of the bones as PhysicalBone3Ds as children.

Below is the one that does work. It has a Skeleton3D, which then has all of the bones as BoneAttachment3Ds.

Hopefully this helps.

All I had was the recognition of the behaviour you mentioned and what caused it in my case. Afraid more detailed analysis is outside my expertise.