使用 NavigationAgent

NavigationAgent 即导航代理,是一种辅助节点,能够为继承自 Node2D/3D 的父节点提供寻路、路径跟随、代理躲避等功能。这类节点会代替父级角色节点对 NavigationServer API 进行常见的调用,针对初学者进行了优化。

2D 和 3D 版本的 NavigationAgent 分别是 NavigationAgent2DNavigationAgent3D

新建的 NavigationAgent 节点会自动加入 World2D/World3D 的默认导航地图。

NavigationsAgent 节点是可选的,不是使用导航系统的硬性要求。对应的功能都可以用脚本代替,替换为对 NavigationServer API 的直接调用。

NavigationAgent 寻路

target_position 设置为全局位置时,NavigationAgent会在其当前导航地图上查询新的导航路径。

匀速遍历, 然后, 可以用下面的伪代码。

  • navigation_layers 位元遮罩可用于限制代理可以使用的导航网格。

  • pathfinding_algorithm 控制路径搜索中路径查找如何通过导航网格多边形。

  • path_postprocessing 设置在路径查找找到的原始路径走廊返回之前是否或如何更改。

  • path_metadata_flags 允许收集路径返回的附加路径点元资料。

警告

禁用路径元旗标将禁用代理上的相关信号发射。

NavigationAgent 路径跟随

为代理程序设定 target_position 后,可以使用 get_next_path_position() 函式来撷取路径中要遵循的下一个位置。

收到下一个路径位置,使用你自己的移动代码将代理的父参与者节点移到此路径位置。

备注

导览系统永远不会移动NavigationAgent的父节点。该动作完全掌握在使用者及其自定义脚本的手中。

NavigationAgent有自己的内部逻辑来处理当前路径并调用更新。

The get_next_path_position() function is responsible for updating many of the agent’s internal states and properties. The function should be repeatedly called once every physics_process until is_navigation_finished() tells that the path is finished. The function should not be called after the target position or path end has been reached as it can make the agent jitter in place due to the repeated path updates. Always check very early in script with is_navigation_finished() if the path is already finished.

这个节点有如下属性可供设置。

  • path_desired_distance 定义了代理将其内部路径索引前进到下一个路径位置的距离。

  • target_desired_distance 定义了代理认为要到达的目标位置及其末端的路径的距离。

  • path_max_distance 定义代理何时请求新路径,原因是它移动得离当前路径点段太远。

当在 _physics_process() 中调用 get_next_path_position() 函数时,所有重要的更新都会被触发。

NavigationAgent可以与 process 一起使用,但仍限于在 physics_process 中发生的单个更新。

以下是NavigationAgent常用的各种节点的脚本示例。

运行以下命令

在编写代理移动脚本时,需要考虑一些常见的用户问题和重要的注意事项。

  • 路径返回为空

    如果代理在导航地图同步之前查询路径,例如在 _ready() 函数中,路径可能返回空。在这种情况下, get_next_path_position() 函数将返回与代理父节点相同的位置,并且代理将考虑到达的路径末端。这是通过进行延迟调用或使用回调来解决的,例如等待导航地图更改信号。

  • 代理在两个位置跳跃

    这通常是由于每个影格非常频繁的路径更新造成的,无论是有意还是无意(例如,最大路径距离设定得太短)。寻路需要找到导览网格上有效的最近位置。如果每个影格都请求新路径,则第一个路径位置可能最终会在代理目前位置的前后不断切换,导致其在两个位置之间跳跃。

  • 代理有时会回溯

    如果代理程序移动得非常快,它可能会超出path_desired_distance检查,而不会推进路径索引。这可能导致代理回溯到它后面的路径点,直到它通过距离检查以增加路径索引。根据代理速度和更新速率相应地增加所需距离通常可以解决此问题,并且可以使用更平衡的导览网格多边形布局,而不会在小空间内挤在一起太多的多边形边缘。

  • 代理有时会向后寻找影格

    与代理在两个位置之间卡住的跳跃相同,这通常是由每个影格非常频繁的路径更新引起的。根据你的导览网格布局,尤其是当代理直接放置在导览网格边缘或边缘连接上时,预计路径位置有时会稍微 “behind” (落后)角色目前方向。这种情况的发生是由于精度问题,并且并非总是能够避免。如果演员立即旋转以面向目前路径位置,这通常只是一个可见的问题。

NavigationAgent 避障

本节解释了如何使用 NavigationAgent 的导航避障功能。

要让 NavigationAgent 使用避障功能,必须将 enable_avoidance 属性设置为 true

../../_images/agent_avoidance_enabled.png

必须连接 NavigationAgent 节点的 velocity_computed 信号,接收安全速度的计算结果。

../../_images/agent_safevelocity_signal.png

Set the velocity of the NavigationAgent node in _physics_process() to update the agent with the current velocity of the agent’s parent node.

只要代理开启了避障,就可以在每个物理帧通过 velocity_computed 信号收到 safe_velocity 向量。应该使用这个速度向量来移动 NavigationAgent 的父节点,这样就能够避免撞到其他使用了避障的代理以及避障障碍物。

备注

计算避障时只会考虑位于同一张地图中其他注册了避障的代理。

NavigationAgent 中与避障相关的属性如下:

  • 属性 “height” 仅在三维中可用。高度与代理的当前全局y轴位置一起决定了代理在回避模拟中的垂直位置。使用2D回避的代理将自动忽略其下方或上方的其他代理或障碍物。

  • 属性 radius 控制迴避圆的大小,或者在3D球体的情况下,控制代理周围的大小。该区域描述的是代理的身体,而不是躲避机动距离。

  • 当搜索应避免的其他代理时,属性 neighbor_distance 控制代理的搜索半径。较低的值可降低处理成本。

  • 属性 max_neighbors 控制在避免计算中考虑多少其他代理(如果它们都具有重叠半径)。较低的值降低了处理成本,但过低的值可能导致代理忽略避免。

  • 属性 time_horizon_agentstime_horizon_obstacles 控制其他代理或障碍物的回避预测时间(以秒为单位)。当特工计算他们的安全速度时,他们选择的速度可以保持这一秒,而不会与另一个躲避物体碰撞。预测时间应尽可能低,因为代理会减慢速度以避免在该时间段内发生碰撞。

  • 属性 max_speed 控制代理回避计算所允许的最大速度。如果代理父母移动得比这个值快,则避免 safe_velocity 可能不够准确,无法避免碰撞。

  • 属性 use_3d_avoidance 在下一次更新时在2D回避(xz轴)和3d回避(xyz轴)之间切换代理。请注意,2D回避和3D回避在单独的回避模拟中运行,因此在它们之间划分的代理不会相互影响。

  • 属性 avoidance_layersavoidance_mask 是类似于例如物理层的位掩码。代理将仅避开位于与其自己的回避掩码位中的至少一个相匹配的回避层上的其他回避对象。

  • avoidance_priority 使优先级较高的代理忽略优先级较低的代理。这可以用于在避免模拟中赋予某些代理更大的重要性,例如重要的npc字符,而不必不断改变其整个避免层或掩码。

回避存在于其自身的空间中,并且没有来自导航网格或物理碰撞的信息。场景背后的回避代理只是平面2D平面上具有不同半径的圆或其他空的3D空间中的球体。导航障碍物可用于将一些环境约束添加到规避模拟中,请参见 使用 NavigationObstacle

备注

回避不会影响寻路。它应该被视为一个额外的选项,用于不断移动无法有效地(重新)烘焙到导航网格中以在其周围移动的对象。

备注

RVO avoidance makes implicit assumptions about natural agent behavior. E.g. that agents move on reasonable passing sides that can be assigned when they encounter each other. This means that very clinical avoidance test scenarios will commonly fail. E.g. agents moved directly against each other with perfect opposite velocities will fail because the agents can not get their passing sides assigned.

使用NavigationAgent enable_avoidance 属性是切换回避的首选选项。以下代码段可用于在代理上切换回避、创建或删除回避回调或切换回避模式。

GDScript

  1. extends NavigationAgent2D
  2. var agent: RID = get_rid()
  3. # Enable avoidance
  4. NavigationServer2D.agent_set_avoidance_enabled(agent, true)
  5. # Create avoidance callback
  6. NavigationServer2D.agent_set_avoidance_callback(agent, Callable(self, "_avoidance_done"))
  7. # Disable avoidance
  8. NavigationServer2D.agent_set_avoidance_enabled(agent, false)
  9. # Delete avoidance callback
  10. NavigationServer2D.agent_set_avoidance_callback(agent, Callable())

GDScript

  1. extends NavigationAgent3D
  2. var agent: RID = get_rid()
  3. # Enable avoidance
  4. NavigationServer3D.agent_set_avoidance_enabled(agent, true)
  5. # Create avoidance callback
  6. NavigationServer3D.agent_set_avoidance_callback(agent, Callable(self, "_avoidance_done"))
  7. # Switch to 3D avoidance
  8. NavigationServer3D.agent_set_use_3d_avoidance(agent, true)
  9. # Disable avoidance
  10. NavigationServer3D.agent_set_avoidance_enabled(agent, false)
  11. # Delete avoidance callback
  12. NavigationServer3D.agent_set_avoidance_callback(agent, Callable())
  13. # Switch to 2D avoidance
  14. NavigationServer3D.agent_set_use_3d_avoidance(agent, false)

NavigationAgent 脚本模板

以下部分提供了NavigationAgents常用节点的脚本模板。

角色为 Node3D

This script adds basic navigation movement to a Node3D with a NavigationAgent3D child node.

GDScript

  1. extends Node3D
  2. @export var movement_speed: float = 4.0
  3. @onready var navigation_agent: NavigationAgent3D = get_node("NavigationAgent3D")
  4. var movement_delta: float
  5. func _ready() -> void:
  6. navigation_agent.velocity_computed.connect(Callable(_on_velocity_computed))
  7. func set_movement_target(movement_target: Vector3):
  8. navigation_agent.set_target_position(movement_target)
  9. func _physics_process(delta):
  10. if navigation_agent.is_navigation_finished():
  11. return
  12. movement_delta = movement_speed * delta
  13. var next_path_position: Vector3 = navigation_agent.get_next_path_position()
  14. var new_velocity: Vector3 = global_position.direction_to(next_path_position) * movement_delta
  15. if navigation_agent.avoidance_enabled:
  16. navigation_agent.velocity = new_velocity
  17. else:
  18. _on_velocity_computed(new_velocity)
  19. func _on_velocity_computed(safe_velocity: Vector3) -> void:
  20. global_position = global_position.move_toward(global_position + safe_velocity, movement_delta)

角色为 CharacterBody3D

This script adds basic navigation movement to a CharacterBody3D with a NavigationAgent3D child node.

GDScript

  1. extends CharacterBody3D
  2. @export var movement_speed: float = 4.0
  3. @onready var navigation_agent: NavigationAgent3D = get_node("NavigationAgent3D")
  4. func _ready() -> void:
  5. navigation_agent.velocity_computed.connect(Callable(_on_velocity_computed))
  6. func set_movement_target(movement_target: Vector3):
  7. navigation_agent.set_target_position(movement_target)
  8. func _physics_process(delta):
  9. if navigation_agent.is_navigation_finished():
  10. return
  11. var next_path_position: Vector3 = navigation_agent.get_next_path_position()
  12. var new_velocity: Vector3 = global_position.direction_to(next_path_position) * movement_speed
  13. if navigation_agent.avoidance_enabled:
  14. navigation_agent.velocity = new_velocity
  15. else:
  16. _on_velocity_computed(new_velocity)
  17. func _on_velocity_computed(safe_velocity: Vector3):
  18. velocity = safe_velocity
  19. move_and_slide()

角色为 RigidBody3D

This script adds basic navigation movement to a RigidBody3D with a NavigationAgent3D child node.

GDScript

  1. extends RigidBody3D
  2. @export var movement_speed: float = 4.0
  3. @onready var navigation_agent: NavigationAgent3D = get_node("NavigationAgent3D")
  4. func _ready() -> void:
  5. navigation_agent.velocity_computed.connect(Callable(_on_velocity_computed))
  6. func set_movement_target(movement_target: Vector3):
  7. navigation_agent.set_target_position(movement_target)
  8. func _physics_process(delta):
  9. if navigation_agent.is_navigation_finished():
  10. return
  11. var next_path_position: Vector3 = navigation_agent.get_next_path_position()
  12. var new_velocity: Vector3 = global_position.direction_to(next_path_position) * movement_speed
  13. if navigation_agent.avoidance_enabled:
  14. navigation_agent.velocity = new_velocity
  15. else:
  16. _on_velocity_computed(new_velocity)
  17. func _on_velocity_computed(safe_velocity: Vector3):
  18. linear_velocity = safe_velocity