3D 导航概述

Godot 提供了多种对象、类和服务器,可帮助 2D 和 3D 游戏实现基于栅格(Grid)或网格(Mesh)的导航和寻路。下文将对 Godot 中与 3D 场景导航相关的对象及其主要用途进行概述。

Godot 为 3D 导航提供了如下对象和类:

  • Astar3D

    Astar3D 对象能够在由具有权重的构成的图中查找最短路径。

    AStar3D 类最适合的是基于单元格的 3D 游戏,角色不需要到达区域中的任意位置,只需要到达预先指定的一些独立位置。

  • NavigationServer3D

    NavigationServer3D 提供了强大的服务器 API,能够在区域中查找两个位置之间的最短路径,区域使用导航网格定义。

    NavigationServer 最适合的是要求角色能够到达区域中任意位置的 3D 实时游戏,区域由导航网格定义。基于网格的导航能够轻松扩展到大型游戏世界,因为大型区域通常能够使用单一多边形定义,如果换成栅格则会需要定义许许多多的单元格。

    NavigationServer 中存放了不同的导航地图,每一张地图都由若干区块组成,区块中存放的是导航网格数据。在地图上放置代理就能够进行避障计算。与服务器通信时,使用 RID 来引用内部的地图、区块和代理。

    • NavigationServer 中可用的 RID 类型如下。

      • 导航地图 RID

        引用指定的导航地图,地图中存放的是区块和代理。地图会尝试将区块中的导航网格根据距离进行合并。每一个物理帧,地图都会同步区块和代理。

      • 导航区块 RID

        引用指定的导航区块,区块中存放的是导航网格数据。使用导航层位掩码可以对区块进行启用/禁用,限制其使用。

      • 导航链接 RID

        引用指定的导航链接,能够将两个导航网格上的位置进行连接,无视距离。

      • 导航代理 RID

        引用指定的避障代理,避障时使用的是半径值。

      • 导航障碍物 RID

        引用指定的避障障碍物,会对代理的避障速度产生影响和约束。

下列场景树节点可以辅助对 NavigationServer3D API 的使用。

  • NavigationRegion3D 节点

    存放 Navigation Mesh 资源的节点,该资源定义的是 NavigationServer3D 中的导航网格。

    • 区块可以启用/禁用。

    • 通过 navigation_layers 掩码,可以对其在寻路中的使用做进一步的限制。

    • NavigationServer3D 会根据距离将不同的导航网格合并成一个导航网格。

  • NavigationLink3D 节点

    将两个导航网格上的位置进行连接的节点,无视距离,可用于寻路。

    • 链接可以启用/禁用。

    • 链接可以设为单向或双向。

    • 通过 navigation_layers 掩码,可以对其在寻路中的使用做进一步的限制。

    链接会告诉寻路存在这样的连接、相关的消耗如何。实际的代理处理以及移动需要在自定义脚本中实现。

  • NavigationAgent3D 节点

    方便调用寻路和避障所需的常规 NavigationServer3D API 的辅助节点。该节点的父节点应该继承自 Node3D。

  • NavigationObstacle3D 节点

    可用于影响和约束启用躲避的代理的躲避速度的节点。此节点不影响代理的寻路。你需要为此更改导航网格。

3D 导航网格由以下资源定义:

  • NavigationMesh 资源

    存放 3D 导航网格数据的资源,提供了 3D 几何体的烘焙选项,既能够在编辑器中定义导航区域,也能够在运行时定义。

    • NavigationRegion3D 节点使用该资源定义其导航区域。

    • NavigationServer3D 使用该资源更新各个区块的导航网格。

    • GridMap 编辑器会在栅格单元格中存在对导航网格的定义时使用该资源。

参见

可以使用 3D 导航演示项目了解 3D 导航如何运作。

3D 场景的设置

下列步骤演示的是最小可行的 3D 导航的基础设置,使用 NavigationServer3D 和 NavigationAgent3D 进行路径移动。

  1. 在场景中添加一个 NavigationRegion3D 节点。

  2. 单击该区块节点,向该节点添加一个新的 NavigationMesh 资源。

    ../../_images/nav_3d_min_setup_step1.png

  3. 将一个新的 MeshInstance3D 节点添加为该区块节点的子节点。

  4. 选中该 MeshInstance3D 节点,添加一个新的 PlaneMesh 并将其 XY 大小设为 10。

  5. 再次选中该区块节点,点击顶栏中的“烘焙导航网格”按钮。

    ../../_images/nav_3d_min_setup_step2.png

  6. 现在就会显示出透明的导航网格,悬浮在 PlaneMesh 上方。

    ../../_images/nav_3d_min_setup_step3.png

  7. 在场景中添加一个 CharacterBody3D 节点,设置基础的碰撞形状,添加一些网格方便观察。

  8. 在该角色节点下添加一个 NavigationAgent3D 节点。

    ../../_images/nav_3d_min_setup_step4.webp

  9. 为 CharacterBody3D 节点添加一个脚本,内容如下。场景完全加载后,我们确保设置移动目标,NavigationServer 有时间进行同步。另外,添加一个 Camera3D、一些灯光以及环境,这样才能够看到东西。

GDScriptC#

  1. extends CharacterBody3D
  2. var movement_speed: float = 2.0
  3. var movement_target_position: Vector3 = Vector3(-3.0,0.0,2.0)
  4. @onready var navigation_agent: NavigationAgent3D = $NavigationAgent3D
  5. func _ready():
  6. # These values need to be adjusted for the actor's speed
  7. # and the navigation layout.
  8. navigation_agent.path_desired_distance = 0.5
  9. navigation_agent.target_desired_distance = 0.5
  10. # Make sure to not await during _ready.
  11. call_deferred("actor_setup")
  12. func actor_setup():
  13. # Wait for the first physics frame so the NavigationServer can sync.
  14. await get_tree().physics_frame
  15. # Now that the navigation map is no longer empty, set the movement target.
  16. set_movement_target(movement_target_position)
  17. func set_movement_target(movement_target: Vector3):
  18. navigation_agent.set_target_position(movement_target)
  19. func _physics_process(delta):
  20. if navigation_agent.is_navigation_finished():
  21. return
  22. var current_agent_position: Vector3 = global_position
  23. var next_path_position: Vector3 = navigation_agent.get_next_path_position()
  24. velocity = current_agent_position.direction_to(next_path_position) * movement_speed
  25. move_and_slide()
  1. using Godot;
  2. public partial class MyCharacterBody3D : CharacterBody3D
  3. {
  4. private NavigationAgent3D _navigationAgent;
  5. private float _movementSpeed = 2.0f;
  6. private Vector3 _movementTargetPosition = new Vector3(-3.0f, 0.0f, 2.0f);
  7. public Vector3 MovementTarget
  8. {
  9. get { return _navigationAgent.TargetPosition; }
  10. set { _navigationAgent.TargetPosition = value; }
  11. }
  12. public override void _Ready()
  13. {
  14. base._Ready();
  15. _navigationAgent = GetNode<NavigationAgent3D>("NavigationAgent3D");
  16. // These values need to be adjusted for the actor's speed
  17. // and the navigation layout.
  18. _navigationAgent.PathDesiredDistance = 0.5f;
  19. _navigationAgent.TargetDesiredDistance = 0.5f;
  20. // Make sure to not await during _Ready.
  21. Callable.From(ActorSetup).CallDeferred();
  22. }
  23. public override void _PhysicsProcess(double delta)
  24. {
  25. base._PhysicsProcess(delta);
  26. if (_navigationAgent.IsNavigationFinished())
  27. {
  28. return;
  29. }
  30. Vector3 currentAgentPosition = GlobalTransform.Origin;
  31. Vector3 nextPathPosition = _navigationAgent.GetNextPathPosition();
  32. Velocity = currentAgentPosition.DirectionTo(nextPathPosition) * _movementSpeed;
  33. MoveAndSlide();
  34. }
  35. private async void ActorSetup()
  36. {
  37. // Wait for the first physics frame so the NavigationServer can sync.
  38. await ToSignal(GetTree(), SceneTree.SignalName.PhysicsFrame);
  39. // Now that the navigation map is no longer empty, set the movement target.
  40. MovementTarget = _movementTargetPosition;
  41. }
  42. }

备注

第一帧的时候,NavigationServer 上的地图还没有同步区块数据,请求路径时都会返回空。在脚本中等待一帧就可以让 NavigationServer 进行同步。