Randomly moving up, around and under a navigation mesh

Godot Version

Godot_v4.3-stable_win64

Question

How can I make a CharacterBody3D move along multiple targets randomly?

I have a game taking place in a swamp and I want to be able to move the alligator NPC which consists of a CharacterBody3D around multiple targets represented by Marker3D nodes. I was following this tutorial which showed navigation with one target :

How could I rework my script for him to move to each of these targets, some of the targets are under the water to make it look like he’s diving and disappearing for a while and coming back up from the water?

extends CharacterBody3D

#Node references
@onready var navigation_agent_3D: NavigationAgent3D = $NavigationAgent3D
@onready var AP = $Billie3/AnimationPlayer
@onready var vis = $VisibleOnScreenEnabler3D

@export var speed = 8
@export var accel = 10

@onready var York : CharacterBody3D = get_tree().get_first_node_in_group("York")

#Statuses
var moving = true
var idle = false

# Called when the node enters the scene tree for the first time.
func _ready():
	_choose_action()
	

#Billie chooses whether to Move or Stay Idle
func _choose_action():
	print("Billie is thinking about what to do next!")
	var func_array: Array[Callable] = [_Billie_move, _Billie_idle ]
	func_array.pick_random().call()
	$ActionTimer.start()

func _on_action_timer_timeout():
	_choose_action()

#Swim and move along navigation mesh
func _Billie_move():
		print("Billie is moving!")
		var SwimAnims = ["Floating", "Diving"]
		AP.play(SwimAnims.pick_random())
		moving = true
		var random_pos = Vector3.ZERO
		random_pos.x = randf_range(-5.0, 5.0)
		random_pos.y = randf_range(-5.0, 5.0)
		navigation_agent_3D.set_target_position(random_pos)
		#Insert Sounds here
		#$Noises.play()
		
		if idle:
			_Billie_idle()
			
		

func _on_navigation_agent_3d_target_reached():
	_choose_action()


func _physics_process(delta: float) -> void:
	var destination = navigation_agent_3D.get_next_path_position()
	var local_destination = destination - global_position
	var direction = local_destination.normalized()
	
	
	velocity = direction * 20
	move_and_slide()

#Stop moving along navigation mesh and play idle animations until next action
func _Billie_idle():
		print("Billie is Idle!")
		moving = false
		idle = true
		navigation_agent_3D.max_speed = 0
		var IdleAnims = ["WavingArms", "WavingHand"]
		AP.play(IdleAnims.pick_random())
		#Insert SFX Here
		#$Noises.play()

#Visibility on Screen from York's POV
func _on_visible_on_screen_enabler_3d_screen_entered():
	print("York can see Billie!")
	York.happy_chat()

#When not Visible from York's POV
func _on_visible_on_screen_enabler_3d_screen_exited():
	print("York can't see Billie!")
	York.upset_chat()

The main level with the 7 targets marked by Marker3D nodes, some above and underwater :

Could I put my Target Positions in an array and have him randomly select one to go towards?

Navigation mesh attached to the water :
image

Yes :grin:

In your move function, you can select the target randomly and pass it to the navigation agent.

1 Like

I added my targets as an array onto my variables section up top, and tried implementing some of the code I learned from the video but I keep getting an error (below this script)

extends CharacterBody3D

#Node references
@onready var target_array = [$"../T1", $"../T2", $"../T3", $"../T4", $"../T5", $"../T6", $"../T7"]
@onready var navigation_agent_3D: NavigationAgent3D = $NavigationAgent3D
@onready var AP = $Billie3/AnimationPlayer
@onready var vis = $VisibleOnScreenEnabler3D

@export var speed = 8
@export var accel = 10

@onready var York : CharacterBody3D = get_tree().get_first_node_in_group("York")

#Statuses
var moving = true
var idle = false

# Called when the node enters the scene tree for the first time.
func _ready():
	_choose_action()
	

#Billie chooses whether to Move or Stay Idle
func _choose_action():
	print("Billie is thinking about what to do next!")
	var func_array: Array[Callable] = [_Billie_move, _Billie_idle ]
	func_array.pick_random().call()
	$ActionTimer.start()

func _on_action_timer_timeout():
	_choose_action()
	print("ActionTimer...OUT")

#Swim and move along navigation mesh
func _Billie_move():
		print("Billie is moving!")
		var SwimAnims = ["Floating", "Diving"]
		AP.play(SwimAnims.pick_random())
		moving = true
		
		var direction = Vector3()
	
		navigation_agent_3D.target_position = target_array.pick_random()
	
		direction = navigation_agent_3D.get_next_path_position() - global_position
		direction = direction.normalized()
	
		move_and_slide()
		
		#Insert Sounds here
		#$Noises.play()
		
		if idle:
			_Billie_idle()
			
		

func _on_navigation_agent_3d_target_reached():
	_choose_action()


func _physics_process(delta: float) -> void:
	pass



#Stop moving along navigation mesh and play idle animations until next action
func _Billie_idle():
		print("Billie is Idle!")
		moving = false
		idle = true
		navigation_agent_3D.max_speed = 0
		var IdleAnims = ["WavingArms", "WavingHand"]
		AP.play(IdleAnims.pick_random())
		#Insert SFX Here
		#$Noises.play()

#Visibility on Screen from York's POV
func _on_visible_on_screen_enabler_3d_screen_entered():
	print("York can see Billie!")
	York.happy_chat()

#When not Visible from York's POV
func _on_visible_on_screen_enabler_3d_screen_exited():
	print("York can't see Billie!")
	York.upset_chat()

Error when ‘Billie’'s move function starts :

E 0:00:47:0609   billie.gd:43 @ _Billie_move(): Condition "err != VK_SUCCESS" is true. Returning: ERR_CANT_CREATE
  <C++ Source>   drivers/vulkan/rendering_device_driver_vulkan.cpp:2606 @ swap_chain_resize()
  <Stack Trace>  billie.gd:43 @ _Billie_move()
                 billie.gd:27 @ _choose_action()
                 billie.gd:20 @ _ready()

I think I’m a little lost on how to get him to move, I’ve been using this as a basis from the Youtube tutorial :

extends CharacterBody3D

#Node references
@onready var navigation_agent_3D: NavigationAgent3D = $NavigationAgent3D
@onready var AP = $Billie3/AnimationPlayer
@onready var vis = $VisibleOnScreenEnabler3D

@export var speed = 8
@export var accel = 10

@onready var York : CharacterBody3D = get_tree().get_first_node_in_group("York")

#Statuses
var moving = true
var idle = false

# Called when the node enters the scene tree for the first time.
func _ready():
	_choose_action()
	

#Billie chooses whether to Move or Stay Idle
func _choose_action():
	print("Billie is thinking about what to do next!")
	var func_array: Array[Callable] = [_Billie_move, _Billie_idle ]
	func_array.pick_random().call()
	$ActionTimer.start()

func _on_action_timer_timeout():
	_choose_action()

#Swim and move along navigation mesh
func _Billie_move():
		print("Billie is moving!")
		var SwimAnims = ["Floating", "Diving"]
		AP.play(SwimAnims.pick_random())
		moving = true
		var random_pos = [$"../../T1",$"../../T2",$"../../T3",$"../../T4",$"../../T5",$"../../T6",$"../../T7"]
		random_pos.x = randf_range(-5.0, 5.0)
		random_pos.y = randf_range(-5.0, 5.0)
		navigation_agent_3D.set_target_position(random_pos.pick_random())
		#Insert Sounds here
		#$Noises.play()
		
		if idle:
			_Billie_idle()
			
		

func _on_navigation_agent_3d_target_reached():
	_choose_action()


func _physics_process(delta: float) -> void:
	var destination = navigation_agent_3D.get_next_path_position()
	var local_destination = destination - global_position
	var direction = local_destination.normalized()
	
	
	velocity = direction * 20
	move_and_slide()

#Stop moving along navigation mesh and play idle animations until next action
func _Billie_idle():
		print("Billie is Idle!")
		moving = false
		idle = true
		navigation_agent_3D.max_speed = 0
		var IdleAnims = ["WavingArms", "WavingHand"]
		AP.play(IdleAnims.pick_random())
		#Insert SFX Here
		#$Noises.play()

#Visibility on Screen from York's POV
func _on_visible_on_screen_enabler_3d_screen_entered():
	print("York can see Billie!")
	York.happy_chat()

#When not Visible from York's POV
func _on_visible_on_screen_enabler_3d_screen_exited():
	print("York can't see Billie!")
	York.upset_chat()

I tried changing my random_pos value to the array but when the move function tries to fire it crashes and gives me this error :

Invalid assignment of property or key 'text' with value of type 'String' on a base object of type 'Nil'.

In this version of the script he doesn’t move but the game doesn’t crash at least.

extends CharacterBody3D

#Node references
@onready var target_array = [$"../../T1",$"../../T2", $"../../T3", $"../../T4", $"../../T5", $"../../T6",$"../../T7" ]
@onready var navigation_agent_3D: NavigationAgent3D = $NavigationAgent3D
@onready var AP = $Billie3/AnimationPlayer
@onready var vis = $VisibleOnScreenEnabler3D

@export var speed = 8
@export var accel = 10

@onready var York : CharacterBody3D = get_tree().get_first_node_in_group("York")

#Statuses
var moving = true
var idle = false

# Called when the node enters the scene tree for the first time.
func _ready():
	_choose_action()
	

#Billie chooses whether to Move or Stay Idle
func _choose_action():
	print("Billie is thinking about what to do next!")
	var func_array: Array[Callable] = [_Billie_move, _Billie_idle ]
	func_array.pick_random().call()
	$ActionTimer.start()

func _on_action_timer_timeout():
	_choose_action()
	print("ActionTimer...OUT")

#Swim and move along navigation mesh
func _Billie_move():
		print("Billie is moving!")
		var SwimAnims = ["Floating", "Diving"]
		AP.play(SwimAnims.pick_random())
		moving = true
		
		var random_pos = Vector3.ZERO
		random_pos.x = randf_range(-5.0, 5.0)
		random_pos.y = randf_range(-5.0, 5.0)
		random_pos.z = randf_range(-5.0, 5.0)
		navigation_agent_3D.set_target_position(random_pos)
		
		get_tree().call_group("Billie", "_update_target_location", target_array.pick_random().global_transform)
		
		#Insert Sounds here
		#$Noises.play()
		
		if idle:
			_Billie_idle()
			
		

func _update_target_location():
	navigation_agent_3D.set_target_location(target_array.pick_random())


func _on_navigation_agent_3d_target_reached():
	_choose_action()


func _physics_process(delta: float) -> void:
	var current_location = global_transform.origin
	var next_location = navigation_agent_3D.get_next_path_position()
	var new_velocity = (next_location - current_location).normalized() * speed
	
	velocity = new_velocity
	
	move_and_slide()



#Stop moving along navigation mesh and play idle animations until next action
func _Billie_idle():
		print("Billie is Idle!")
		moving = false
		idle = true
		navigation_agent_3D.max_speed = 0
		var IdleAnims = ["WavingArms", "WavingHand"]
		AP.play(IdleAnims.pick_random())
		#Insert SFX Here
		#$Noises.play()

#Visibility on Screen from York's POV
func _on_visible_on_screen_enabler_3d_screen_entered():
	print("York can see Billie!")
	York.happy_chat()

#When not Visible from York's POV
func _on_visible_on_screen_enabler_3d_screen_exited():
	print("York can't see Billie!")
	York.upset_chat()

You had a couple of issues but you solved most of them :+1:

Now what you have to do in your script is (see the comments that I added):

func _Billie_move():
		print("Billie is moving!")
		var SwimAnims = ["Floating", "Diving"]
		AP.play(SwimAnims.pick_random())
		moving = true
		
		# Choose a random target from your Marker3D array.
		var random_target = target_array.pick_random()

		# Get the position of the target.
		var random_pos = random_target.global_position

		# Now navigate to that postion.
		navigation_agent_3D.set_target_position(random_pos)
		
		get_tree().call_group("Billie", "_update_target_location", target_array.pick_random().global_transform)
		
		#Insert Sounds here
		#$Noises.play()
		
		if idle:
			_Billie_idle()
1 Like

Replaced the piece of my script in the move function with yours, and thankfully it doesn’t crash but he’s just stuck in place, even when the move function is active.

Move function firing in console :

Current Script :

extends CharacterBody3D

#Node references
@onready var target_array = [$"../../T1",$"../../T2", $"../../T3", $"../../T4", $"../../T5", $"../../T6",$"../../T7" ]
@onready var navigation_agent_3D: NavigationAgent3D = $NavigationAgent3D
@onready var AP = $Billie3/AnimationPlayer
@onready var vis = $VisibleOnScreenEnabler3D

@export var speed = 8
@export var accel = 10

@onready var York : CharacterBody3D = get_tree().get_first_node_in_group("York")

#Statuses
var moving = true
var idle = false

# Called when the node enters the scene tree for the first time.
func _ready():
	_choose_action()
	

#Billie chooses whether to Move or Stay Idle
func _choose_action():
	print("Billie is thinking about what to do next!")
	var func_array: Array[Callable] = [_Billie_move, _Billie_idle ]
	func_array.pick_random().call()
	$ActionTimer.start()

func _on_action_timer_timeout():
	_choose_action()
	print("ActionTimer...OUT")

#Swim and move along navigation mesh
func _Billie_move():
		print("Billie is moving!")
		var SwimAnims = ["Floating", "Diving"]
		AP.play(SwimAnims.pick_random())
		moving = true
		
		#Choose a random Target from the Target Array 
		var random_target = target_array.pick_random()
		
		#Get the position of the target 
		var random_pos = random_target.global_position
		
		#Now Navigate to that position ]
		get_tree().call_group("Billie", "update_target_location", target_array.pick_random().global_transform)
		
		#Insert Sounds here
		#$Noises.play()
		
		if idle:
			_Billie_idle()
			
		

func _update_target_location():
	navigation_agent_3D.set_target_location(target_array.pick_random())


func _on_navigation_agent_3d_target_reached():
	_choose_action()


func _physics_process(delta: float) -> void:
	var destination = navigation_agent_3D.get_next_path_position()
	var local_destination = destination - global_position
	var direction = local_destination.normalized()
	
	velocity = direction * 5.0
	move_and_slide()



#Stop moving along navigation mesh and play idle animations until next action
func _Billie_idle():
		print("Billie is Idle!")
		moving = false
		idle = true
		navigation_agent_3D.max_speed = 0
		var IdleAnims = ["WavingArms", "WavingHand"]
		AP.play(IdleAnims.pick_random())
		#Insert SFX Here
		#$Noises.play()

#Visibility on Screen from York's POV
func _on_visible_on_screen_enabler_3d_screen_entered():
	print("York can see Billie!")
	York.happy_chat()

#When not Visible from York's POV
func _on_visible_on_screen_enabler_3d_screen_exited():
	print("York can't see Billie!")
	York.upset_chat()

Billie is animated but not physically moving positions :

I have navigation paths visible in debug and unfortunately not seeing any paths.

Uh, did not read your code close enough. You have some more things going on there, that you have to clean up. E.g. you are the navigation agents target position in multiple ways and also choose different target locations while you do so.

It may also be related to your state handling, maybe the idle and move variables are set up in a way that they block actual movement. I recommend to set some break points and use the debugger to go through your code step by step and see what is happening. Maybe calling the idle function in the move function causes some issues.

I’m also not sure if you have set up the navigation correctly, e.g. if you have the navmesh created correctly. See 3D navigation overview — Godot Engine (stable) documentation in English for reference.

On the other hand: If I interpret your first screenshot correctly, Billie should simply move on a square area? If yes, you don’t necessarily need navmesh navigation and you could simply move in a straight line:

func _Billie_move():
		print("Billie is moving!")
		var SwimAnims = ["Floating", "Diving"]
		AP.play(SwimAnims.pick_random())
		moving = true

		var random_target = target_array.pick_random()
		var target_vector = random_target.global_position - global_position
		
		velocity = target_vector.normalized() * 5

func _physics_process(delta):
		move_and_slide()

Sorry, I cannot try this right now, so you have to fix any typos or mistakes that I did by yourself :slight_smile:

1 Like

Thank you so much for your help, I didn’t even realize I could go along a line curve path until this!
Instead of the navigation agent I have a PathFollow 3D node now and just used those instead of the markers and navigationagents. So much more simple this way. Billie only needs an animation handling and its score functions script now and the pathfollow does it’s own thing.

Just for reference I used this tutorial :

And the only script is attached to a Path3D Node :

extends PathFollow3D

func _physics_process(delta):
	progress += 200 * delta