So I've been trying to implement a version of this into a 3D game. My enemy is supposed to walk in circles between four navigation points (labeled in the code as nav_points), using a NavigationAgent3D to pathfind between them while simultaneously avoiding dynamic obstacles using the roughly the same system described in the link. I've additionally implemented raycast visualizers to help with debugging, which consist of a Node3D with a MeshInstance3D child with an elongated BoxMesh shape and a simple shader material that allows me to change their color based on danger. I've also got code to change their length based on interest and point them in the available movement directions.

The problem is that while my enemy is able to pathfind using this system, he eventually gets stuck on a wall for no apparent reason, and the raycast visualizers are all bunched up into a small wedge facing nearly the exact same direction, which is the direction opposite the wall he's stuck on. I'm not sure why the raycast visualizers aren't spread out in all directions, or whether that's a problem with my visualization code or if the available movement directions are also bunched up in tiny wedge.

Here's my code:

extends CharacterBody3D

var default_speed = 100
var target_is_nav_point = true
var raycast_visualization = true
var current_nav_index = 0

var number_of_directions = 16
var movement_directions = []
var interest = []
var danger = []
var raycast_visualizers = []
var chosen_direction = Vector3.ZERO

@onready var navigation_agent = $NavigationAgent3D
@onready var nav_points = [get_node("../NavPoint1"), get_node("../NavPoint2"), get_node("../NavPoint3"), get_node("../NavPoint4")]
@onready var nearest_nav_point = nav_points[0]
@onready var next_nav_point = nearest_nav_point
@onready var raycast_visualizer_scene = load("res://assets/enemies/raycast_visualizer.tscn")

func _ready():
	movement_directions.resize(number_of_directions)
	interest.resize(number_of_directions)
	danger.resize(number_of_directions)
	raycast_visualizers.resize(number_of_directions)
	
	for direction in number_of_directions:
		var angle = direction * 2 * PI / number_of_directions
		movement_directions[direction] = Vector2.UP.rotated(angle)
		
		if raycast_visualization:
			raycast_visualizers[direction] = raycast_visualizer_scene.instantiate()
			add_child(raycast_visualizers[direction])
			raycast_visualizers[direction].global_position = global_position
			raycast_visualizers[direction].visible = true
			var raycast_visualizer_mesh = raycast_visualizers[direction].get_node("RayCastVisualizer")
			var unique_raycast_visualizer_mesh = raycast_visualizer_mesh.mesh.duplicate()
			var unique_raycast_visualizer_mat = unique_raycast_visualizer_mesh.get("material").duplicate()
			raycast_visualizer_mesh.set("mesh", unique_raycast_visualizer_mesh)
			raycast_visualizer_mesh.set("material", unique_raycast_visualizer_mat)
		
		
	nearest_nav_point = nav_points[0]
	for i in nav_points.size():
		if global_position.distance_squared_to(nav_points[i].global_position) < global_position.distance_to(nearest_nav_point.global_position): 
			nearest_nav_point = nav_points[i]
	next_nav_point = nearest_nav_point
	
	set_physics_process(false)
	call_deferred("await_physics") 


func _physics_process(delta):
	set_interest()
	set_danger()
	var direction = choose_direction()
	velocity = direction * default_speed * delta
	move_and_slide()
	look_at(direction)


func await_physics():
	await get_tree().physics_frame
	navigation_agent.set_target_position(next_nav_point.global_position)
	target_is_nav_point = true


func set_interest():
	var next_path_position = to_local(navigation_agent.get_next_path_position())
	var path_direction = Vector2(next_path_position.x, next_path_position.z).normalized()
	for direction in number_of_directions:
		var dot = movement_directions[direction].rotated(rotation.y).dot(path_direction)
		interest[direction] = max(0, dot)



func set_danger():
	var space_state = get_world_3d().direct_space_state
	var danger_raycast_length = 2.0
	var danger_raycast_offset = Vector3(0, 1, 0)
	for direction in number_of_directions:
		#direction_3d is LOCAL
		var direction_3d: Vector3
		direction_3d.x = movement_directions[direction].rotated(rotation.y).x
		direction_3d.y = 0
		direction_3d.z = movement_directions[direction].rotated(rotation.y).y
		
		var cast_from = danger_raycast_offset
		var cast_to = direction_3d
		
		var danger_raycast = PhysicsRayQueryParameters3D.create(to_global(cast_from), to_global(cast_to), collision_mask, [self])
		var danger_raycast_result = space_state.intersect_ray(danger_raycast)
		
		if danger_raycast_result == {}: 
			danger[direction] = 0.0
		else:
			var collision_distance = cast_from.distance_to(danger_raycast_result.position)
			danger[direction] = remap(-collision_distance, -5.0, 0.0, 0.0, 1.0)
			danger[direction] = clamp(danger[direction], 0.0, 1.0)
		
		if raycast_visualization:
			raycast_visualizers[direction].position = danger_raycast_offset
			raycast_visualizers[direction].look_at(direction_3d)
			raycast_visualizers[direction].get_node("RayCastVisualizer").mesh.set("material/shader_parameter/blend", danger[direction])


func choose_direction():
	var chosen_direction := Vector2.ZERO
	var chosen_direction_3d := Vector3.ZERO
	for direction in number_of_directions:
		interest[direction] -= danger[direction]
		interest[direction] = clamp(interest[direction], 0.0, 1.0)
		
		if raycast_visualization:
			raycast_visualizers[direction].get_node("RayCastVisualizer").mesh.size.z = interest[direction] * 2
		
		chosen_direction += movement_directions[direction] * interest[direction]
	chosen_direction = chosen_direction.normalized()
	chosen_direction_3d.x = chosen_direction.rotated(rotation.y).x
	chosen_direction_3d.z = chosen_direction.rotated(rotation.y).y
	return chosen_direction_3d


func _on_NavigationAgent3D_target_reached():
	if current_nav_index + 1 <= nav_points.size() - 1:
		current_nav_index += 1
	else:
		current_nav_index = 0
	next_nav_point = nav_points[current_nav_index]

UPDATE: I fixed several issues with the original code through much trial and error, and at this point, my enemy is walking along the path, but facing the wrong direction. As I understand it, with the way I'm using the look_at() function, it shouldn't be possible that he's moving in one direction and looking in another. Additionally, he's still running into things despite the danger being calculated. I've replaced the color-changing sticks I was using to debug the raycasts with sphere meshes that are supposed to be placed at the collision point of each raycast and change color based on the danger value, but they're only doing that at certain angles, whereas the ones that are supposed to be positioned at the collision point of other raycasts are just clipping through walls instead. Any ideas?

Updated code:

extends CharacterBody3D

var default_speed = 95
var first_nav_point = true
var current_nav_index = 0
var next_path_point = Vector3.ZERO
var physics_frame_counter: int = 0
var raycast_visualization = true

var number_of_directions = 16
var movement_directions = []
var interest = []
var danger = []
var raycast_visualizers = []
var chosen_direction = Vector2.ZERO

@onready var last_global_position = global_position + Vector3(1,1,1)
@onready var player = get_node(GameVariables.player_path)
@onready var animation_tree = $Model.get_node("AnimationTree")
@onready var animation_state = $Model.get_node("AnimationTree").get("parameters/playback")
@onready var animation = $Model.get_node("AnimationPlayer")
@onready var vision_cone = $ShapeCast3D
@onready var model = $Model
@onready var mesh = $Model.get_node("Armature/Skeleton3D/Cube")
@onready var navigation_agent = $NavigationAgent3D
@onready var nav_points = [get_node("../NavPoint1"), get_node("../NavPoint2"), get_node("../NavPoint3"), get_node("../NavPoint4")]
@onready var nearest_nav_point = nav_points[0]
@onready var next_nav_point = nearest_nav_point
@onready var raycast_visualizer_scene = load("res://assets/enemies/raycast_visualizer.tscn")

func _ready():
	movement_directions.resize(number_of_directions)
	interest.resize(number_of_directions)
	danger.resize(number_of_directions)
	raycast_visualizers.resize(number_of_directions)
	
	for direction in number_of_directions:
		var angle = direction * 2 * PI / number_of_directions
		movement_directions[direction] = Vector2.DOWN.rotated(angle)
		
		if raycast_visualization:
			raycast_visualizers[direction] = raycast_visualizer_scene.instantiate()
			add_child(raycast_visualizers[direction])
			raycast_visualizers[direction].global_position = global_position
			raycast_visualizers[direction].visible = true
			var raycast_visualizer_mesh = raycast_visualizers[direction].get_node("MeshInstance3D")
			var unique_raycast_visualizer_mesh = raycast_visualizer_mesh.mesh.duplicate()
			var unique_raycast_visualizer_mat = raycast_visualizer_mesh.mesh.get("material").duplicate()
			raycast_visualizer_mesh.set("mesh", unique_raycast_visualizer_mesh)
			raycast_visualizer_mesh.mesh.set("material", unique_raycast_visualizer_mat)
	
	
	set_physics_process(false)
	call_deferred("await_physics") 
	
	
	


func _physics_process(delta):
	if navigation_agent.is_navigation_finished():
		return
	set_interest()
	set_danger()
	var direction = choose_direction()
	velocity = direction * default_speed * delta
	move_and_slide()
	look_at(direction)


func await_physics():
	if physics_frame_counter < 2:
		await get_tree().physics_frame
		physics_frame_counter += 1
		await_physics()
	else:
		set_physics_process(true)


func set_interest():
	var next_path_position = to_local(navigation_agent.get_next_path_position())
	var path_direction = Vector2(next_path_position.x, next_path_position.z).normalized()
	for direction in number_of_directions:
		var dot = movement_directions[direction].rotated(rotation.y).dot(path_direction)
		interest[direction] = max(dot, 0)


func set_danger():
	var space_state = get_world_3d().direct_space_state
	var danger_raycast_length = 2.5
	var danger_raycast_offset = Vector3(0, 0.5, 0)
	for direction in number_of_directions:
		#direction_3d is LOCAL
		var direction_3d: Vector3
		direction_3d.x = movement_directions[direction].rotated(rotation.y).x
		direction_3d.y = 0 
		direction_3d.z = movement_directions[direction].rotated(rotation.y).y
		
		var cast_from = danger_raycast_offset
		var cast_to = direction_3d * danger_raycast_length
		cast_to.y = danger_raycast_offset.y
		var danger_raycast = PhysicsRayQueryParameters3D.create(to_global(cast_from), to_global(cast_to), collision_mask, [self])
		danger_raycast.hit_from_inside = true
		var danger_raycast_result = space_state.intersect_ray(danger_raycast)
		if not danger_raycast_result.has("position"):
			danger[direction] = 0.0
		else:
			var collision_distance = cast_from.distance_to(to_local(danger_raycast_result.position))
			var formatted_distance = remap(-collision_distance, -danger_raycast_length, 0.0, 0.0, 1.0)
			danger[direction] = float((int(formatted_distance * 1000) ^ 2)) / 1000
			danger[direction] = clamp(danger[direction], 0.0, 1.0)
		
		if raycast_visualization:
			if danger_raycast_result.has("position"):
				raycast_visualizers[direction].position = to_local(danger_raycast_result.position - Vector3(0.1, 0, 0.1))
			else:
				raycast_visualizers[direction].position = cast_to
			raycast_visualizers[direction].get_node("MeshInstance3D").mesh.get("material").set("shader_parameter/blend", danger[direction])
			
			
func choose_direction():
	chosen_direction = Vector2.ZERO
	var chosen_direction_3d := Vector3.ZERO
	for direction in number_of_directions:
		interest[direction] -= danger[direction]
		interest[direction] = max(interest[direction], 0)
		
		chosen_direction += movement_directions[direction] * interest[direction]
		#print(interest[direction])
	chosen_direction = chosen_direction.normalized()
	chosen_direction_3d.x = -chosen_direction.x
	chosen_direction_3d.z = -chosen_direction.y
	#print(str("Interest: ", interest, ", Danger: ", danger))
	
	return chosen_direction_3d
	
	
func _on_NavigationAgent3D_target_reached():
	if current_nav_index + 1 <= nav_points.size() - 1:
		current_nav_index += 1
	else:
		current_nav_index = 0
	next_nav_point = nav_points[current_nav_index]