Godot Version
4.7.1
Question
I’m searching for advice to improve behavior of NPC to feel more organic and extendable.
I don’t have much experience with it and from tutorials and old projects I only worked with Raycast and RaycastShape, which works but makes it a bit too robotic.
The purpose of NPC is having routine around the map for going from “barrack” to “camp” , from there go to “treasure/bank” to exchange with other npc, other npc will do the same in reverse.
Current code
extends CharacterBody3D
@onready var navigation_agent_3d: NavigationAgent3D = $NavigationAgent3D
var gravity: float = 9.8
const SPEED = 5.0
var player
var spawn_positon : Vector3
func _ready() -> void:
player = get_tree().get_first_node_in_group("Player")
print(player)
spawn_positon = global_position
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y -= gravity * delta
if player_is_visible():
navigation_agent_3d.target_position = player.global_position
var next_position = navigation_agent_3d.get_next_path_position()
var direction = global_position.direction_to(next_position)
look_at_target(direction)
velocity.x = direction.x * SPEED
velocity.z = direction.z * SPEED
else:
velocity.x = move_toward(velocity.x, 0, SPEED)
velocity.z = move_toward(velocity.z, 0, SPEED)
move_and_slide()
# Main Logic with raycast from NPC to Player (no offsets for now)
func player_is_visible() -> bool:
if player == null:
return false
var space_state = get_world_3d().direct_space_state
var from = global_position
var to = player.global_position
var query = PhysicsRayQueryParameters3D.create(from, to)
query.exclude = [self]
var result = space_state.intersect_ray(query)
if result.is_empty():
return true
else:
return result.collider == player
func look_at_target(direction: Vector3) -> void:
var adjusted_direction = direction
adjusted_direction.y = 0
if adjusted_direction.length_squared() < 0.0001:
return
look_at(global_position + adjusted_direction, Vector3.UP, true)
It detects player and keep making raycast from npc to player and if there is no collider it follows player.
simple change of condition logic made a bit more challenging it for player
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y -= gravity * delta
if player_is_visible():
navigation_agent_3d.target_position = player.global_position
var next_position = navigation_agent_3d.get_next_path_position()
var direction = global_position.direction_to(next_position)
look_at_target(direction)
velocity.x = direction.x * SPEED
velocity.z = direction.z * SPEED
else:
var next_position = navigation_agent_3d.get_next_path_position()
var direction = global_position.direction_to(next_position)
look_at_target(direction)
velocity.x = direction.x * SPEED
velocity.z = direction.z * SPEED
move_and_slide()
ok this could be further improved as I figure out the physic frame on first frame haven’t collided with wall
extends CharacterBody3D
@onready var navigation_agent_3d: NavigationAgent3D = $NavigationAgent3D
var gravity: float = 9.8
const SPEED = 5.0
var player
var game_ready = false
func _ready() -> void:
player = get_tree().get_first_node_in_group("Player")
await get_tree().physics_frame
await get_tree().physics_frame
game_ready = true
func _physics_process(delta: float) -> void:
if game_ready == true:
if not is_on_floor():
velocity.y -= gravity * delta
if player_is_visible():
navigation_agent_3d.target_position = player.global_position
var next_position = navigation_agent_3d.get_next_path_position()
var direction = global_position.direction_to(next_position)
look_at_target(direction)
velocity.x = direction.x * SPEED
velocity.z = direction.z * SPEED
else:
var next_position = navigation_agent_3d.get_next_path_position()
var direction = global_position.direction_to(next_position)
look_at_target(direction)
velocity.x = direction.x * SPEED
velocity.z = direction.z * SPEED
move_and_slide()
else:
pass
# Main Logic with raycast from NPC to Player (no offsets for now)
func player_is_visible() -> bool:
if player == null:
return false
var space_state = get_world_3d().direct_space_state
var from = global_position
var to = player.global_position
var query = PhysicsRayQueryParameters3D.create(from, to)
query.exclude = [self]
var result = space_state.intersect_ray(query)
if result.is_empty():
return true
else:
return result.collider == player
func look_at_target(direction: Vector3) -> void:
var adjusted_direction = direction
adjusted_direction.y = 0
if adjusted_direction.length_squared() < 0.0001:
return
look_at(global_position + adjusted_direction, Vector3.UP, true)