Why enemy AI never switches from wander to idle state?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By Boohooligan

Hi,

I’m trying to create a basic enemy AI that has two states: IDLE and WANDER.
I’m using Navmesh 3D demo from Godot’s asset store.

If stop_distance is set to 1.0, the AI will pathfind to a single location, then once (target_position - transform.origin).length() evaluates to value of about 5.7 the AI stops pathfinding never reaching the IDLE state again (tested with print() statements).

Setting the stop_distance to above 5.7 correctly switches between IDLE and WANDER states and AI proceeds to pathfind.

Any idea why this might be happening?

extends KinematicBody

var move_speed = 3.0

enum STATE {IDLE, WANDER}
var state = STATE.IDLE

onready var start_position = get_global_transform().origin
onready var target_position = get_global_transform().origin

var nav = null
var path = []

var stop_distance = 1.0

func _ready():
	nav = get_parent() # Navigation node is parent of enemy node in the scene tree during run-time

func _process(delta):
	match state:
		STATE.IDLE:
			var next_waypoint = Vector3(rand_range(5, 20), translation.y, rand_range(5, 20))
			target_position = start_position + next_waypoint
			var p = nav.get_simple_path(start_position, target_position, true)
			if not p.empty():
				path = Array(p)
				path.invert()
				state = STATE.WANDER
		STATE.WANDER:
			var to_walk = delta * move_speed
			var to_watch = Vector3.UP
			while to_walk > 0 and path.size() >= 2:
				var pfrom = path[path.size() - 1]
				var pto = path[path.size() - 2]
				to_watch = (pto - pfrom).normalized()
				var d = pfrom.distance_to(pto)
				if d <= to_walk:
					path.remove(path.size() - 1)
					to_walk -= d
				else:
					path[path.size() - 1] = pfrom.linear_interpolate(pto, to_walk / d)
					to_walk = 0
				var atpos = path[path.size() - 1]
				var atdir = to_watch
				atdir.y = 0
				var t = Transform()
				t.origin = atpos
				t = t.looking_at(atpos + atdir, Vector3.UP)
				set_transform(t)
				if path.size() < 2:
					path = []
					set_process(false)
			if (target_position - transform.origin).length() < stop_distance:
				start_position = get_global_transform().origin
				state = STATE.IDLE

Optional: Why divide to_walk by d for the rate argument in pfrom.linear_interpolate(pto, to_walk / d)?
Answer: this ensures the enemy AI lerps to destination at constant speed until the end, otherwise it will begin to slow down as it approaches the destination.

Optional 2: Why does the Navmesh3D demo project inverts the path array(after converting it to Array)? Unfortunately it’s not documented or explained in their code.
Answer: removing last element from array is more efficient rather than from the beginning.

:bust_in_silhouette: Reply From: Boohooligan

The issue was that target position wasn’t on the navigation mesh due to global vs local coordinates, but above it. Enemy AI would arrive at X and Z position but Y would be above the navigation mesh, which is why the length() was 5.7 units away from final position.

We need to make sure to calculate position and path in local space using to_local(), then convert the results back to global with to_global(), and use get_closest_point() of Navigation node to get proper target position on the navigation mesh. This will result is correct length().