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.

使用 NavigationServer

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

與 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 在各自維度上的功能是等價的。

技術上來說,也可以用一個維度的導覽網格工具來建立另一個維度的網格,例如:用平面 3D 幾何體搭配 3D NavigationMesh 來烘焙 2D 導覽網格,或是用 NavigationRegion2D 與 NavigationPolygons 的多邊形繪製工具來產生平面 3D 導覽網格。

等待同步

在遊戲剛開始、新場景載入,或是程式動態變更導覽時,對 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 正式套用所有變更與進行物理運算之前。