使用导航网格

../../_images/nav_meshes.webp

2D 和 3D 版本的导航网格分别为 NavigationPolygonNavigationMesh

备注

导航网格描述的只是代理中心位置的可达区域,会忽略代理可能存在的半径值。如果你想要让寻路考虑代理的(碰撞)尺寸,就需要让导航网格收缩对应的量。

导航系统的工作独立于渲染或物理等其他引擎部分。导航系统在寻路时只考虑导航网格,视觉效果和碰撞形状等会被导航系统完全忽略。如果在寻路时需要考虑其他数据(例如视觉效果),则需要对导航网格进行相应调整。在导航网格中考虑导航限制的过程通常称为导航网格烘焙。

导航网格凸多边形与凹多边形的比较

导航网格描述的是代理的中心点能够安全站立的表面,而物理形状描述的则是外部的碰撞范围。

如果你在遵循导航路径时遇到剪切或碰撞问题,请务必记住,你需要通过合适的导航网格告诉导航系统你的意图。导航系统本身永远不会知道 “这是树木/岩石/墙壁碰撞形状或可视化网格”,因为它只知道 “我被告知在这里可以安全通过,因为它在导航网格上”。

导航网格的烘焙可以使用 NavigationRegion2DNavigationRegion3D 实现,也可以直接使用 NavigationServer2DNavigationServer3D 的 API。

使用导航区块 NavigationRegion 烘焙导航网格

烘焙导航网格的步骤

根据代理半径和几何体之间的偏移量烘焙导航网格。

使用导航区块节点可以更方便地进行导航网格烘焙。使用导航区域节点进行烘焙时,所有解析、烘焙和区块更新步骤都会合并到一个函数中。

2D 和 3D 版本分别为 NavigationRegion2DNavigationRegion3D

Baking with a NavigationRegion2DBaking with a NavigationRegion3D

在编辑器中选择导航区块2D节点时,编辑器顶部栏会显示烘焙选项和多边形绘制工具。

../../_images/nav_region_baking_01.webp

为了使区块工作,需要添加一个 NavigationPolygon 资源。

解析和烘焙导航网格的属性是所用资源的一部分,可以在资源检查器中找到。

../../_images/nav_region_baking_02.webp

下列属性会影响来源几何体的解析结果。

  • “parsed_geometry_type”用于筛选是否应从 :ref:`SceneTree<class_SceneTree>`解析视觉对象或物理对象或两者。有关解析哪些对象以及如何解析的更多详细信息,请参阅下面关于解析源几何体的部分。

  • parsed_geometry_type 包括物理碰撞时, collision_mask 过滤哪些物理碰撞对象被包括在内。

  • source_geometry_mode 定义在哪个节点上开始解析,以及如何遍历 SceneTree

  • source_geometry_group_name 在只应解析某个节点组时使用。取决于所选的 source_geometry_mode

添加来源几何体后,以下属性可以控制烘焙的结果。

  • cell_size 设置栅格网格大小,并且应与导航地图大小相匹配。

  • agent_radius 收缩烘焙的导航网格,以便为代理(碰撞)大小提供足够的边距。

NavigationRegion2D烘焙也可以在运行时与脚本一起使用。

GDScript

  1. var on_thread: bool = true
  2. bake_navigation_polygon(on_thread)

要使用默认设置快速测试 2D 烘焙:

  • 添加一个 NavigationRegion2D

  • 为 NavigationRegion2D 添加一个 NavigationPolygon 资源。

  • 在 NavigationRegion2D 下面添加一个 Polygon2D

  • 使用选中的 NavigationRegion2D 绘制工具绘制一个 NavigationPolygon 轮廓。

  • 使用选中的 Polygon2D 绘制工具在 NavigationPolygon 轮廓中绘制一个 Polygon2D 轮廓。

  • 点击编辑器的烘焙按钮,就会出现导航网格。

../../_images/nav_region_baking_01.webp ../../_images/nav_mesh_mini_2d.webp

在编辑器中选择NavigationRegion3D节点后,烘焙选项将显示在编辑器的顶部栏中。

../../_images/nav_mesh_bake_toolbar.webp

添加 NavigationMesh 资源后,区块才能够正常工作。

解析和烘焙导航网格的属性是所用资源的一部分,可以在资源检查器中找到。

../../_images/nav_region3d_baking_01.webp

下列属性会影响来源几何体的解析结果。

  • “parsed_geometry_type”用于筛选是否应从 :ref:`SceneTree<class_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_climbagent_max_slope 可移除相邻体素之间高度差过大或其表面过陡的区域。

警告

太小的 cell_sizecell_height 可能会创建太多的体素,从而有可能冻结游戏甚至崩溃。

NavigationRegion3D烘焙也可以在运行时与脚本一起使用。

GDScript

  1. var on_thread: bool = true
  2. bake_navigation_mesh(on_thread)

要使用默认设置快速测试 3D 烘焙:

../../_images/nav_mesh_bake_toolbar.webp ../../_images/nav_mesh_mini_3d.webp

使用 NavigationServer 烘焙导航网格

NavigationServer2DNavigationServer3D 都提供了烘焙导航网格相关的 API 函数,可以在烘焙过程中的不同阶段单独调用。

  • parse_source_geometry_data() 可用于将源几何体解析为可重用和可序列化的资源。

  • bake_from_source_geometry_data() 可用于根据已解析的数据烘焙导航网格,例如避免(冗余)解析的运行时性能问题。

  • bake_from_source_geometry_data_async() 是相同的,但烘焙用线程延迟的导航网格,而不是阻塞主线程。

与NavigationRegion相比,NavigationServer对导航网格烘焙过程提供了更精细的控制。反过来,它的使用更加复杂,但也提供了更高级的选项。

NavigationServer相对于NavigationRegion的其他一些优点是:

  • 服务器可以在不烘焙的情况下解析源几何体,例如缓存以供以后使用。

  • 服务器允许选择根节点,以便手动启动源几何体解析。

  • 服务器可以接受程序生成的源几何图形数据并从中烘焙。

  • 服务器可以按顺序烘焙多个导航网格,同时(重新)使用相同的源几何体数据。

若要使用NavigationServer烘焙导航网格,则需要源几何体。源几何体是导航网格烘焙过程中应考虑的几何体数据。二维和三维导航网格都是通过从源几何体烘焙来创建的。

2D 和 3D 版本的源几何体资源分别为 NavigationMeshSourceGeometryData2DNavigationMeshSourceGeometryData3D

源几何体可以是从视觉网格、物理碰撞或程序创建的数据阵列(如轮廓(2D)和三角面(3D))解析的几何体。为方便起见,通常直接从场景树中的节点设置解析源几何体。对于运行时导航网格(重新)烘焙,请注意几何体解析始终发生在主线程上。

备注

SceneTree不是线程安全的。从SceneTree解析源几何体只能在主线程上完成。

警告

需要从GPU接收来自视觉网格和多边形的数据,从而在此过程中停止RenderingServer。对于运行时(重新)烘焙,更喜欢使用物理形状作为解析的源几何体。

源几何图形存储在资源中,因此创建的几何图形可以重复用于多次烘焙。例如,为来自同一源几何体的不同代理大小烘焙多个导航网格。这也允许将源几何体保存到磁盘,以便稍后加载,例如避免在运行时再次解析的开销。

几何数据通常应保持非常简单。需要尽可能多的边缘,但尽可能少。尤其是在2D中,应避免重复和嵌套几何体,因为它会强制计算多边形孔,从而导致翻转多边形。嵌套几何体的示例是完全放置在另一个StaticBody2D形状的边界内的较小StaticBody2D形状。

针对大世界烘焙导航网格块

构建导航网格块

在运行时构建并更新导航网格块。

To avoid misaligned edges between different region chunks the navigation meshes have two important properties for the navigation mesh baking process. The baking bound and the border size. Together they can be used to ensure perfectly aligned edges between region chunks.

指定烘焙范围和边框大小的导航网格块

指定烘焙范围和额外边框大小的导航网格块。

The baking bound, which is an axis-aligned Rect2 for 2D and AABB for 3D, limits the used source geometry by discarding all the geometry that is outside of the bounds.

The NavigationPolygon properties baking_rect and baking_rect_offset can be used to create and place the 2D baking bound.

The NavigationMesh properties filter_baking_aabb and filter_baking_aabb_offset can be used to create and place the 3D baking bound.

With only the baking bound set another problem still exists. The resulting navigation mesh will inevitably be affected by necessary offsets like the agent_radius which makes the edges not align properly.

导航网格块之间有间隙

受到烘焙代理半径偏移的影响,导航网格块之间有明显的间隙。

This is where the border_size property for navigation mesh comes in. The border size is an inward margin from the baking bound. The important characteristic of the border size is that it is unaffected by most offsets and postprocessing like the agent_radius.

Instead of discarding source geometry, the border size discards parts of the final surface of the baked navigation mesh. If the baking bound is large enough the border size can remove the problematic surface parts so that only the intended chunk size is left.

导航网格块之间没有间隙

导航网格块互相对齐了边缘,没有间隙。

备注

The baking bounds need to be large enough to include a reasonable amount of source geometry from all the neighboring chunks.

烘焙导航网格时的常见问题

在创建或烘焙导航网格时,需要考虑一些常见的用户问题和重要的注意事项。

  • Navigation mesh baking creates frame rate problems at runtime

    默认情况下,导航网格烘焙是在后台线程上完成的,因此只要平台支持线程,实际烘焙就很少是任何性能问题的根源(假设运行时重新烘焙的几何体大小合理且复杂)。

    运行时性能问题的常见来源是涉及节点和场景树的源几何体的解析步骤。SceneTree不是线程安全的,因此所有节点都需要在主线程上解析。一些具有大量数据的节点在运行时可能非常重且解析缓慢,例如,TileMap为每个使用的单元和TileMapLayer都有一个或多个多边形要解析。持有网格的节点需要从RenderingServer请求数据,从而在此过程中停止渲染。

    为了提高性能,请使用更优化的形状,例如在详细的视觉网格上使用碰撞形状,并提前合并和简化尽可能多的几何体。如果没有任何帮助,请不要解析SceneTree并使用脚本添加源几何体过程。如果仅使用纯数据数组作为源几何体,则可以在后台线程上完成整个烘焙过程。

  • Navigation mesh 在 2D 创建 unintended holes。

    The navigation mesh baking in 2D is done by doing polygon clipping operations based on outline paths. Polygons with “holes” are a necessary evil to create more complex 2D polygons but can become unpredictable for users with many complex shapes involved.

    为避免多边形孔计算出现任何意外问题,请避免将任何轮廓嵌套在同一类型的其他轮廓内(可遍历/障碍物)。这包括来自节点的已解析形状。例如,将较小的StaticBody2D形状放置在较大的StaticBody2D形状内可能会导致所生成的多边形翻转。

  • 导航网格显示在三维几何体内部。

    3D中的导航网格烘焙没有“内部”的概念。用于光栅化几何体的体素单元被占用或未被占用。删除其他几何图形中位于地面上的几何图形。如果无法做到这一点,请在内部添加较小的“虚拟”几何体,并尽可能少地添加三角形,这样单元就会被一些东西占据。

    A NavigationObstacle3D shape set to bake with navigation mesh can be used to discard geometry as well.

NavigationObstacle3D unwanted geometry discard

A NavigationObstacle3D shape can be used to discard unwanted navigation mesh parts.

Navigation mesh script templates

以下脚本使用NavigationServer解析场景树中的源几何体,烘焙导航网格,并使用更新的导航网格更新导航区域。

2D GDScript3D GDScript

  1. extends Node2D
  2. var navigation_mesh: NavigationPolygon
  3. var source_geometry : NavigationMeshSourceGeometryData2D
  4. var callback_parsing : Callable
  5. var callback_baking : Callable
  6. var region_rid: RID
  7. func _ready() -> void:
  8. navigation_mesh = NavigationPolygon.new()
  9. navigation_mesh.agent_radius = 10.0
  10. source_geometry = NavigationMeshSourceGeometryData2D.new()
  11. callback_parsing = on_parsing_done
  12. callback_baking = on_baking_done
  13. region_rid = NavigationServer2D.region_create()
  14. # Enable the region and set it to the default navigation map.
  15. NavigationServer2D.region_set_enabled(region_rid, true)
  16. NavigationServer2D.region_set_map(region_rid, get_world_2d().get_navigation_map())
  17. # Some mega-nodes like TileMap are often not ready on the first frame.
  18. # Also the parsing needs to happen on the main-thread.
  19. # So do a deferred call to avoid common parsing issues.
  20. parse_source_geometry.call_deferred()
  21. func parse_source_geometry() -> void:
  22. source_geometry.clear()
  23. var root_node: Node2D = self
  24. # Parse the obstruction outlines from all child nodes of the root node by default.
  25. NavigationServer2D.parse_source_geometry_data(
  26. navigation_mesh,
  27. source_geometry,
  28. root_node,
  29. callback_parsing
  30. )
  31. func on_parsing_done() -> void:
  32. # If we did not parse a TileMap with navigation mesh cells we may now only
  33. # have obstruction outlines so add at least one traversable outline
  34. # so the obstructions outlines have something to "cut" into.
  35. source_geometry.add_traversable_outline(PackedVector2Array([
  36. Vector2(0.0, 0.0),
  37. Vector2(500.0, 0.0),
  38. Vector2(500.0, 500.0),
  39. Vector2(0.0, 500.0)
  40. ]))
  41. # Bake the navigation mesh on a thread with the source geometry data.
  42. NavigationServer2D.bake_from_source_geometry_data_async(
  43. navigation_mesh,
  44. source_geometry,
  45. callback_baking
  46. )
  47. func on_baking_done() -> void:
  48. # Update the region with the updated navigation mesh.
  49. NavigationServer2D.region_set_navigation_polygon(region_rid, navigation_mesh)
  1. extends Node3D
  2. var navigation_mesh: NavigationMesh
  3. var source_geometry : NavigationMeshSourceGeometryData3D
  4. var callback_parsing : Callable
  5. var callback_baking : Callable
  6. var region_rid: RID
  7. func _ready() -> void:
  8. navigation_mesh = NavigationMesh.new()
  9. navigation_mesh.agent_radius = 0.5
  10. source_geometry = NavigationMeshSourceGeometryData3D.new()
  11. callback_parsing = on_parsing_done
  12. callback_baking = on_baking_done
  13. region_rid = NavigationServer3D.region_create()
  14. # Enable the region and set it to the default navigation map.
  15. NavigationServer3D.region_set_enabled(region_rid, true)
  16. NavigationServer3D.region_set_map(region_rid, get_world_3d().get_navigation_map())
  17. # Some mega-nodes like GridMap are often not ready on the first frame.
  18. # Also the parsing needs to happen on the main-thread.
  19. # So do a deferred call to avoid common parsing issues.
  20. parse_source_geometry.call_deferred()
  21. func parse_source_geometry() -> void:
  22. source_geometry.clear()
  23. var root_node: Node3D = self
  24. # Parse the geometry from all mesh child nodes of the root node by default.
  25. NavigationServer3D.parse_source_geometry_data(
  26. navigation_mesh,
  27. source_geometry,
  28. root_node,
  29. callback_parsing
  30. )
  31. func on_parsing_done() -> void:
  32. # Bake the navigation mesh on a thread with the source geometry data.
  33. NavigationServer3D.bake_from_source_geometry_data_async(
  34. navigation_mesh,
  35. source_geometry,
  36. callback_baking
  37. )
  38. func on_baking_done() -> void:
  39. # Update the region with the updated navigation mesh.
  40. NavigationServer3D.region_set_navigation_mesh(region_rid, navigation_mesh)

The following script uses the NavigationServer to update a navigation region with procedurally generated navigation mesh data.

2D GDScript3D GDScript

  1. extends Node2D
  2. var navigation_mesh: NavigationPolygon
  3. var region_rid: RID
  4. func _ready() -> void:
  5. navigation_mesh = NavigationPolygon.new()
  6. region_rid = NavigationServer2D.region_create()
  7. # Enable the region and set it to the default navigation map.
  8. NavigationServer2D.region_set_enabled(region_rid, true)
  9. NavigationServer2D.region_set_map(region_rid, get_world_2d().get_navigation_map())
  10. # Add vertices for a convex polygon.
  11. navigation_mesh.vertices = PackedVector2Array([
  12. Vector2(0.0, 0.0),
  13. Vector2(100.0, 0.0),
  14. Vector2(100.0, 100.0),
  15. Vector2(0.0, 100.0)
  16. ])
  17. # Add indices for the polygon.
  18. navigation_mesh.add_polygon(
  19. PackedInt32Array([0, 1, 2, 3])
  20. )
  21. NavigationServer2D.region_set_navigation_polygon(region_rid, navigation_mesh)
  1. extends Node3D
  2. var navigation_mesh: NavigationMesh
  3. var region_rid: RID
  4. func _ready() -> void:
  5. navigation_mesh = NavigationMesh.new()
  6. region_rid = NavigationServer3D.region_create()
  7. # Enable the region and set it to the default navigation map.
  8. NavigationServer3D.region_set_enabled(region_rid, true)
  9. NavigationServer3D.region_set_map(region_rid, get_world_3d().get_navigation_map())
  10. # Add vertices for a convex polygon.
  11. navigation_mesh.vertices = PackedVector3Array([
  12. Vector3(-1.0, 0.0, 1.0),
  13. Vector3(1.0, 0.0, 1.0),
  14. Vector3(1.0, 0.0, -1.0),
  15. Vector3(-1.0, 0.0, -1.0),
  16. ])
  17. # Add indices for the polygon.
  18. navigation_mesh.add_polygon(
  19. PackedInt32Array([0, 1, 2, 3])
  20. )
  21. NavigationServer3D.region_set_navigation_mesh(region_rid, navigation_mesh)