使用 NavigationAgent
NavigationAgent 是協助節點,結合了路徑尋找、路徑跟隨與代理體迴避等功能,可加掛於 Node2D/Node3D 類型的父節點。它們以更直觀的方式,協助父角色節點呼叫 NavigationServer API,對初學者特別友善。
NavigationAgent 有 2D 與 3D 版本,分別為 NavigationAgent2D 與 NavigationAgent3D。
新的 NavigationAgent 節點會自動加入 World2D/World3D 的預設導航地圖。
NavigationAgent 節點為選用元件,並非使用導航系統的必要條件。其所有功能皆可透過腳本直接呼叫 NavigationServer API 實作。
小訣竅
若需更進階的用法,請考慮使用 使用 NavigationPathQueryObject,而非僅使用 NavigationAgent 節點。
NavigationAgent 尋路
當 NavigationAgent 的 target_position 設定為全域座標時,會在目前的導航地圖上查詢新的路徑。
尋路結果可由下列屬性影響。
navigation_layers位元遮罩可用來限制代理體可使用的導航網格。pathfinding_algorithm控制尋路演算法在導航網格多邊形中的搜尋方式。path_postprocessing設定是否、以及如何對尋路找到的原始路徑走廊進行後處理。path_metadata_flags允許收集路徑點額外的元資料。simplify_path與simplify_epsilon屬性可用來簡化路徑,移除不重要的路徑節點。
警告
如果關閉路徑元資料旗標,代理體將不會發出相關訊號。
NavigationAgent 路徑跟隨
當代理體設定了 target_position 後,可透過 get_next_path_position() 取得路徑上的下一個要跟隨的位置。
取得下一個路徑位置後,請用你自訂的移動程式,讓代理的父層角色節點朝該位置移動。
備註
導航系統不會自動移動 NavigationAgent 的父節點。移動完全由使用者自訂腳本控制。
NavigationAgent 內部會處理自身的路徑狀態與更新流程。
get_next_path_position() 函式會更新代理體多項內部狀態與屬性。此函式應於每次 _physics_process 只呼叫一次,直到 is_navigation_finished() 回報路徑已完成。若已抵達目標點或路徑終點,請勿再呼叫本函式,否則代理體會因持續更新路徑而在原地抖動。建議在腳本中儘早以 is_navigation_finished() 檢查路徑是否已結束。
以下距離屬性會影響路徑跟隨行為。
當代理體與下一個路徑點的距離小於
path_desired_distance時,代理體會將內部路徑索引推進到下個路徑點。當代理體與目標路徑點的距離小於
target_desired_distance時,代理體會判定已抵達目標點並結束路徑。當代理體偏離理想路徑到下一個路徑點的距離大於
path_max_distance時,會重新請求尋路,因為代理體已經太遠離原路徑。
上述重要更新皆會於 _physics_process() 內呼叫 get_next_path_position() 時執行。
NavigationAgent 雖可搭配 process,但每次僅會於 _physics_process 進行一次更新。
下方將提供常與 NavigationAgent 搭配使用的各類節點之腳本範例。
路徑跟隨常見問題
編寫代理體移動腳本時,需注意下列常見問題與重要事項。
- 取得的路徑為空
如果在導航地圖尚未同步前(如於
_ready()中)查詢路徑,可能會取得空路徑。這時get_next_path_position()會回傳與代理體父節點相同的位置,並視為已到達終點。解決方式為延遲呼叫,或等待導航地圖變更訊號後再查詢路徑。
- 代理體卡在兩個位置來回抖動
這通常是因為每幀都在重新計算路徑(例如最大路徑距離設定過短)所致。尋路會尋找離代理體最近且有效的導航網格位置,如果每一幀都重新尋路,第一個路徑點可能會在代理體當前位置前後不斷切換,導致角色來回抖動。
- 代理體有時會倒退回頭
若代理體速度過快,可能會直接超過
path_desired_distance而無法推進路徑索引,導致代理體回頭走回上一個路徑點,直到通過距離判斷才往下個路徑點前進。請依代理體速度與更新頻率適當調整距離設定,並優化導航網格多邊形佈局(避免小範圍內擠太多邊)以改善此問題。
- 代理體有時會瞬間朝反方向旋轉
和上面代理體在兩點間抖動的情形一樣,這也多半是每幀過度更新路徑造成。根據導航網格佈局,尤其當代理體剛好位於網格邊緣或連接點時,路徑點有時可能會落在角色當下朝向的後方,此為精度誤差所致,不一定能完全避免。通常只有在角色瞬間朝向目前路徑點時才會明顯出現該現象。
NavigationAgent 迴避
本節介紹如何使用 NavigationAgent 的專屬導航迴避功能。
要讓 NavigationAgent 使用避障功能,必須將 avoidance_enabled 屬性設為 true。
必須連接 NavigationAgent 節點的 velocity_computed 訊號,才能取得安全速度運算結果。
請於 _physics_process() 內設定 NavigationAgent 節點的 velocity,以更新代理體的當前速度(通常為父節點的速度)。
當代理體啟用迴避後,每個物理更新時會收到 velocity_computed 訊號與 safe_velocity 向量。請使用此向量移動 NavigationAgent 的父節點,以避開其他代理體或迴避障礙物。
備註
只有同一張地圖上啟用迴避註冊的其他代理體,才會納入迴避運算考量。
下列 NavigationAgent 屬性與迴避有關:
height屬性僅供 3D 使用。高度結合代理體目前全域 y 軸位置,決定代理體在迴避模擬中的垂直層級。使用 2D 迴避時,會自動忽略高於或低於自身的其他代理體或障礙。
radius屬性設定代理體迴避圓的半徑(3D 為球體),代表代理體本身的碰撞體積,而非迴避動作的距離。
neighbor_distance屬性設定代理體搜尋其他需迴避代理的半徑。數值越低,運算成本越小。
max_neighbors屬性控制迴避運算時最多考慮多少個重疊半徑的代理體。設太低雖可減少運算,但可能導致部分代理體忽略迴避。
time_horizon_agents及time_horizon_obstacles屬性設定預測其他代理體/障礙物的避障時間(秒)。代理體會選擇在此時間內不會碰撞的安全速度。預測時間建議盡量設低,否則代理體會提前減速以避免碰撞。
max_speed屬性決定代理體避障運算時允許的最大速度。若父節點速度大於此值,safe_velocity可能不足以避免碰撞。
use_3d_avoidance屬性可切換代理體在下次更新時使用 2D(XZ 軸)或 3D(XYZ 軸)避障。2D 與 3D 避障彼此獨立,互不影響。
avoidance_layers與avoidance_mask屬性為位元遮罩,類似物理層。代理體僅會避開與自身遮罩有交集的迴避層物件。
avoidance_priority屬性讓高優先級代理體可忽略低優先級代理體。可用於讓特定代理體(如重要 NPC)在避障計算中獲得更高權重,而無需更動其整體遮罩或層級。
避障運算是在獨立空間進行,無法直接取得導航網格或物理碰撞資訊。實際上每個代理體在避障系統中是平面圓形(2D)或球體(3D)。若需考慮環境限制,可使用 NavigationObstacles,詳見 使用 NavigationObstacle。
備註
避障功能不會影響路徑尋找。它適合用於那些無法(重新)烘焙到導航網格上的持續移動物件,以便讓代理體能繞行。
備註
RVO 避障法預設代理體會有自然的避讓行為,例如雙方相遇時會自動分流至左右。因此極端測試情境常會失敗,例如代理體完全正面對衝且速度相反,會因無法分配避讓方向而失效。
建議以 NavigationAgent 的 avoidance_enabled 屬性來切換避障。以下程式碼片段可用於切換代理的避障、建立或刪除避障回呼,或切換避障模式。
extends NavigationAgent2D
func _ready() -> void:
var agent: RID = get_rid()
# Enable avoidance
NavigationServer2D.agent_set_avoidance_enabled(agent, true)
# Create avoidance callback
NavigationServer2D.agent_set_avoidance_callback(agent, Callable(self, "_avoidance_done"))
# Disable avoidance
NavigationServer2D.agent_set_avoidance_enabled(agent, false)
# Delete avoidance callback
NavigationServer2D.agent_set_avoidance_callback(agent, Callable())
using Godot;
public partial class MyNavigationAgent2D : NavigationAgent2D
{
public override void _Ready()
{
Rid agent = GetRid();
// Enable avoidance
NavigationServer2D.AgentSetAvoidanceEnabled(agent, true);
// Create avoidance callback
NavigationServer2D.AgentSetAvoidanceCallback(agent, Callable.From(AvoidanceDone));
// Disable avoidance
NavigationServer2D.AgentSetAvoidanceEnabled(agent, false);
//Delete avoidance callback
NavigationServer2D.AgentSetAvoidanceCallback(agent, default);
}
private void AvoidanceDone() { }
}
extends NavigationAgent3D
func _ready() -> void:
var agent: RID = get_rid()
# Enable avoidance
NavigationServer3D.agent_set_avoidance_enabled(agent, true)
# Create avoidance callback
NavigationServer3D.agent_set_avoidance_callback(agent, Callable(self, "_avoidance_done"))
# Switch to 3D avoidance
NavigationServer3D.agent_set_use_3d_avoidance(agent, true)
# Disable avoidance
NavigationServer3D.agent_set_avoidance_enabled(agent, false)
# Delete avoidance callback
NavigationServer3D.agent_set_avoidance_callback(agent, Callable())
# Switch to 2D avoidance
NavigationServer3D.agent_set_use_3d_avoidance(agent, false)
using Godot;
public partial class MyNavigationAgent3D : NavigationAgent3D
{
public override void _Ready()
{
Rid agent = GetRid();
// Enable avoidance
NavigationServer3D.AgentSetAvoidanceEnabled(agent, true);
// Create avoidance callback
NavigationServer3D.AgentSetAvoidanceCallback(agent, Callable.From(AvoidanceDone));
// Switch to 3D avoidance
NavigationServer3D.AgentSetUse3DAvoidance(agent, true);
// Disable avoidance
NavigationServer3D.AgentSetAvoidanceEnabled(agent, false);
//Delete avoidance callback
NavigationServer3D.AgentSetAvoidanceCallback(agent, default);
// Switch to 2D avoidance
NavigationServer3D.AgentSetUse3DAvoidance(agent, false);
}
private void AvoidanceDone() { }
}
NavigationAgent 腳本範例
下列章節提供常與 NavigationAgent 搭配的節點腳本範本。
extends Node2D
@export var movement_speed: float = 4.0
@onready var navigation_agent: NavigationAgent2D = get_node("NavigationAgent2D")
var movement_delta: float
func _ready() -> void:
navigation_agent.velocity_computed.connect(Callable(_on_velocity_computed))
func set_movement_target(movement_target: Vector2):
navigation_agent.set_target_position(movement_target)
func _physics_process(delta):
# Do not query when the map has never synchronized and is empty.
if NavigationServer2D.map_get_iteration_id(navigation_agent.get_navigation_map()) == 0:
return
if navigation_agent.is_navigation_finished():
return
movement_delta = movement_speed * delta
var next_path_position: Vector2 = navigation_agent.get_next_path_position()
var new_velocity: Vector2 = global_position.direction_to(next_path_position) * movement_delta
if navigation_agent.avoidance_enabled:
navigation_agent.set_velocity(new_velocity)
else:
_on_velocity_computed(new_velocity)
func _on_velocity_computed(safe_velocity: Vector2) -> void:
global_position = global_position.move_toward(global_position + safe_velocity, movement_delta)
extends CharacterBody2D
@export var movement_speed: float = 4.0
@onready var navigation_agent: NavigationAgent2D = get_node("NavigationAgent2D")
func _ready() -> void:
navigation_agent.velocity_computed.connect(Callable(_on_velocity_computed))
func set_movement_target(movement_target: Vector2):
navigation_agent.set_target_position(movement_target)
func _physics_process(delta):
# Do not query when the map has never synchronized and is empty.
if NavigationServer2D.map_get_iteration_id(navigation_agent.get_navigation_map()) == 0:
return
if navigation_agent.is_navigation_finished():
return
var next_path_position: Vector2 = navigation_agent.get_next_path_position()
var new_velocity: Vector2 = global_position.direction_to(next_path_position) * movement_speed
if navigation_agent.avoidance_enabled:
navigation_agent.set_velocity(new_velocity)
else:
_on_velocity_computed(new_velocity)
func _on_velocity_computed(safe_velocity: Vector2):
velocity = safe_velocity
move_and_slide()
extends RigidBody2D
@export var movement_speed: float = 4.0
@onready var navigation_agent: NavigationAgent2D = get_node("NavigationAgent2D")
func _ready() -> void:
navigation_agent.velocity_computed.connect(Callable(_on_velocity_computed))
func set_movement_target(movement_target: Vector2):
navigation_agent.set_target_position(movement_target)
func _physics_process(delta):
# Do not query when the map has never synchronized and is empty.
if NavigationServer2D.map_get_iteration_id(navigation_agent.get_navigation_map()) == 0:
return
if navigation_agent.is_navigation_finished():
return
var next_path_position: Vector2 = navigation_agent.get_next_path_position()
var new_velocity: Vector2 = global_position.direction_to(next_path_position) * movement_speed
if navigation_agent.avoidance_enabled:
navigation_agent.set_velocity(new_velocity)
else:
_on_velocity_computed(new_velocity)
func _on_velocity_computed(safe_velocity: Vector2):
linear_velocity = safe_velocity
using Godot;
public partial class MyNode2D : Node2D
{
[Export]
public float MovementSpeed { get; set; } = 4.0f;
NavigationAgent2D _navigationAgent;
private float _movementDelta;
public override void _Ready()
{
_navigationAgent = GetNode<NavigationAgent2D>("NavigationAgent2D");
_navigationAgent.VelocityComputed += OnVelocityComputed;
}
private void SetMovementTarget(Vector2 movementTarget)
{
_navigationAgent.TargetPosition = movementTarget;
}
public override void _PhysicsProcess(double delta)
{
// Do not query when the map has never synchronized and is empty.
if (NavigationServer2D.MapGetIterationId(_navigationAgent.GetNavigationMap()) == 0)
{
return;
}
if (_navigationAgent.IsNavigationFinished())
{
return;
}
_movementDelta = MovementSpeed * (float)delta;
Vector2 nextPathPosition = _navigationAgent.GetNextPathPosition();
Vector2 newVelocity = GlobalPosition.DirectionTo(nextPathPosition) * _movementDelta;
if (_navigationAgent.AvoidanceEnabled)
{
_navigationAgent.Velocity = newVelocity;
}
else
{
OnVelocityComputed(newVelocity);
}
}
private void OnVelocityComputed(Vector2 safeVelocity)
{
GlobalPosition = GlobalPosition.MoveToward(GlobalPosition + safeVelocity, _movementDelta);
}
}
using Godot;
public partial class MyCharacterBody2D : CharacterBody2D
{
[Export]
public float MovementSpeed { get; set; } = 4.0f;
NavigationAgent2D _navigationAgent;
public override void _Ready()
{
_navigationAgent = GetNode<NavigationAgent2D>("NavigationAgent2D");
_navigationAgent.VelocityComputed += OnVelocityComputed;
}
private void SetMovementTarget(Vector2 movementTarget)
{
_navigationAgent.TargetPosition = movementTarget;
}
public override void _PhysicsProcess(double delta)
{
// Do not query when the map has never synchronized and is empty.
if (NavigationServer2D.MapGetIterationId(_navigationAgent.GetNavigationMap()) == 0)
{
return;
}
if (_navigationAgent.IsNavigationFinished())
{
return;
}
Vector2 nextPathPosition = _navigationAgent.GetNextPathPosition();
Vector2 newVelocity = GlobalPosition.DirectionTo(nextPathPosition) * MovementSpeed;
if (_navigationAgent.AvoidanceEnabled)
{
_navigationAgent.Velocity = newVelocity;
}
else
{
OnVelocityComputed(newVelocity);
}
}
private void OnVelocityComputed(Vector2 safeVelocity)
{
Velocity = safeVelocity;
MoveAndSlide();
}
}
using Godot;
public partial class MyRigidBody2D : RigidBody2D
{
[Export]
public float MovementSpeed { get; set; } = 4.0f;
NavigationAgent2D _navigationAgent;
public override void _Ready()
{
_navigationAgent = GetNode<NavigationAgent2D>("NavigationAgent2D");
_navigationAgent.VelocityComputed += OnVelocityComputed;
}
private void SetMovementTarget(Vector2 movementTarget)
{
_navigationAgent.TargetPosition = movementTarget;
}
public override void _PhysicsProcess(double delta)
{
// Do not query when the map has never synchronized and is empty.
if (NavigationServer2D.MapGetIterationId(_navigationAgent.GetNavigationMap()) == 0)
{
return;
}
if (_navigationAgent.IsNavigationFinished())
{
return;
}
Vector2 nextPathPosition = _navigationAgent.GetNextPathPosition();
Vector2 newVelocity = GlobalPosition.DirectionTo(nextPathPosition) * MovementSpeed;
if (_navigationAgent.AvoidanceEnabled)
{
_navigationAgent.Velocity = newVelocity;
}
else
{
OnVelocityComputed(newVelocity);
}
}
private void OnVelocityComputed(Vector2 safeVelocity)
{
LinearVelocity = safeVelocity;
}
}
extends Node3D
@export var movement_speed: float = 4.0
@onready var navigation_agent: NavigationAgent3D = get_node("NavigationAgent3D")
var physics_delta: float
func _ready() -> void:
navigation_agent.velocity_computed.connect(Callable(_on_velocity_computed))
func set_movement_target(movement_target: Vector3):
navigation_agent.set_target_position(movement_target)
func _physics_process(delta):
# Save the delta for use in _on_velocity_computed.
physics_delta = delta
# Do not query when the map has never synchronized and is empty.
if NavigationServer3D.map_get_iteration_id(navigation_agent.get_navigation_map()) == 0:
return
if navigation_agent.is_navigation_finished():
return
var next_path_position: Vector3 = navigation_agent.get_next_path_position()
var new_velocity: Vector3 = global_position.direction_to(next_path_position) * movement_speed
if navigation_agent.avoidance_enabled:
navigation_agent.set_velocity(new_velocity)
else:
_on_velocity_computed(new_velocity)
func _on_velocity_computed(safe_velocity: Vector3) -> void:
global_position = global_position.move_toward(global_position + safe_velocity, physics_delta * movement_speed)
extends CharacterBody3D
@export var movement_speed: float = 4.0
@onready var navigation_agent: NavigationAgent3D = get_node("NavigationAgent3D")
func _ready() -> void:
navigation_agent.velocity_computed.connect(Callable(_on_velocity_computed))
func set_movement_target(movement_target: Vector3):
navigation_agent.set_target_position(movement_target)
func _physics_process(delta):
# Do not query when the map has never synchronized and is empty.
if NavigationServer3D.map_get_iteration_id(navigation_agent.get_navigation_map()) == 0:
return
if navigation_agent.is_navigation_finished():
return
var next_path_position: Vector3 = navigation_agent.get_next_path_position()
var new_velocity: Vector3 = global_position.direction_to(next_path_position) * movement_speed
if navigation_agent.avoidance_enabled:
navigation_agent.set_velocity(new_velocity)
else:
_on_velocity_computed(new_velocity)
func _on_velocity_computed(safe_velocity: Vector3):
velocity = safe_velocity
move_and_slide()
extends RigidBody3D
@export var movement_speed: float = 4.0
@onready var navigation_agent: NavigationAgent3D = get_node("NavigationAgent3D")
func _ready() -> void:
navigation_agent.velocity_computed.connect(Callable(_on_velocity_computed))
func set_movement_target(movement_target: Vector3):
navigation_agent.set_target_position(movement_target)
func _physics_process(delta):
# Do not query when the map has never synchronized and is empty.
if NavigationServer3D.map_get_iteration_id(navigation_agent.get_navigation_map()) == 0:
return
if navigation_agent.is_navigation_finished():
return
var next_path_position: Vector3 = navigation_agent.get_next_path_position()
var new_velocity: Vector3 = global_position.direction_to(next_path_position) * movement_speed
if navigation_agent.avoidance_enabled:
navigation_agent.set_velocity(new_velocity)
else:
_on_velocity_computed(new_velocity)
func _on_velocity_computed(safe_velocity: Vector3):
linear_velocity = safe_velocity
using Godot;
public partial class MyNode3D : Node3D
{
[Export]
public float MovementSpeed { get; set; } = 4.0f;
NavigationAgent3D _navigationAgent;
private float _movementDelta;
public override void _Ready()
{
_navigationAgent = GetNode<NavigationAgent3D>("NavigationAgent3D");
_navigationAgent.VelocityComputed += OnVelocityComputed;
}
private void SetMovementTarget(Vector3 movementTarget)
{
_navigationAgent.TargetPosition = movementTarget;
}
public override void _PhysicsProcess(double delta)
{
// Do not query when the map has never synchronized and is empty.
if (NavigationServer3D.MapGetIterationId(_navigationAgent.GetNavigationMap()) == 0)
{
return;
}
if (_navigationAgent.IsNavigationFinished())
{
return;
}
_movementDelta = MovementSpeed * (float)delta;
Vector3 nextPathPosition = _navigationAgent.GetNextPathPosition();
Vector3 newVelocity = GlobalPosition.DirectionTo(nextPathPosition) * _movementDelta;
if (_navigationAgent.AvoidanceEnabled)
{
_navigationAgent.Velocity = newVelocity;
}
else
{
OnVelocityComputed(newVelocity);
}
}
private void OnVelocityComputed(Vector3 safeVelocity)
{
GlobalPosition = GlobalPosition.MoveToward(GlobalPosition + safeVelocity, _movementDelta);
}
}
using Godot;
public partial class MyCharacterBody3D : CharacterBody3D
{
[Export]
public float MovementSpeed { get; set; } = 4.0f;
NavigationAgent3D _navigationAgent;
public override void _Ready()
{
_navigationAgent = GetNode<NavigationAgent3D>("NavigationAgent3D");
_navigationAgent.VelocityComputed += OnVelocityComputed;
}
private void SetMovementTarget(Vector3 movementTarget)
{
_navigationAgent.TargetPosition = movementTarget;
}
public override void _PhysicsProcess(double delta)
{
// Do not query when the map has never synchronized and is empty.
if (NavigationServer3D.MapGetIterationId(_navigationAgent.GetNavigationMap()) == 0)
{
return;
}
if (_navigationAgent.IsNavigationFinished())
{
return;
}
Vector3 nextPathPosition = _navigationAgent.GetNextPathPosition();
Vector3 newVelocity = GlobalPosition.DirectionTo(nextPathPosition) * MovementSpeed;
if (_navigationAgent.AvoidanceEnabled)
{
_navigationAgent.Velocity = newVelocity;
}
else
{
OnVelocityComputed(newVelocity);
}
}
private void OnVelocityComputed(Vector3 safeVelocity)
{
Velocity = safeVelocity;
MoveAndSlide();
}
}
using Godot;
public partial class MyRigidBody3D : RigidBody3D
{
[Export]
public float MovementSpeed { get; set; } = 4.0f;
NavigationAgent3D _navigationAgent;
public override void _Ready()
{
_navigationAgent = GetNode<NavigationAgent3D>("NavigationAgent3D");
_navigationAgent.VelocityComputed += OnVelocityComputed;
}
private void SetMovementTarget(Vector3 movementTarget)
{
_navigationAgent.TargetPosition = movementTarget;
}
public override void _PhysicsProcess(double delta)
{
// Do not query when the map has never synchronized and is empty.
if (NavigationServer3D.MapGetIterationId(_navigationAgent.GetNavigationMap()) == 0)
{
return;
}
if (_navigationAgent.IsNavigationFinished())
{
return;
}
Vector3 nextPathPosition = _navigationAgent.GetNextPathPosition();
Vector3 newVelocity = GlobalPosition.DirectionTo(nextPathPosition) * MovementSpeed;
if (_navigationAgent.AvoidanceEnabled)
{
_navigationAgent.Velocity = newVelocity;
}
else
{
OnVelocityComputed(newVelocity);
}
}
private void OnVelocityComputed(Vector3 safeVelocity)
{
LinearVelocity = safeVelocity;
}
}