使用 NavigationServer

NavigationServer 即导航服务器。2D 和 3D 版本的 NavigationServer 分别为 NavigationServer2DNavigationServer3D

2D 和 3D 使用的 NavigationServer 是一样的,NavigationServer3D 是主要服务器。NavigationServer2D 只是一个前端,会进行 2D 位置和 3D 位置的相互转换。因此,完全可以只用 NavigationServer3D 的 API 来实现 2D 导航(就是会有点繁琐)。

与 NavigationServer 通信

要使用 NavigationServer 首先就需要为请求准备参数,这个请求会被发送给 NavigationServer,用来进行更新和数据的请求。

地图、区块、代理等 NavigationServer 内部的对象使用 RID 来进行引用,这是一种用作标识符的数字。场景树中每个导航相关的节点都提供了返回该节点 RID 的函数。

线程与同步

NavigationServer 不会立即为所有更改执行更新,而是会等到物理帧的结尾再同步所有更改。

对地图、区块、代理进行更改都需要等待同步。之所以要进行同步,是因为部分更新的开销非常大,并且需要所有其他对象更新后的数据,例如重新计算整个导航地图。另外 NavigationServer 的部分功能默认会使用线程池,例如代理之间的避障计算。

大多数 get() 函数都不需要等待,因为这些函数只会从 NavigationServer 请求数据,不会进行修改。请注意,并不是所有数据都会考虑到同一帧里做出的更改。比如避障代理在当前帧更改了导航地图的话,那么 agent_get_map() 函数在同步之前就仍然会返回旧的地图。部分节点会在向 NavigationServer 发送更新前在内部存储对应的值,所以这些节点属于例外。在这种节点上使用某个值的 getter 时,如果同一帧存在更新,那么返回的就是存储在该节点上的更新后的值。

NavigationServer 是线程安全的,因为它会把所有需要执行更改的 API 调用放在队列中,在同步阶段再执行。NavigationServer 的同步发生在物理帧,在脚本以及节点的场景输入之后。

备注

划重点:大多数对 NavigationServer 的更改都会在下一个物理帧之后生效,不会立即生效。包括场景树中导航相关节点做出的更改,以及脚本做出的更改。

备注

Setter 和删除函数都需要同步。

2D 和 3D NavigationServer 的区别

NavigationServer2D 和 NavigationServer3D 在各自维度中的功能是等价的,底层使用的相同的 NavigationServer。

严格来讲,并不存在技术意义上的 NavigationServer2D。NavigationServer2D 只是 NavigationServer3D API 的前端,用于简化 Vector2(x, y)Vector3(x, 0.0, z) 之间的来回转换。2D 使用的是扁平的 3D 网格寻路,NavigationServer2D 负责进行转换工作。后续指南中提及不带 2D、3D 后缀的 NavigationServer 时,通常表示对这两种服务器均适用,只需在 Vector2(x, y)Vector3(x, 0.0, z) 之间进行转换即可。

从技术上讲,可以使用工具在一个维度上为另一个维度创建导航网格,例如,当使用平面三维源几何体时,使用3D NavigationMesh烘焙二维导航网格,或者使用NavigationRegion2D和NavigationPolygons的多边形轮廓绘制工具创建三维平面导航网格。

使用NavigationServer2D API创建的任何RID也适用于NavigationServer3D API,2D和3D回避代理都可以存在于同一地图上。

备注

在二维和三维中创建的区域将在放置在同一地图上时合并其导航网格,并应用合并条件。NavigationServer不会区分NavigationRegion2D和NavigationRegion3D节点,因为这两个节点都是服务器上的区域。默认情况下,这些节点注册在不同的导航地图上,因此只有在手动更改地图(例如使用脚本)时才能进行合并。

启用回避的Actor在放置在同一张地图上时将同时回避2D和3D回避代理。

警告

自定义的 Godot 构建如果禁用了 3D,则无法使用 NavigationServer2D。

等待同步

在游戏开始时,新场景或程序导览发生变化,对导览服务器的任何路径查询都将传回空或错误。

此时导航地图仍然为空或未更新。场景树中的所有节点都需要首先将其导航相关数据上传到NavigationServer。每个添加或更改的地图、区域或代理都需要在NavigationServer中注册。之后,NavigationServer需要**物理帧**进行同步,以更新地图、区域和代理。

一种解决方法是延迟调用自定义设置函数(这样所有节点都准备好了)。设置功能进行所有导航更改,例如添加程序性内容。之后,函数在继续路径查询之前等待下一个物理帧。

extends Node3D

func _ready():
    # Use call deferred to make sure the entire scene tree nodes are setup
    # else await on 'physics_frame' in a _ready() might get stuck.
    custom_setup.call_deferred()

func custom_setup():

    # Create a new navigation map.
    var map: RID = NavigationServer3D.map_create()
    NavigationServer3D.map_set_up(map, Vector3.UP)
    NavigationServer3D.map_set_active(map, true)

    # Create a new navigation region and add it to the map.
    var region: RID = NavigationServer3D.region_create()
    NavigationServer3D.region_set_transform(region, Transform3D())
    NavigationServer3D.region_set_map(region, map)

    # Create a procedural navigation mesh for the region.
    var new_navigation_mesh: NavigationMesh = NavigationMesh.new()
    var vertices: PackedVector3Array = PackedVector3Array([
        Vector3(0, 0, 0),
        Vector3(9.0, 0, 0),
        Vector3(0, 0, 9.0)
    ])
    new_navigation_mesh.set_vertices(vertices)
    var polygon: PackedInt32Array = PackedInt32Array([0, 1, 2])
    new_navigation_mesh.add_polygon(polygon)
    NavigationServer3D.region_set_navigation_mesh(region, new_navigation_mesh)

    # Wait for NavigationServer sync to adapt to made changes.
    await get_tree().physics_frame

    # Query the path from the navigation server.
    var start_position: Vector3 = Vector3(0.1, 0.0, 0.1)
    var target_position: Vector3 = Vector3(1.0, 0.0, 1.0)
    var optimize_path: bool = true

    var path: PackedVector3Array = NavigationServer3D.map_get_path(
        map,
        start_position,
        target_position,
        optimize_path
    )

    print("Found a path!")
    print(path)

服务器避障回调

如果 RVO 避障代理注册了避障回调,NavigationServer 会在 PhysicsServer 同步前发送对应的 velocity_computed 信号。

更多 NavigationAgent 相关的信息见 使用 NavigationAgent

使用避障的 NavigationAgent 的简化执行顺序如下:

  • 物理帧开始。

  • _physics_process(delta)

  • 设置 NavigationAgent 节点的 velocity 属性。

  • 代理向 NavigationServer 发送速度和位置。

  • NavigationServer 等待同步。

  • NavigationServer 同步并为所有注册的避障代理计算避障速度。

  • NavigationServer 通过信号为每个注册的避障代理发送安全速度向量。

  • 代理收到信号并移动父节点,例如通过 move_and_slidelinear_velocity 移动。

  • PhysicsServer 同步。

  • 物理帧结束。

因此,在回调函数中使用安全速度来移动角色物理体无论从线程还是物理的角度看都是安全的,因为相关的操作都在同一个物理帧中进行,之后 PhysicsServer 才会提交更改并进行计算。