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...
使用導航網格
導航網格的 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);
}
}