Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
使用 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 的父節點,以避開其他代理體或迴避障礙物。
備註
只有同一張地圖上啟用迴避註冊的其他代理體,才會納入迴避運算考量。
備註
The NavigationAgent must be supplied with a target_position attribute,
even if you are only using the agent for avoidance. Otherwise, the safe_velocity
received from the velocity_computed signal will always be the zero vector.
下列 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;
}
}