Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
使用 NavigationAgent
NavigationAgent 即导航代理,是一种辅助节点,能够为继承自 Node2D/3D 的父节点提供寻路、路径跟随、代理躲避等功能。这类节点会代替父级角色节点对 NavigationServer API 进行常见的调用,针对初学者提供便利。
2D 和 3D 版本的 NavigationAgent 分别是 NavigationAgent2D 和 NavigationAgent3D。
新建的 NavigationAgent 节点会自动加入 World2D/World3D 的默认导航地图。
NavigationAgent 节点是可选的,不是使用导航系统的硬性要求。它的所有功能都可以用脚本代替,替换为对 NavigationServer API 的直接调用。
小技巧
有关更高级的用法,比起 NavigationAgent 可改用使用 NavigationPathQueryObjects。
NavigationAgent 寻路
当 target_position 设置为全局位置时,NavigationAgent 会在其当前导航地图上查询新的导航路径。
可以通过以下属性影响寻路结果。
navigation_layers位掩码可用于限制代理能够使用的导航网格。pathfinding_algorithm控制路径搜索中路径查找通过导航网格多边形的方式。path_postprocessing设置在路径查找得到的原始路径走廊返回之前是否或如何更改。path_metadata_flags允许收集路径返回的附加路径点元数据。simplify_path和simplify_epsilon属性可用于从路径中移除不太关键的点。
警告
禁用路径元标志将禁用代理上的相关信号发射。
NavigationAgent 路径跟随
为代理设定 target_position 后,可以使用 get_next_path_position() 函数来获取路径中要跟随的下一个位置。
收到下一个路径位置后,使用你自己的移动代码将代理的父角色节点移到此路径位置。
备注
导航系统永远不会移动 NavigationAgent 的父节点。该动作完全掌握在用户及其自定义脚本的手中。
NavigationAgent 有自己的内部逻辑来处理当前路径并调用更新。
get_next_path_position() 函数负责更新代理的许多内部状态和属性。你需要在每个 physics_process 中都调用该函数,且仅调用一次,直到 is_navigation_finished() 的返回值显示该路径已完成。到达目标位置或路径末端后,就不应再调用该函数了,否则将反复更新路径,可能导致代理在原地抖动。务必要尽早在脚本中使用 is_navigation_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 检查的范围,而不会推进路径索引。这可能导致代理回溯到它后方的路径点,直到它通过距离检查以增加路径索引。根据代理速度和更新速率相应地增加所需距离通常可以解决此问题,还可以使用更平衡的导航网格多边形布局,避免在小空间内有太多的多边形边缘挤在一起。
- 代理有时会回头看一帧
与代理卡在两个位置之间不断跳跃的情况相同,这通常是过于频繁地每帧更新路径造成的。根据导航网格的不同布局,特别是当代理是被直接放置在导航网格的边缘或边缘连接上时,路径位置有时会稍微“落后”于角色的朝向。这是精度问题导致的,有时难以避免。此现象通常只有在角色被瞬间旋转到面向当前路径位置时,才会造成肉眼可见的问题。
NavigationAgent 避障
本节解释了如何使用 NavigationAgent 的导航避障功能。
要让 NavigationAgent 使用避障功能,必须将 avoidance_enabled 属性设置为 true。
必须连接 NavigationAgent 节点的 velocity_computed 信号,接收安全速度的计算结果。
请在 _physics_process() 中设置 NavigationAgent 节点的 velocity,利用代理父节点的当前速度来更新代理。
只要代理开启了避障,就可以在每个物理帧通过 velocity_computed 信号收到 safe_velocity 向量。应该使用这个速度向量来移动 NavigationAgent 的父节点,这样就能够避免撞到其他使用了避障的代理以及避障障碍物。
备注
计算避障时只会考虑位于同一张地图中其他注册了避障的代理。
备注
NavigationAgent 必须 提供一个 target_position (目标位置)属性,哪怕你只是单纯想用这个代理来做避障。否则的话,从 velocity_computed 信号里获取到的 safe_velocity (安全速度)永远都会是一个零向量(也就是没有任何速度)。
NavigationAgent 中与避障相关的属性如下:
属性
height仅在 3D 中可用。高度与代理的当前全局 y 轴位置一起决定了代理在避障模拟中的垂直位置。使用 2D 避障的代理将自动忽略其下方或上方的其他代理或障碍物。属性
radius控制避障圆的大小,或者 3D 的情况下,代理周围避障球体的大小。该区域描述的是代理的身体,而不是躲避机动距离。当搜索应回避的其他代理时,属性
neighbor_distance控制代理的搜索半径。较低的值可降低处理成本。属性
max_neighbors控制在避障计算中最多考虑多少其他代理(如果它们都具有重叠半径)。较低的值可以降低处理成本,但过低的值可能导致代理忽略避障。属性
time_horizon_agents和time_horizon_obstacles控制对于其他代理或障碍物的回避预测时间(以秒为单位)。代理计算安全速度时,选择的速度可以维持这么多秒,而不会与另一个避障物体碰撞。预测时间应尽可能低,因为代理会减慢速度以避免在该时间段内发生碰撞。属性
max_speed控制代理回避计算所允许的最大速度。如果代理父节点移动得比这个值快,则避障的safe_velocity可能不够准确,无法避免碰撞。属性
use_3d_avoidance在下一次更新时在 2D 避障(xz 轴)和 3D 避障(xyz 轴)之间切换代理。请注意,2D 避障和 3D 避障在单独的避障模拟中运行,因此它们的代理不会相互影响。属性
avoidance_layers和avoidance_mask是类似于物理层的位掩码。代理将仅避开位于与其自己的避障掩码位中的至少一个相匹配的避障层上的其他避障对象。
avoidance_priority使优先级较高的代理忽略优先级较低的代理。这可以用于在避障模拟中赋予某些代理更大的重要性(例如重要的非玩家角色),而不必不断改变其整个避障层或掩码。
避障存在于其自身的空间中,没有来自导航网格或物理碰撞的信息。在幕后,避障代理只是平坦 2D 平面上具有不同半径的圆,或空白 3D 空间中的球体。NavigationObstacles 可用于将一些环境约束添加到避障模拟中,请参见使用 NavigationObstacle。
备注
避障不会影响寻路。它应该被视为一个额外的选项,用于避开不断移动、无法高效地(重新)烘焙到导航网格中的对象。
备注
RVO 避障算法对代理的自然行为做出了隐含假设。例如,代理相遇时能够合理地决定在某一侧通行。这意味着非常严格的避障测试场景通常会失败。例如,两个以完全相反速度直接面对面移动的代理会避障失败,因为代理无法决定在哪一侧通行。
使用 NavigationAgent avoidance_enabled 属性是切换避障的首选选项。以下代码段可用于在代理上切换避障、创建或删除避障回调或切换避障模式。
extends NavigationAgent2D
func _ready() -> void:
var agent: RID = get_rid()
# Enable avoidance
NavigationServer2D.agent_set_avoidance_enabled(agent, true)
# Create avoidance callback
NavigationServer2D.agent_set_avoidance_callback(agent, Callable(self, "_avoidance_done"))
# Disable avoidance
NavigationServer2D.agent_set_avoidance_enabled(agent, false)
# Delete avoidance callback
NavigationServer2D.agent_set_avoidance_callback(agent, Callable())
using Godot;
public partial class MyNavigationAgent2D : NavigationAgent2D
{
public override void _Ready()
{
Rid agent = GetRid();
// Enable avoidance
NavigationServer2D.AgentSetAvoidanceEnabled(agent, true);
// Create avoidance callback
NavigationServer2D.AgentSetAvoidanceCallback(agent, Callable.From(AvoidanceDone));
// Disable avoidance
NavigationServer2D.AgentSetAvoidanceEnabled(agent, false);
//Delete avoidance callback
NavigationServer2D.AgentSetAvoidanceCallback(agent, default);
}
private void AvoidanceDone() { }
}
extends NavigationAgent3D
func _ready() -> void:
var agent: RID = get_rid()
# Enable avoidance
NavigationServer3D.agent_set_avoidance_enabled(agent, true)
# Create avoidance callback
NavigationServer3D.agent_set_avoidance_callback(agent, Callable(self, "_avoidance_done"))
# Switch to 3D avoidance
NavigationServer3D.agent_set_use_3d_avoidance(agent, true)
# Disable avoidance
NavigationServer3D.agent_set_avoidance_enabled(agent, false)
# Delete avoidance callback
NavigationServer3D.agent_set_avoidance_callback(agent, Callable())
# Switch to 2D avoidance
NavigationServer3D.agent_set_use_3d_avoidance(agent, false)
using Godot;
public partial class MyNavigationAgent3D : NavigationAgent3D
{
public override void _Ready()
{
Rid agent = GetRid();
// Enable avoidance
NavigationServer3D.AgentSetAvoidanceEnabled(agent, true);
// Create avoidance callback
NavigationServer3D.AgentSetAvoidanceCallback(agent, Callable.From(AvoidanceDone));
// Switch to 3D avoidance
NavigationServer3D.AgentSetUse3DAvoidance(agent, true);
// Disable avoidance
NavigationServer3D.AgentSetAvoidanceEnabled(agent, false);
//Delete avoidance callback
NavigationServer3D.AgentSetAvoidanceCallback(agent, default);
// Switch to 2D avoidance
NavigationServer3D.AgentSetUse3DAvoidance(agent, false);
}
private void AvoidanceDone() { }
}
NavigationAgent 脚本模板
以下部分提供了 NavigationAgent 常用节点的脚本模板。
extends Node2D
@export var movement_speed: float = 4.0
@onready var navigation_agent: NavigationAgent2D = get_node("NavigationAgent2D")
var movement_delta: float
func _ready() -> void:
navigation_agent.velocity_computed.connect(Callable(_on_velocity_computed))
func set_movement_target(movement_target: Vector2):
navigation_agent.set_target_position(movement_target)
func _physics_process(delta):
# Do not query when the map has never synchronized and is empty.
if NavigationServer2D.map_get_iteration_id(navigation_agent.get_navigation_map()) == 0:
return
if navigation_agent.is_navigation_finished():
return
movement_delta = movement_speed * delta
var next_path_position: Vector2 = navigation_agent.get_next_path_position()
var new_velocity: Vector2 = global_position.direction_to(next_path_position) * movement_delta
if navigation_agent.avoidance_enabled:
navigation_agent.set_velocity(new_velocity)
else:
_on_velocity_computed(new_velocity)
func _on_velocity_computed(safe_velocity: Vector2) -> void:
global_position = global_position.move_toward(global_position + safe_velocity, movement_delta)
extends CharacterBody2D
@export var movement_speed: float = 4.0
@onready var navigation_agent: NavigationAgent2D = get_node("NavigationAgent2D")
func _ready() -> void:
navigation_agent.velocity_computed.connect(Callable(_on_velocity_computed))
func set_movement_target(movement_target: Vector2):
navigation_agent.set_target_position(movement_target)
func _physics_process(delta):
# Do not query when the map has never synchronized and is empty.
if NavigationServer2D.map_get_iteration_id(navigation_agent.get_navigation_map()) == 0:
return
if navigation_agent.is_navigation_finished():
return
var next_path_position: Vector2 = navigation_agent.get_next_path_position()
var new_velocity: Vector2 = global_position.direction_to(next_path_position) * movement_speed
if navigation_agent.avoidance_enabled:
navigation_agent.set_velocity(new_velocity)
else:
_on_velocity_computed(new_velocity)
func _on_velocity_computed(safe_velocity: Vector2):
velocity = safe_velocity
move_and_slide()
extends RigidBody2D
@export var movement_speed: float = 4.0
@onready var navigation_agent: NavigationAgent2D = get_node("NavigationAgent2D")
func _ready() -> void:
navigation_agent.velocity_computed.connect(Callable(_on_velocity_computed))
func set_movement_target(movement_target: Vector2):
navigation_agent.set_target_position(movement_target)
func _physics_process(delta):
# Do not query when the map has never synchronized and is empty.
if NavigationServer2D.map_get_iteration_id(navigation_agent.get_navigation_map()) == 0:
return
if navigation_agent.is_navigation_finished():
return
var next_path_position: Vector2 = navigation_agent.get_next_path_position()
var new_velocity: Vector2 = global_position.direction_to(next_path_position) * movement_speed
if navigation_agent.avoidance_enabled:
navigation_agent.set_velocity(new_velocity)
else:
_on_velocity_computed(new_velocity)
func _on_velocity_computed(safe_velocity: Vector2):
linear_velocity = safe_velocity
using Godot;
public partial class MyNode2D : Node2D
{
[Export]
public float MovementSpeed { get; set; } = 4.0f;
NavigationAgent2D _navigationAgent;
private float _movementDelta;
public override void _Ready()
{
_navigationAgent = GetNode<NavigationAgent2D>("NavigationAgent2D");
_navigationAgent.VelocityComputed += OnVelocityComputed;
}
private void SetMovementTarget(Vector2 movementTarget)
{
_navigationAgent.TargetPosition = movementTarget;
}
public override void _PhysicsProcess(double delta)
{
// Do not query when the map has never synchronized and is empty.
if (NavigationServer2D.MapGetIterationId(_navigationAgent.GetNavigationMap()) == 0)
{
return;
}
if (_navigationAgent.IsNavigationFinished())
{
return;
}
_movementDelta = MovementSpeed * (float)delta;
Vector2 nextPathPosition = _navigationAgent.GetNextPathPosition();
Vector2 newVelocity = GlobalPosition.DirectionTo(nextPathPosition) * _movementDelta;
if (_navigationAgent.AvoidanceEnabled)
{
_navigationAgent.Velocity = newVelocity;
}
else
{
OnVelocityComputed(newVelocity);
}
}
private void OnVelocityComputed(Vector2 safeVelocity)
{
GlobalPosition = GlobalPosition.MoveToward(GlobalPosition + safeVelocity, _movementDelta);
}
}
using Godot;
public partial class MyCharacterBody2D : CharacterBody2D
{
[Export]
public float MovementSpeed { get; set; } = 4.0f;
NavigationAgent2D _navigationAgent;
public override void _Ready()
{
_navigationAgent = GetNode<NavigationAgent2D>("NavigationAgent2D");
_navigationAgent.VelocityComputed += OnVelocityComputed;
}
private void SetMovementTarget(Vector2 movementTarget)
{
_navigationAgent.TargetPosition = movementTarget;
}
public override void _PhysicsProcess(double delta)
{
// Do not query when the map has never synchronized and is empty.
if (NavigationServer2D.MapGetIterationId(_navigationAgent.GetNavigationMap()) == 0)
{
return;
}
if (_navigationAgent.IsNavigationFinished())
{
return;
}
Vector2 nextPathPosition = _navigationAgent.GetNextPathPosition();
Vector2 newVelocity = GlobalPosition.DirectionTo(nextPathPosition) * MovementSpeed;
if (_navigationAgent.AvoidanceEnabled)
{
_navigationAgent.Velocity = newVelocity;
}
else
{
OnVelocityComputed(newVelocity);
}
}
private void OnVelocityComputed(Vector2 safeVelocity)
{
Velocity = safeVelocity;
MoveAndSlide();
}
}
using Godot;
public partial class MyRigidBody2D : RigidBody2D
{
[Export]
public float MovementSpeed { get; set; } = 4.0f;
NavigationAgent2D _navigationAgent;
public override void _Ready()
{
_navigationAgent = GetNode<NavigationAgent2D>("NavigationAgent2D");
_navigationAgent.VelocityComputed += OnVelocityComputed;
}
private void SetMovementTarget(Vector2 movementTarget)
{
_navigationAgent.TargetPosition = movementTarget;
}
public override void _PhysicsProcess(double delta)
{
// Do not query when the map has never synchronized and is empty.
if (NavigationServer2D.MapGetIterationId(_navigationAgent.GetNavigationMap()) == 0)
{
return;
}
if (_navigationAgent.IsNavigationFinished())
{
return;
}
Vector2 nextPathPosition = _navigationAgent.GetNextPathPosition();
Vector2 newVelocity = GlobalPosition.DirectionTo(nextPathPosition) * MovementSpeed;
if (_navigationAgent.AvoidanceEnabled)
{
_navigationAgent.Velocity = newVelocity;
}
else
{
OnVelocityComputed(newVelocity);
}
}
private void OnVelocityComputed(Vector2 safeVelocity)
{
LinearVelocity = safeVelocity;
}
}
extends Node3D
@export var movement_speed: float = 4.0
@onready var navigation_agent: NavigationAgent3D = get_node("NavigationAgent3D")
var physics_delta: float
func _ready() -> void:
navigation_agent.velocity_computed.connect(Callable(_on_velocity_computed))
func set_movement_target(movement_target: Vector3):
navigation_agent.set_target_position(movement_target)
func _physics_process(delta):
# Save the delta for use in _on_velocity_computed.
physics_delta = delta
# Do not query when the map has never synchronized and is empty.
if NavigationServer3D.map_get_iteration_id(navigation_agent.get_navigation_map()) == 0:
return
if navigation_agent.is_navigation_finished():
return
var next_path_position: Vector3 = navigation_agent.get_next_path_position()
var new_velocity: Vector3 = global_position.direction_to(next_path_position) * movement_speed
if navigation_agent.avoidance_enabled:
navigation_agent.set_velocity(new_velocity)
else:
_on_velocity_computed(new_velocity)
func _on_velocity_computed(safe_velocity: Vector3) -> void:
global_position = global_position.move_toward(global_position + safe_velocity, physics_delta * movement_speed)
extends CharacterBody3D
@export var movement_speed: float = 4.0
@onready var navigation_agent: NavigationAgent3D = get_node("NavigationAgent3D")
func _ready() -> void:
navigation_agent.velocity_computed.connect(Callable(_on_velocity_computed))
func set_movement_target(movement_target: Vector3):
navigation_agent.set_target_position(movement_target)
func _physics_process(delta):
# Do not query when the map has never synchronized and is empty.
if NavigationServer3D.map_get_iteration_id(navigation_agent.get_navigation_map()) == 0:
return
if navigation_agent.is_navigation_finished():
return
var next_path_position: Vector3 = navigation_agent.get_next_path_position()
var new_velocity: Vector3 = global_position.direction_to(next_path_position) * movement_speed
if navigation_agent.avoidance_enabled:
navigation_agent.set_velocity(new_velocity)
else:
_on_velocity_computed(new_velocity)
func _on_velocity_computed(safe_velocity: Vector3):
velocity = safe_velocity
move_and_slide()
extends RigidBody3D
@export var movement_speed: float = 4.0
@onready var navigation_agent: NavigationAgent3D = get_node("NavigationAgent3D")
func _ready() -> void:
navigation_agent.velocity_computed.connect(Callable(_on_velocity_computed))
func set_movement_target(movement_target: Vector3):
navigation_agent.set_target_position(movement_target)
func _physics_process(delta):
# Do not query when the map has never synchronized and is empty.
if NavigationServer3D.map_get_iteration_id(navigation_agent.get_navigation_map()) == 0:
return
if navigation_agent.is_navigation_finished():
return
var next_path_position: Vector3 = navigation_agent.get_next_path_position()
var new_velocity: Vector3 = global_position.direction_to(next_path_position) * movement_speed
if navigation_agent.avoidance_enabled:
navigation_agent.set_velocity(new_velocity)
else:
_on_velocity_computed(new_velocity)
func _on_velocity_computed(safe_velocity: Vector3):
linear_velocity = safe_velocity
using Godot;
public partial class MyNode3D : Node3D
{
[Export]
public float MovementSpeed { get; set; } = 4.0f;
NavigationAgent3D _navigationAgent;
private float _movementDelta;
public override void _Ready()
{
_navigationAgent = GetNode<NavigationAgent3D>("NavigationAgent3D");
_navigationAgent.VelocityComputed += OnVelocityComputed;
}
private void SetMovementTarget(Vector3 movementTarget)
{
_navigationAgent.TargetPosition = movementTarget;
}
public override void _PhysicsProcess(double delta)
{
// Do not query when the map has never synchronized and is empty.
if (NavigationServer3D.MapGetIterationId(_navigationAgent.GetNavigationMap()) == 0)
{
return;
}
if (_navigationAgent.IsNavigationFinished())
{
return;
}
_movementDelta = MovementSpeed * (float)delta;
Vector3 nextPathPosition = _navigationAgent.GetNextPathPosition();
Vector3 newVelocity = GlobalPosition.DirectionTo(nextPathPosition) * _movementDelta;
if (_navigationAgent.AvoidanceEnabled)
{
_navigationAgent.Velocity = newVelocity;
}
else
{
OnVelocityComputed(newVelocity);
}
}
private void OnVelocityComputed(Vector3 safeVelocity)
{
GlobalPosition = GlobalPosition.MoveToward(GlobalPosition + safeVelocity, _movementDelta);
}
}
using Godot;
public partial class MyCharacterBody3D : CharacterBody3D
{
[Export]
public float MovementSpeed { get; set; } = 4.0f;
NavigationAgent3D _navigationAgent;
public override void _Ready()
{
_navigationAgent = GetNode<NavigationAgent3D>("NavigationAgent3D");
_navigationAgent.VelocityComputed += OnVelocityComputed;
}
private void SetMovementTarget(Vector3 movementTarget)
{
_navigationAgent.TargetPosition = movementTarget;
}
public override void _PhysicsProcess(double delta)
{
// Do not query when the map has never synchronized and is empty.
if (NavigationServer3D.MapGetIterationId(_navigationAgent.GetNavigationMap()) == 0)
{
return;
}
if (_navigationAgent.IsNavigationFinished())
{
return;
}
Vector3 nextPathPosition = _navigationAgent.GetNextPathPosition();
Vector3 newVelocity = GlobalPosition.DirectionTo(nextPathPosition) * MovementSpeed;
if (_navigationAgent.AvoidanceEnabled)
{
_navigationAgent.Velocity = newVelocity;
}
else
{
OnVelocityComputed(newVelocity);
}
}
private void OnVelocityComputed(Vector3 safeVelocity)
{
Velocity = safeVelocity;
MoveAndSlide();
}
}
using Godot;
public partial class MyRigidBody3D : RigidBody3D
{
[Export]
public float MovementSpeed { get; set; } = 4.0f;
NavigationAgent3D _navigationAgent;
public override void _Ready()
{
_navigationAgent = GetNode<NavigationAgent3D>("NavigationAgent3D");
_navigationAgent.VelocityComputed += OnVelocityComputed;
}
private void SetMovementTarget(Vector3 movementTarget)
{
_navigationAgent.TargetPosition = movementTarget;
}
public override void _PhysicsProcess(double delta)
{
// Do not query when the map has never synchronized and is empty.
if (NavigationServer3D.MapGetIterationId(_navigationAgent.GetNavigationMap()) == 0)
{
return;
}
if (_navigationAgent.IsNavigationFinished())
{
return;
}
Vector3 nextPathPosition = _navigationAgent.GetNextPathPosition();
Vector3 newVelocity = GlobalPosition.DirectionTo(nextPathPosition) * MovementSpeed;
if (_navigationAgent.AvoidanceEnabled)
{
_navigationAgent.Velocity = newVelocity;
}
else
{
OnVelocityComputed(newVelocity);
}
}
private void OnVelocityComputed(Vector3 safeVelocity)
{
LinearVelocity = safeVelocity;
}
}