使用导航网格

2D 和 3D 版本的导航网格分别为 NavigationPolygon 和 NavigationMesh。
备注
导航网格描述的只是代理中心位置的可达区域,会忽略代理可能存在的半径值。如果你想要让寻路考虑代理的(碰撞)尺寸,就需要让导航网格收缩对应的量。
导航系统的工作独立于渲染或物理等其他引擎部分。导航系统在寻路时只考虑导航网格,视觉效果和碰撞形状等会被导航系统完全忽略。如果在寻路时需要考虑其他数据(例如视觉效果),则需要对导航网格进行相应调整。在导航网格中考虑导航限制的过程通常称为导航网格烘焙。

导航网格描述的是代理的中心点能够安全站立的表面,而物理形状描述的则是外部的碰撞范围。
如果你在遵循导航路径时遇到剪切或碰撞问题,请务必记住,你需要通过合适的导航网格告诉导航系统你的意图。导航系统本身永远不会知道 "这是树木/岩石/墙壁碰撞形状或可视化网格",因为它只知道 "我被告知在这里可以安全通过,因为它在导航网格上"。
导航网格的烘焙可以使用 NavigationRegion2D 或 NavigationRegion3D 实现,也可以直接使用 NavigationServer2D 和 NavigationServer3D 的 API。
使用导航区块 NavigationRegion 烘焙导航网格

根据代理半径和几何体之间的偏移量烘焙导航网格。
使用导航区块节点可以更方便地进行导航网格烘焙。使用导航区域节点进行烘焙时,所有解析、烘焙和区块更新步骤都会合并到一个函数中。
2D 和 3D 版本分别为 NavigationRegion2D 和 NavigationRegion3D。
小技巧
The navigation mesh source_geometry_mode
can be switched to parse specific node group names so nodes that should be baked can be placed anywhere in the scene.
在编辑器中选择导航区块2D节点时,编辑器顶部栏会显示烘焙选项和多边形绘制工具。

为了使区块工作,需要添加一个 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 版本的源几何体资源分别为 NavigationMeshSourceGeometryData2D 和 NavigationMeshSourceGeometryData3D。
源几何体可以是从视觉网格、物理碰撞或程序创建的数据阵列(如轮廓(2D)和三角面(3D))解析的几何体。为方便起见,通常直接从场景树中的节点设置解析源几何体。对于运行时导航网格(重新)烘焙,请注意几何体解析始终发生在主线程上。
备注
SceneTree不是线程安全的。从SceneTree解析源几何体只能在主线程上完成。
警告
需要从GPU接收来自视觉网格和多边形的数据,从而在此过程中停止RenderingServer。对于运行时(重新)烘焙,更喜欢使用物理形状作为解析的源几何体。
源几何图形存储在资源中,因此创建的几何图形可以重复用于多次烘焙。例如,为来自同一源几何体的不同代理大小烘焙多个导航网格。这也允许将源几何体保存到磁盘,以便稍后加载,例如避免在运行时再次解析的开销。
几何数据通常应保持非常简单。需要尽可能多的边缘,但尽可能少。尤其是在2D中,应避免重复和嵌套几何体,因为它会强制计算多边形孔,从而导致翻转多边形。嵌套几何体的示例是完全放置在另一个StaticBody2D形状的边界内的较小StaticBody2D形状。
针对大世界烘焙导航网格块

在运行时构建并更新导航网格块。
参见
You can see the navigation mesh chunk baking in action in the Navigation Mesh Chunks 2D and Navigation Mesh Chunks 3D demo projects.
为了避免不同区块之间的边缘不对齐,导航网格提供了两个重要的属性,用于控制烘焙过程:烘焙边界和边框大小。两者配合使用就可以确保不同区块的边缘能够完美对齐。

指定烘焙范围和额外边框大小的导航网格块。
烘焙边界在 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
等大多数偏移和后期处理的影响。
边框大小并不会丢弃来源几何体,而是会丢弃烘焙后导航网格最终表面上的部分区域。如果烘焙边界足够大,边框大小就能够去除表面上有问题的部分区域,只保留预期的区块大小。

导航网格块互相对齐了边缘,没有间隙。
备注
烘焙边界需要设置得足够大,才能够包含来自所有相邻块的合理数量的来源几何体。
警告
In 3D the functionality of the border size is limited to the xz-axis.
烘焙导航网格时的常见问题
在创建或烘焙导航网格时,需要考虑一些常见的用户问题和重要的注意事项。
- 运行时烘焙导航网格会造成帧率问题
默认情况下,导航网格烘焙是在后台线程上完成的,因此只要平台支持线程,实际烘焙就很少是任何性能问题的根源(假设运行时重新烘焙的几何体大小合理且复杂)。
运行时性能问题的常见来源是涉及节点和场景树的源几何体的解析步骤。SceneTree不是线程安全的,因此所有节点都需要在主线程上解析。一些具有大量数据的节点在运行时可能非常重且解析缓慢,例如,TileMap为每个使用的单元和TileMapLayer都有一个或多个多边形要解析。持有网格的节点需要从RenderingServer请求数据,从而在此过程中停止渲染。
为了提高性能,请使用更优化的形状,例如在详细的视觉网格上使用碰撞形状,并提前合并和简化尽可能多的几何体。如果没有任何帮助,请不要解析SceneTree并使用脚本添加源几何体过程。如果仅使用纯数据数组作为源几何体,则可以在后台线程上完成整个烘焙过程。
- Navigation mesh 在 2D 创建 unintended holes。
2D 中的导航网格烘焙是通过基于轮廓路径执行多边形剪裁操作来完成的。创建更加复杂的 2D 多边形时,多边形带有“孔洞”是必要之恶,不过对需要用到大量复杂形状的用户而言,可能会变得无法预测。
为避免多边形孔计算出现任何意外问题,请避免将任何轮廓嵌套在同一类型的其他轮廓内(可遍历/障碍物)。这包括来自节点的已解析形状。例如,将较小的StaticBody2D形状放置在较大的StaticBody2D形状内可能会导致所生成的多边形翻转。
- 导航网格显示在三维几何体内部。
3D中的导航网格烘焙没有“内部”的概念。用于光栅化几何体的体素单元被占用或未被占用。删除其他几何图形中位于地面上的几何图形。如果无法做到这一点,请在内部添加较小的“虚拟”几何体,并尽可能少地添加三角形,这样单元就会被一些东西占据。
烘焙导航网格时也可以用 NavigationObstacle3D 形状来丢弃几何体。

可以用 NavigationObstacle3D 形状来丢弃导航网格上不需要的部分。
导航网格脚本模板
以下脚本使用NavigationServer解析场景树中的源几何体,烘焙导航网格,并使用更新的导航网格更新导航区域。
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);
}
}