使用 NavigationServer
NavigationServer 提供了 2D 和 3D 兩個版本,分別為 NavigationServer2D 與 NavigationServer3D。
2D 與 3D 實際上共用同一個 NavigationServer,其中以 NavigationServer3D 為主要伺服器。NavigationServer2D 是一個前端,它負責將 2D 位置轉換為 3D 位置再轉回來。因此,完全可以(只是有點麻煩)只使用 NavigationServer3D API 來實作 2D 導覽。
與 NavigationServer 溝通
要與 NavigationServer 互動,必須先準備好查詢(query)的參數,這些查詢可以發送給 NavigationServer 以進行更新或取得資料。
導覽伺服器內部的地圖、區域與代理等物件,都是用 RID 作為識別碼來引用。場景樹中每個與導覽相關的節點都提供一個可取得該節點 RID 的函式。
執行緒與同步
NavigationServer 並不會立即套用每一次的變更,而是等到**物理影格(physics frame)**結束時再一次同步所有的更動。
所有對地圖、區域與代理的變更都必須經過同步後才會生效。進行同步是因為某些更新(如重新計算整張導覽地圖)非常耗費資源,也需要其他物件的最新資料。此外,NavigationServer 某些功能(如代理之間的避障計算)預設會使用**執行緒池(threadpool)**來處理。
大多數僅用於取得資料、不會進行更動的 get() 函式,都不需要等待同步。但請注意,這些資料不一定會反映同一影格內做出的變更。例如,如果一個避障代理在這一影格修改了導覽地圖,那麼 agent_get_map() 在同步前仍會回傳舊地圖。例外情況是:有些節點會在送出更新給 NavigationServer 前,先在內部暫存資料,這種情況下,若在同一影格內用 getter 取得該值,就會拿到已被更新的內容。
NavigationServer 具備**執行緒安全性(thread-safe)**,因為所有會產生變更的 API 呼叫都會放進佇列,等同步階段時再一起執行。導覽伺服器的同步會在物理影格內、所有腳本與節點場景輸入都處理完之後進行。
備註
重點是:大多數對 NavigationServer 的變更都要等到下一個物理影格後才會生效,而不是馬上見效。這包含在場景樹導覽相關節點或腳本中做的所有變動。
備註
所有 setter 與刪除類型的函式都需要同步後才會生效。
2D 與 3D NavigationServer 的差異
NavigationServer2D 與 NavigationServer3D 在各自維度的功能是等價的,且底層都共用同一個 NavigationServer。
嚴格來說,NavigationServer2D 其實只是個前端介面。它的作用是把 Vector2(x, y) 和 Vector3(x, 0.0, z) 互相轉換,以便調用 NavigationServer3D 的 API。2D 的導覽路徑規劃其實是基於平面的 3D 網格,NavigationServer2D 只負責轉換座標。當說明文件只提 NavigationServer 而沒加 2D 或 3D 字尾時,通常表示你只需把 Vector2(x, y) 跟 Vector3(x, 0.0, z) 互換即可。
技術上來說,也可以用一個維度的導覽網格工具來建立另一個維度的網格,例如:用平面 3D 幾何體搭配 3D NavigationMesh 來烘焙 2D 導覽網格,或是用 NavigationRegion2D 與 NavigationPolygons 的多邊形繪製工具來產生平面 3D 導覽網格。
任何用 NavigationServer2D API 建立的 RID 也能直接用於 NavigationServer3D API,2D 與 3D 的避障代理也可以同時存在於同一張地圖上。
備註
在 2D 與 3D 中建立的區域,只要放到同一個地圖上,且符合合併條件時,它們的導覽網格就會被合併。NavigationServer 不會區分 NavigationRegion2D 與 NavigationRegion3D 節點,因為在伺服器端它們都是區域。預設情況下,這些節點會註冊到不同的導覽地圖,所以只有手動變更地圖(例如用腳本時)才會發生這種合併。
若 Actor 啟用避障功能,當其與 2D 和 3D 的避障代理同時存在於同一張地圖時,都會進行避障。
警告
在 Godot 自訂編譯時若停用 3D,將無法使用 NavigationServer2D。
等待同步
在遊戲剛開始、新場景載入,或是程式動態變更導覽時,對 NavigationServer 發出的任何路徑查詢,可能都會回傳空值或錯誤值。
這是因為導覽地圖此時尚未建構或尚未更新。場景樹中的所有節點都必須先把各自的導覽資料上傳到 NavigationServer,每個新增或變動的地圖、區域、代理都需要向伺服器註冊。完成後,NavigationServer 需要經過一個**物理影格**的同步流程,才能讓地圖、區域與代理的狀態正確更新。
一種解法是使用延遲呼叫(deferred call)來執行自訂初始化函式,確保所有節點都已經準備好。在這個設定函式裡進行所有導覽相關的變更(比如新增程式產生的內容),然後等待下一個物理影格,再進行路徑查詢。
extends Node3D
func _ready():
# Use call deferred to make sure the entire scene tree nodes are setup
# else await on 'physics_frame' in a _ready() might get stuck.
custom_setup.call_deferred()
func custom_setup():
# Create a new navigation map.
var map: RID = NavigationServer3D.map_create()
NavigationServer3D.map_set_up(map, Vector3.UP)
NavigationServer3D.map_set_active(map, true)
# Create a new navigation region and add it to the map.
var region: RID = NavigationServer3D.region_create()
NavigationServer3D.region_set_transform(region, Transform3D())
NavigationServer3D.region_set_map(region, map)
# Create a procedural navigation mesh for the region.
var new_navigation_mesh: NavigationMesh = NavigationMesh.new()
var vertices: PackedVector3Array = PackedVector3Array([
Vector3(0, 0, 0),
Vector3(9.0, 0, 0),
Vector3(0, 0, 9.0)
])
new_navigation_mesh.set_vertices(vertices)
var polygon: PackedInt32Array = PackedInt32Array([0, 1, 2])
new_navigation_mesh.add_polygon(polygon)
NavigationServer3D.region_set_navigation_mesh(region, new_navigation_mesh)
# Wait for NavigationServer sync to adapt to made changes.
await get_tree().physics_frame
# Query the path from the navigation server.
var start_position: Vector3 = Vector3(0.1, 0.0, 0.1)
var target_position: Vector3 = Vector3(1.0, 0.0, 1.0)
var optimize_path: bool = true
var path: PackedVector3Array = NavigationServer3D.map_get_path(
map,
start_position,
target_position,
optimize_path
)
print("Found a path!")
print(path)
using Godot;
public partial class MyNode3D : Node3D
{
public override void _Ready()
{
// Use call deferred to make sure the entire scene tree nodes are setup
// else await on 'physics_frame' in a _Ready() might get stuck.
CallDeferred(MethodName.CustomSetup);
}
private async void CustomSetup()
{
// Create a new navigation map.
Rid map = NavigationServer3D.MapCreate();
NavigationServer3D.MapSetUp(map, Vector3.Up);
NavigationServer3D.MapSetActive(map, true);
// Create a new navigation region and add it to the map.
Rid region = NavigationServer3D.RegionCreate();
NavigationServer3D.RegionSetTransform(region, Transform3D.Identity);
NavigationServer3D.RegionSetMap(region, map);
// Create a procedural navigation mesh for the region.
var newNavigationMesh = new NavigationMesh()
{
Vertices =
[
new Vector3(0.0f, 0.0f, 0.0f),
new Vector3(9.0f, 0.0f, 0.0f),
new Vector3(0.0f, 0.0f, 9.0f),
],
};
int[] polygon = [0, 1, 2];
newNavigationMesh.AddPolygon(polygon);
NavigationServer3D.RegionSetNavigationMesh(region, newNavigationMesh);
// Wait for NavigationServer sync to adapt to made changes.
await ToSignal(GetTree(), SceneTree.SignalName.PhysicsFrame);
// Query the path from the navigation server.
var startPosition = new Vector3(0.1f, 0.0f, 0.1f);
var targetPosition = new Vector3(1.0f, 0.0f, 1.0f);
Vector3[] path = NavigationServer3D.MapGetPath(map, startPosition, targetPosition, optimize: true);
GD.Print("Found a path!");
GD.Print((Variant)path);
}
}
伺服器避障回呼
如果 RVO 避障代理註冊了避障回呼,NavigationServer 會在 PhysicsServer 同步前,發出這些代理的 velocity_computed 訊號。
想進一步了解 NavigationAgent,請參閱 使用 NavigationAgent。
使用避障功能的 NavigationAgent 執行流程簡述如下:
物理影格開始。
執行
_physics_process(delta)。在 NavigationAgent 節點設置
velocity屬性。代理將速度與位置傳送給 NavigationServer。
NavigationServer 等待同步。
NavigationServer 進行同步,並計算所有註冊避障代理的避障速度。
NavigationServer 會針對每個已註冊的避障代理,發送安全速度向量(safe velocity)訊號。
代理收到訊號後,會用
move_and_slide或linear_velocity等方式移動其父節點。PhysicsServer 進行同步。
物理影格結束。
因此,在回呼函式中用安全速度(safe velocity)移動 physicsbody actor 是完全執行緒安全且物理安全的,因為這一切都發生在同一個物理影格內、PhysicsServer 正式套用所有變更與進行物理運算之前。