使用 NavigationServer

NavigationServer 提供了 2D 和 3D 兩個版本,分別為 NavigationServer2DNavigationServer3D

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)

伺服器避障回呼

如果 RVO 避障代理註冊了避障回呼,NavigationServer 會在 PhysicsServer 同步前,發出這些代理的 velocity_computed 訊號。

想進一步了解 NavigationAgent,請參閱 使用 NavigationAgent

使用避障功能的 NavigationAgent 執行流程簡述如下:

  • 物理影格開始。

  • 執行 _physics_process(delta)

  • 在 NavigationAgent 節點設置 velocity 屬性。

  • 代理將速度與位置傳送給 NavigationServer。

  • NavigationServer 等待同步。

  • NavigationServer 進行同步,並計算所有註冊避障代理的避障速度。

  • NavigationServer 會針對每個已註冊的避障代理,發送安全速度向量(safe velocity)訊號。

  • 代理收到訊號後,會用 move_and_slidelinear_velocity 等方式移動其父節點。

  • PhysicsServer 進行同步。

  • 物理影格結束。

因此,在回呼函式中用安全速度(safe velocity)移動 physicsbody actor 是完全執行緒安全且物理安全的,因為這一切都發生在同一個物理影格內、PhysicsServer 正式套用所有變更與進行物理運算之前。