3D 导航概述
Godot 提供了多种对象、类和服务器,可帮助 2D 和 3D 游戏实现基于栅格(Grid)或网格(Mesh)的导航和寻路。下文将对 Godot 中与 3D 场景导航相关的对象及其主要用途进行概述。
Godot 为 3D 导航提供了如下对象和类:
-
Astar3D
对象能够在由具有权重的点构成的图中查找最短路径。AStar3D 类最适合的是基于单元格的 3D 游戏,角色不需要到达区域中的任意位置,只需要到达预先指定的一些独立位置。
-
NavigationServer3D
提供了强大的服务器 API,能够在区域中查找两个位置之间的最短路径,区域使用导航网格定义。NavigationServer 最适合的是要求角色能够到达区域中任意位置的 3D 实时游戏,区域由导航网格定义。基于网格的导航能够轻松扩展到大型游戏世界,因为大型区域通常能够使用单一多边形定义,如果换成栅格则会需要定义许许多多的单元格。
NavigationServer 中存放了不同的导航地图,每一张地图都由若干区块组成,区块中存放的是导航网格数据。在地图上放置代理就能够进行避障计算。与服务器通信时,使用 RID 来引用内部的地图、区块和代理。
NavigationServer 中可用的 RID 类型如下。
导航地图 RID
引用指定的导航地图,地图中存放的是区块和代理。地图会尝试将区块中的导航网格根据距离进行合并。每一个物理帧,地图都会同步区块和代理。
导航区块 RID
引用指定的导航区块,区块中存放的是导航网格数据。使用导航层位掩码可以对区块进行启用/禁用,限制其使用。
导航链接 RID
引用指定的导航链接,能够将两个导航网格上的位置进行连接,无视距离。
导航代理 RID
引用指定的避障代理,避障时使用的是半径值。
导航障碍物 RID
引用指定的避障障碍物,会对代理的避障速度产生影响和约束。
下列场景树节点可以辅助对 NavigationServer3D API 的使用。
-
存放 Navigation Mesh 资源的节点,该资源定义的是 NavigationServer3D 中的导航网格。
区块可以启用/禁用。
通过
navigation_layers
掩码,可以对其在寻路中的使用做进一步的限制。NavigationServer3D 会根据距离将不同的导航网格合并成一个导航网格。
-
将两个导航网格上的位置进行连接的节点,无视距离,可用于寻路。
链接可以启用/禁用。
链接可以设为单向或双向。
通过
navigation_layers
掩码,可以对其在寻路中的使用做进一步的限制。
链接会告诉寻路存在这样的连接、相关的消耗如何。实际的代理处理以及移动需要在自定义脚本中实现。
-
方便调用寻路和避障所需的常规 NavigationServer3D API 的辅助节点。该节点的父节点应该继承自 Node3D。
-
可用于影响和约束启用躲避的代理的躲避速度的节点。此节点不影响代理的寻路。你需要为此更改导航网格。
3D 导航网格由以下资源定义:
-
存放 3D 导航网格数据的资源,提供了 3D 几何体的烘焙选项,既能够在编辑器中定义导航区域,也能够在运行时定义。
NavigationRegion3D 节点使用该资源定义其导航区域。
NavigationServer3D 使用该资源更新各个区块的导航网格。
GridMap 编辑器会在栅格单元格中存在对导航网格的定义时使用该资源。
参见
可以使用 3D 导航演示项目了解 3D 导航如何运作。
3D 场景的设置
下列步骤演示的是最小可行的 3D 导航的基础设置,使用 NavigationServer3D 和 NavigationAgent3D 进行路径移动。
在场景中添加一个 NavigationRegion3D 节点。
单击该区块节点,向该节点添加一个新的 NavigationMesh 资源。
将一个新的 MeshInstance3D 节点添加为该区块节点的子节点。
选中该 MeshInstance3D 节点,添加一个新的 PlaneMesh 并将其 XY 大小设为 10。
再次选中该区块节点,点击顶栏中的“烘焙导航网格”按钮。
现在就会显示出透明的导航网格,悬浮在 PlaneMesh 上方。
在场景中添加一个 CharacterBody3D 节点,设置基础的碰撞形状,添加一些网格方便观察。
在该角色节点下添加一个 NavigationAgent3D 节点。
- 为 CharacterBody3D 节点添加一个脚本,内容如下。场景完全加载后,我们确保设置移动目标,NavigationServer 有时间进行同步。另外,添加一个 Camera3D、一些灯光以及环境,这样才能够看到东西。
GDScriptC#
extends CharacterBody3D
var movement_speed: float = 2.0
var movement_target_position: Vector3 = Vector3(-3.0,0.0,2.0)
@onready var navigation_agent: NavigationAgent3D = $NavigationAgent3D
func _ready():
# These values need to be adjusted for the actor's speed
# and the navigation layout.
navigation_agent.path_desired_distance = 0.5
navigation_agent.target_desired_distance = 0.5
# Make sure to not await during _ready.
call_deferred("actor_setup")
func actor_setup():
# Wait for the first physics frame so the NavigationServer can sync.
await get_tree().physics_frame
# Now that the navigation map is no longer empty, set the movement target.
set_movement_target(movement_target_position)
func set_movement_target(movement_target: Vector3):
navigation_agent.set_target_position(movement_target)
func _physics_process(delta):
if navigation_agent.is_navigation_finished():
return
var current_agent_position: Vector3 = global_position
var next_path_position: Vector3 = navigation_agent.get_next_path_position()
velocity = current_agent_position.direction_to(next_path_position) * movement_speed
move_and_slide()
using Godot;
public partial class MyCharacterBody3D : CharacterBody3D
{
private NavigationAgent3D _navigationAgent;
private float _movementSpeed = 2.0f;
private Vector3 _movementTargetPosition = new Vector3(-3.0f, 0.0f, 2.0f);
public Vector3 MovementTarget
{
get { return _navigationAgent.TargetPosition; }
set { _navigationAgent.TargetPosition = value; }
}
public override void _Ready()
{
base._Ready();
_navigationAgent = GetNode<NavigationAgent3D>("NavigationAgent3D");
// These values need to be adjusted for the actor's speed
// and the navigation layout.
_navigationAgent.PathDesiredDistance = 0.5f;
_navigationAgent.TargetDesiredDistance = 0.5f;
// Make sure to not await during _Ready.
Callable.From(ActorSetup).CallDeferred();
}
public override void _PhysicsProcess(double delta)
{
base._PhysicsProcess(delta);
if (_navigationAgent.IsNavigationFinished())
{
return;
}
Vector3 currentAgentPosition = GlobalTransform.Origin;
Vector3 nextPathPosition = _navigationAgent.GetNextPathPosition();
Velocity = currentAgentPosition.DirectionTo(nextPathPosition) * _movementSpeed;
MoveAndSlide();
}
private async void ActorSetup()
{
// Wait for the first physics frame so the NavigationServer can sync.
await ToSignal(GetTree(), SceneTree.SignalName.PhysicsFrame);
// Now that the navigation map is no longer empty, set the movement target.
MovementTarget = _movementTargetPosition;
}
}
备注
第一帧的时候,NavigationServer 上的地图还没有同步区块数据,请求路径时都会返回空。在脚本中等待一帧就可以让 NavigationServer 进行同步。