使用導航網格
導航網格的 2D 與 3D 版本分別為 NavigationPolygon 與 NavigationMesh。
備註
導航網格僅描述代理物件中心點可通行的區域,不會考慮代理物件的半徑大小。如果你需要尋路時考慮代理物件(碰撞)尺寸,必須相應縮小導航網格。
導航功能與引擎其他部分(如繪圖或物理)是獨立運作的。進行尋路時,系統只會參考導航網格,其它如可見物件或碰撞形狀都會被完全忽略。如果你希望尋路時考慮其他資料(例如可見物件),就必須自行調整導航網格。將這些限制條件烘焙進導航網格的過程,通常稱為導航網格烘焙(baking)。
導航網格描述的是代理物件中心能安全站立的表面,這與物理形狀描述外部碰撞邊界不同。
如果在循導航路徑時發生穿模或碰撞問題,請記得必須透過合適的導航網格來告知導航系統你的設計意圖。導航系統本身並不會知道「這裡是一棵樹/石頭/牆壁的碰撞形狀或視覺網格」,它只知道「這裡有導航網格,所以可以安全通行」。
導航網格的烘焙可以透過 NavigationRegion2D 或 NavigationRegion3D 節點來進行,也可以直接使用 NavigationServer2D 與 NavigationServer3D API 來操作。
使用 NavigationRegion 烘焙導航網格
根據代理半徑對幾何進行偏移後烘焙導航網格。
透過 NavigationRegion 節點,導航網格烘焙變得更方便。使用 NavigationRegion 節點烘焙時,解析、烘焙與區域更新等步驟會自動整合為單一功能。
這些節點在 2D 與 3D 中分別為 NavigationRegion2D 與 NavigationRegion3D。
小訣竅
導航網格的 source_geometry_mode 可切換為僅解析特定節點群組名稱,讓需要被烘焙的節點可以放在場景的任意位置。
當在編輯器選擇 NavigationRegion2D 節點時,頂部工具列會顯示烘焙選項與多邊形繪製工具。
為了讓此區域能運作,必須新增一個 NavigationPolygon 資源。
用來解析與烘焙導航網格的屬性會在該資源中,並可於資源屬性檢查器中找到。
以下屬性可影響來源幾何解析的結果。
parsed_geometry_type用來過濾是否從 SceneTree 解析視覺物件、物理物件,或兩者都解析。更多細節請參考下方的『解析來源幾何』章節。當
parsed_geometry_type包含靜態碰撞器時,collision_mask可以用來過濾哪些物理碰撞物件會被包含。source_geometry_mode用來定義從哪些節點開始解析,以及如何遍歷 SceneTree。僅解析某個節點群組時,請設置
source_geometry_group_name,實際效果取決於source_geometry_mode。
設置來源幾何之後,可用以下屬性調整烘焙結果。
cell_size設定網格的像素(格點)大小,應與導航地圖尺寸相符。agent_radius會縮小烘焙後的導航網格,為代理物件(碰撞)尺寸預留足夠邊界。
NavigationRegion2D 的烘焙功能也可以於執行時透過腳本使用。
var on_thread: bool = true
bake_navigation_polygon(on_thread)
bool onThread = true;
BakeNavigationPolygon(onThread);
要快速測試 2D 導航網格的預設烘焙流程:
新增一個 NavigationRegion2D。
在 NavigationRegion2D 上新增 NavigationPolygon 資源。
在 NavigationRegion2D 節點下方新增 Polygon2D。
使用 NavigationRegion2D 的多邊形繪製工具繪製一個 NavigationPolygon 外框。
使用 Polygon2D 的繪製工具,在 NavigationPolygon 外框內畫出一個 Polygon2D 外框。
點擊編輯器的烘焙按鈕,即可看到生成的導航網格。
在編輯器中選擇 NavigationRegion3D 節點時,頂部工具列會顯示烘焙相關選項。
必須新增一個 NavigationMesh 資源到該區域才能運作。
用來解析與烘焙導航網格的屬性會在該資源中,並可於資源屬性檢查器中找到。
以下屬性可影響來源幾何解析的結果。
parsed_geometry_type用來過濾是否從 SceneTree 解析視覺物件、物理物件,或兩者都解析。更多細節請參考下方的『解析來源幾何』章節。當
parsed_geometry_type包含靜態碰撞器時,collision_mask可以用來過濾哪些物理碰撞物件會被包含。source_geometry_mode用來定義從哪些節點開始解析,以及如何遍歷 SceneTree。僅解析某個節點群組時,請設置
source_geometry_group_name,實際效果取決於source_geometry_mode。
設置來源幾何之後,可用以下屬性調整烘焙結果。
cell_size與cell_height設定體素網格的格點大小,應與導航地圖尺寸相符。agent_radius會縮小烘焙後的導航網格,為代理物件(碰撞)尺寸預留足夠邊界。agent_height用於排除代理物件高度無法通過的區域。agent_max_climb與agent_max_slope可排除相鄰體素高度差過大或表面坡度過陡的區域。
警告
cell_size 或 cell_height 設得太小時,將產生過多體素,可能導致遊戲卡死甚至崩潰。
NavigationRegion3D 的烘焙功能也可於執行時透過腳本使用。
var on_thread: bool = true
bake_navigation_mesh(on_thread)
bool onThread = true;
BakeNavigationMesh(onThread);
要快速測試 3D 導航網格的預設烘焙流程:
新增一個 NavigationRegion3D。
在 NavigationRegion3D 上新增 NavigationMesh 資源。
在 NavigationRegion3D 節點下方新增 MeshInstance3D。
在 MeshInstance3D 上新增一個 PlaneMesh。
點擊編輯器的烘焙按鈕,即可看到生成的導航網格。
使用 NavigationServer 烘焙導航網格
NavigationServer2D 與 NavigationServer3D 提供 API,可分別呼叫導航網格烘焙流程中的各個步驟。
parse_source_geometry_data()用於將來源幾何解析為可重複使用並可序列化的資源。bake_from_source_geometry_data()用於根據已解析的資料烘焙導航網格,例如可避免執行時重複解析而導致效能問題。bake_from_source_geometry_data_async()功能相同,但會以多線程方式延遲烘焙導航網格,不會阻塞主線程。
與 NavigationRegion 相比,NavigationServer 提供更細緻的導航網格烘焙流程控制,但相對更複雜,也具備更多進階選項。
NavigationServer 相較於 NavigationRegion 的其他優點包括:
伺服器可僅解析來源幾何而不立即烘焙,例如用於快取以便日後使用。
伺服器允許手動指定來源幾何解析時的根節點。
伺服器可接受並烘焙由程式產生的來源幾何資料。
伺服器可連續烘焙多個導航網格,並重複利用同一組來源幾何資料。
使用 NavigationServer 烘焙導航網格時,必須有來源幾何。來源幾何即是導航網格烘焙過程中應考慮的幾何資料。2D 與 3D 導航網格皆是由來源幾何烘焙而成。
來源幾何資源在 2D 與 3D 中分別為 NavigationMeshSourceGeometryData2D 與 NavigationMeshSourceGeometryData3D。
來源幾何可以來自視覺網格、物理碰撞,或程式產生的資料陣列(例如 2D 輪廓、3D 三角面)。一般情況下,來源幾何會直接從 SceneTree 的節點配置中解析。注意,執行時重新烘焙導航網格時,解析幾何資料一定會在主線程進行。
備註
SceneTree 並非執行緒安全,從 SceneTree 解析來源幾何只能在主線程執行。
警告
從視覺網格與多邊形取得資料時需由 GPU 傳回,過程會讓 RenderingServer 停頓。執行時重新烘焙導航網格時,建議盡量使用物理形狀作為解析來源幾何。
來源幾何會存於資源中,因此可多次重複利用,例如針對不同代理尺寸烘焙多個導航網格。也可以將來源幾何儲存到磁碟以便日後載入,避免執行時重複解析造成效能負擔。
幾何資料應盡量簡單,邊數只要足夠即可,避免冗餘。特別是在 2D 中,應避免重複或巢狀幾何,否則會強制啟用多邊形洞計算,導致多邊形翻轉。例如,一個較小的 StaticBody2D 完全放在另一個 StaticBody2D 範圍內,就是巢狀幾何。
在大型世界場景中分塊烘焙導航網格
執行時建立與更新各個導航網格分塊。
也參考
你可以在 Navigation Mesh Chunks 2D 與 Navigation Mesh Chunks 3D 範例專案中看到導航網格分塊烘焙的實際運作。
為避免不同區塊之間的邊緣錯位,導航網格烘焙過程有兩個重要屬性:烘焙邊界(baking bound)與邊界尺寸(border size)。兩者搭配可確保區塊之間的邊緣完美對齊。
導航網格分塊可僅設烘焙邊界,或額外加上邊界尺寸來烘焙。
烘焙邊界在 2D 中為軸對齊的 Rect2,在 3D 中為 AABB,會將烘焙範圍外的來源幾何全部排除。
NavigationPolygon 的 baking_rect 與 baking_rect_offset 屬性可用來設定 2D 烘焙邊界與其位置。
NavigationMesh 的 filter_baking_aabb 與 filter_baking_aabb_offset 屬性可用來設定 3D 烘焙邊界與其位置。
僅設置烘焙邊界仍會有問題,例如 agent_radius 等必要偏移會導致產生的導航網格邊緣無法正確對齊。
由於烘焙時考慮代理半徑偏移,導航網格分塊之間會出現明顯間隙。
這時就需要使用導航網格的 border_size 屬性。邊界尺寸是烘焙邊界向內的留白,其特點是大多數偏移與後處理(如 agent_radius)都不會影響它。
邊界尺寸不是用來排除來源幾何,而是用來裁剪烘焙後導航網格的表面。如果烘焙邊界夠大,邊界尺寸就能移除有問題的表面,只留下預期尺寸的分塊。
邊緣對齊且無間隙的導航網格分塊。
備註
烘焙邊界必須足夠大,才能涵蓋所有相鄰分塊的合理來源幾何。
警告
在 3D 中,邊界尺寸僅作用於 xz 平面。
導航網格烘焙常見問題
建立或烘焙導航網格時,常見以下問題與注意事項。
- 導航網格烘焙於執行時造成 FPS 掉幀
導航網格烘焙預設會在背景執行緒進行,只要平台支援多執行緒,實際上烘焙本身很少成為效能問題來源(前提是執行時重新烘焙的幾何數量與複雜度是合理的)。
執行時效能問題多半來自來源幾何的解析階段,這通常需要存取節點與 SceneTree。而 SceneTree 並非執行緒安全,所有節點必須在主線程解析。部分數據量龐大的節點(如 TileMap,每個格子及圖層都需解析多邊形)在執行時解析會相當耗時。包含網格的節點還需從 RenderingServer 請求資料,也會讓渲染過程暫停。
提升效能的方法包括:盡可能使用簡化的形狀(如碰撞形狀代替複雜視覺網格)、預先合併與簡化幾何。若仍無法改善,則不要從 SceneTree 解析,而是用腳本程式生成來源幾何。若來源僅為純資料陣列,整個烘焙過程皆可於背景執行緒進行。
- 2D 導航網格產生非預期的孔洞。
2D 導航網格烘焙會根據外框路徑進行多邊形裁切。多邊形上的「孔洞」是為了支援更複雜的 2D 形狀,但當有眾多複雜形狀時,這些孔洞的結果可能會變得難以預測。
為避免多邊形孔洞計算產生非預期問題,請避免在同類型(可通行/障礙)外框內巢狀其他外框,包括節點解析所得的形狀。例如,將一個小的 StaticBody2D 放在較大的 StaticBody2D 內,可能會導致多邊形翻轉。
- 3D 導航網格出現在幾何體內部。
3D 導航網格烘焙時並沒有「內部」的概念,體素格點只分為被佔據或未被佔據。請移除被其他幾何包覆的地面幾何。如果不行,可以在內部加入最簡單的「假」幾何(少量三角形),讓這些格點有所佔用。
也可以將 NavigationObstacle3D 設為參與導航網格烘焙,以移除不需要的幾何。
可利用 NavigationObstacle3D 形狀來移除不需要的導航網格部分。
導航網格腳本範本
以下腳本範例會使用 NavigationServer 解析 SceneTree 的來源幾何,烘焙導航網格,並用新導航網格更新導航區域。
extends Node2D
var navigation_mesh: NavigationPolygon
var source_geometry : NavigationMeshSourceGeometryData2D
var callback_parsing : Callable
var callback_baking : Callable
var region_rid: RID
func _ready() -> void:
navigation_mesh = NavigationPolygon.new()
navigation_mesh.agent_radius = 10.0
source_geometry = NavigationMeshSourceGeometryData2D.new()
callback_parsing = on_parsing_done
callback_baking = on_baking_done
region_rid = NavigationServer2D.region_create()
# Enable the region and set it to the default navigation map.
NavigationServer2D.region_set_enabled(region_rid, true)
NavigationServer2D.region_set_map(region_rid, get_world_2d().get_navigation_map())
# Some mega-nodes like TileMap are often not ready on the first frame.
# Also the parsing needs to happen on the main-thread.
# So do a deferred call to avoid common parsing issues.
parse_source_geometry.call_deferred()
func parse_source_geometry() -> void:
source_geometry.clear()
var root_node: Node2D = self
# Parse the obstruction outlines from all child nodes of the root node by default.
NavigationServer2D.parse_source_geometry_data(
navigation_mesh,
source_geometry,
root_node,
callback_parsing
)
func on_parsing_done() -> void:
# If we did not parse a TileMap with navigation mesh cells we may now only
# have obstruction outlines so add at least one traversable outline
# so the obstructions outlines have something to "cut" into.
source_geometry.add_traversable_outline(PackedVector2Array([
Vector2(0.0, 0.0),
Vector2(500.0, 0.0),
Vector2(500.0, 500.0),
Vector2(0.0, 500.0)
]))
# Bake the navigation mesh on a thread with the source geometry data.
NavigationServer2D.bake_from_source_geometry_data_async(
navigation_mesh,
source_geometry,
callback_baking
)
func on_baking_done() -> void:
# Update the region with the updated navigation mesh.
NavigationServer2D.region_set_navigation_polygon(region_rid, navigation_mesh)
using Godot;
public partial class MyNode2D : Node2D
{
private NavigationPolygon _navigationMesh;
private NavigationMeshSourceGeometryData2D _sourceGeometry;
private Callable _callbackParsing;
private Callable _callbackBaking;
private Rid _regionRid;
public override void _Ready()
{
_navigationMesh = new NavigationPolygon();
_navigationMesh.AgentRadius = 10.0f;
_sourceGeometry = new NavigationMeshSourceGeometryData2D();
_callbackParsing = Callable.From(OnParsingDone);
_callbackBaking = Callable.From(OnBakingDone);
_regionRid = NavigationServer2D.RegionCreate();
// Enable the region and set it to the default navigation map.
NavigationServer2D.RegionSetEnabled(_regionRid, true);
NavigationServer2D.RegionSetMap(_regionRid, GetWorld2D().NavigationMap);
// Some mega-nodes like TileMap are often not ready on the first frame.
// Also the parsing needs to happen on the main-thread.
// So do a deferred call to avoid common parsing issues.
CallDeferred(MethodName.ParseSourceGeometry);
}
private void ParseSourceGeometry()
{
_sourceGeometry.Clear();
Node2D rootNode = this;
// Parse the obstruction outlines from all child nodes of the root node by default.
NavigationServer2D.ParseSourceGeometryData(
_navigationMesh,
_sourceGeometry,
rootNode,
_callbackParsing
);
}
private void OnParsingDone()
{
// If we did not parse a TileMap with navigation mesh cells we may now only
// have obstruction outlines so add at least one traversable outline
// so the obstructions outlines have something to "cut" into.
_sourceGeometry.AddTraversableOutline(
[
new Vector2(0.0f, 0.0f),
new Vector2(500.0f, 0.0f),
new Vector2(500.0f, 500.0f),
new Vector2(0.0f, 500.0f),
]);
// Bake the navigation mesh on a thread with the source geometry data.
NavigationServer2D.BakeFromSourceGeometryDataAsync(_navigationMesh, _sourceGeometry, _callbackBaking);
}
private void OnBakingDone()
{
// Update the region with the updated navigation mesh.
NavigationServer2D.RegionSetNavigationPolygon(_regionRid, _navigationMesh);
}
}
extends Node3D
var navigation_mesh: NavigationMesh
var source_geometry : NavigationMeshSourceGeometryData3D
var callback_parsing : Callable
var callback_baking : Callable
var region_rid: RID
func _ready() -> void:
navigation_mesh = NavigationMesh.new()
navigation_mesh.agent_radius = 0.5
source_geometry = NavigationMeshSourceGeometryData3D.new()
callback_parsing = on_parsing_done
callback_baking = on_baking_done
region_rid = NavigationServer3D.region_create()
# Enable the region and set it to the default navigation map.
NavigationServer3D.region_set_enabled(region_rid, true)
NavigationServer3D.region_set_map(region_rid, get_world_3d().get_navigation_map())
# Some mega-nodes like GridMap are often not ready on the first frame.
# Also the parsing needs to happen on the main-thread.
# So do a deferred call to avoid common parsing issues.
parse_source_geometry.call_deferred()
func parse_source_geometry() -> void:
source_geometry.clear()
var root_node: Node3D = self
# Parse the geometry from all mesh child nodes of the root node by default.
NavigationServer3D.parse_source_geometry_data(
navigation_mesh,
source_geometry,
root_node,
callback_parsing
)
func on_parsing_done() -> void:
# Bake the navigation mesh on a thread with the source geometry data.
NavigationServer3D.bake_from_source_geometry_data_async(
navigation_mesh,
source_geometry,
callback_baking
)
func on_baking_done() -> void:
# Update the region with the updated navigation mesh.
NavigationServer3D.region_set_navigation_mesh(region_rid, navigation_mesh)
using Godot;
public partial class MyNode3D : Node3D
{
private NavigationMesh _navigationMesh;
private NavigationMeshSourceGeometryData3D _sourceGeometry;
private Callable _callbackParsing;
private Callable _callbackBaking;
private Rid _regionRid;
public override void _Ready()
{
_navigationMesh = new NavigationMesh();
_navigationMesh.AgentRadius = 0.5f;
_sourceGeometry = new NavigationMeshSourceGeometryData3D();
_callbackParsing = Callable.From(OnParsingDone);
_callbackBaking = Callable.From(OnBakingDone);
_regionRid = NavigationServer3D.RegionCreate();
// Enable the region and set it to the default navigation map.
NavigationServer3D.RegionSetEnabled(_regionRid, true);
NavigationServer3D.RegionSetMap(_regionRid, GetWorld3D().NavigationMap);
// Some mega-nodes like GridMap are often not ready on the first frame.
// Also the parsing needs to happen on the main-thread.
// So do a deferred call to avoid common parsing issues.
CallDeferred(MethodName.ParseSourceGeometry);
}
private void ParseSourceGeometry ()
{
_sourceGeometry.Clear();
Node3D rootNode = this;
// Parse the geometry from all mesh child nodes of the root node by default.
NavigationServer3D.ParseSourceGeometryData(
_navigationMesh,
_sourceGeometry,
rootNode,
_callbackParsing
);
}
private void OnParsingDone()
{
// Bake the navigation mesh on a thread with the source geometry data.
NavigationServer3D.BakeFromSourceGeometryDataAsync(_navigationMesh, _sourceGeometry, _callbackBaking);
}
private void OnBakingDone()
{
// Update the region with the updated navigation mesh.
NavigationServer3D.RegionSetNavigationMesh(_regionRid, _navigationMesh);
}
}
以下腳本範例會利用 NavigationServer 將程式生成的導航網格資料套用於導航區域。
extends Node2D
var navigation_mesh: NavigationPolygon
var region_rid: RID
func _ready() -> void:
navigation_mesh = NavigationPolygon.new()
region_rid = NavigationServer2D.region_create()
# Enable the region and set it to the default navigation map.
NavigationServer2D.region_set_enabled(region_rid, true)
NavigationServer2D.region_set_map(region_rid, get_world_2d().get_navigation_map())
# Add vertices for a convex polygon.
navigation_mesh.vertices = PackedVector2Array([
Vector2(0.0, 0.0),
Vector2(100.0, 0.0),
Vector2(100.0, 100.0),
Vector2(0.0, 100.0),
])
# Add indices for the polygon.
navigation_mesh.add_polygon(
PackedInt32Array([0, 1, 2, 3])
)
NavigationServer2D.region_set_navigation_polygon(region_rid, navigation_mesh)
using Godot;
public partial class MyNode2D : Node2D
{
private NavigationPolygon _navigationMesh;
private Rid _regionRid;
public override void _Ready()
{
_navigationMesh = new NavigationPolygon();
_regionRid = NavigationServer2D.RegionCreate();
// Enable the region and set it to the default navigation map.
NavigationServer2D.RegionSetEnabled(_regionRid, true);
NavigationServer2D.RegionSetMap(_regionRid, GetWorld2D().NavigationMap);
// Add vertices for a convex polygon.
_navigationMesh.Vertices =
[
new Vector2(0, 0),
new Vector2(100.0f, 0),
new Vector2(100.0f, 100.0f),
new Vector2(0, 100.0f),
];
// Add indices for the polygon.
_navigationMesh.AddPolygon([0, 1, 2, 3]);
NavigationServer2D.RegionSetNavigationPolygon(_regionRid, _navigationMesh);
}
}
extends Node3D
var navigation_mesh: NavigationMesh
var region_rid: RID
func _ready() -> void:
navigation_mesh = NavigationMesh.new()
region_rid = NavigationServer3D.region_create()
# Enable the region and set it to the default navigation map.
NavigationServer3D.region_set_enabled(region_rid, true)
NavigationServer3D.region_set_map(region_rid, get_world_3d().get_navigation_map())
# Add vertices for a convex polygon.
navigation_mesh.vertices = PackedVector3Array([
Vector3(-1.0, 0.0, 1.0),
Vector3(1.0, 0.0, 1.0),
Vector3(1.0, 0.0, -1.0),
Vector3(-1.0, 0.0, -1.0),
])
# Add indices for the polygon.
navigation_mesh.add_polygon(
PackedInt32Array([0, 1, 2, 3])
)
NavigationServer3D.region_set_navigation_mesh(region_rid, navigation_mesh)
using Godot;
public partial class MyNode3D : Node3D
{
private NavigationMesh _navigationMesh;
private Rid _regionRid;
public override void _Ready()
{
_navigationMesh = new NavigationMesh();
_regionRid = NavigationServer3D.RegionCreate();
// Enable the region and set it to the default navigation map.
NavigationServer3D.RegionSetEnabled(_regionRid, true);
NavigationServer3D.RegionSetMap(_regionRid, GetWorld3D().NavigationMap);
// Add vertices for a convex polygon.
_navigationMesh.Vertices =
[
new Vector3(-1.0f, 0.0f, 1.0f),
new Vector3(1.0f, 0.0f, 1.0f),
new Vector3(1.0f, 0.0f, -1.0f),
new Vector3(-1.0f, 0.0f, -1.0f),
];
// Add indices for the polygon.
_navigationMesh.AddPolygon([0, 1, 2, 3]);
NavigationServer3D.RegionSetNavigationMesh(_regionRid, _navigationMesh);
}
}