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]