name: navmesh
description: 游戏AI的导航网格生成和寻径技能。能够在Unity、Unreal和Godot引擎中创建和配置导航网格、寻径查询、动态障碍物和导航代理设置。
allowed-tools: 读,搜索,写,Bash,编辑,全局匹配,网络获取,
导航网格技能
为多个引擎的游戏AI系统提供全面的导航网格生成和寻径实现。
概览
这项技能提供创建、配置和使用导航网格进行AI寻径的能力。它涵盖了导航网格生成、代理配置、动态障碍物、网格外链接和运行时导航查询。
能力
导航网格生成
- 配置导航网格构建设置
- 定义可行走区域和表面
- 设置具有成本的区域类型
- 生成运行时导航网格
代理配置
- 配置代理半径和高度
- 设置移动参数
- 定义避让优先级
- 配置步高和斜坡
寻径
- 查询点之间的路径
- 处理部分路径
- 实现路径平滑
- 支持层次寻径
动态导航
- 处理动态障碍物
- 实现导航网格雕刻
- 在运行时更新导航网格
- 处理移动平台
网格外链接
- 创建跳跃链接
- 配置下落连接
- 处理梯子和传送
- 设置单向路径
先决条件
Unity
// 内置:包管理器 > AI导航
// 安装:com.unity.ai.navigation
Unreal Engine
// 在Build.cs中启用NavigationSystem模块
PublicDependencyModuleNames.AddRange(new string[] {
"NavigationSystem",
"AIModule"
});
Godot
# 在项目设置中启用NavigationServer2D/3D
# 使用NavigationRegion2D/3D和NavigationAgent2D/3D节点
使用模式
Unity导航设置
// 导航网格代理配置
using UnityEngine;
using UnityEngine.AI;
public class AINavigation : MonoBehaviour
{
[Header("导航设置")]
[SerializeField] private float moveSpeed = 3.5f;
[SerializeField] private float angularSpeed = 120f;
[SerializeField] private float stoppingDistance = 0.5f;
private NavMeshAgent _agent;
private Transform _target;
private void Awake()
{
_agent = GetComponent<NavMeshAgent>();
ConfigureAgent();
}
private void ConfigureAgent()
{
_agent.speed = moveSpeed;
_agent.angularSpeed = angularSpeed;
_agent.stoppingDistance = stoppingDistance;
_agent.autoBraking = true;
}
public void SetDestination(Vector3 destination)
{
if (NavMesh.SamplePosition(destination, out NavMeshHit hit, 2f, NavMesh.AllAreas))
{
_agent.SetDestination(hit.position);
}
}
public void SetTarget(Transform target)
{
_target = target;
}
private void Update()
{
if (_target != null)
{
SetDestination(_target.position);
}
}
public bool HasReachedDestination()
{
if (!_agent.pathPending)
{
if (_agent.remainingDistance <= _agent.stoppingDistance)
{
if (!_agent.hasPath || _agent.velocity.sqrMagnitude == 0f)
{
return true;
}
}
}
return false;
}
public bool IsPathValid()
{
return _agent.hasPath && _agent.pathStatus == NavMeshPathStatus.PathComplete;
}
}
// 动态导航网格障碍物
using UnityEngine;
using UnityEngine.AI;
public class DynamicObstacle : MonoBehaviour
{
private NavMeshObstacle _obstacle;
private void Awake()
{
_obstacle = GetComponent<NavMeshObstacle>();
_obstacle.carving = true;
_obstacle.carvingMoveThreshold = 0.1f;
_obstacle.carvingTimeToStationary = 0.5f;
}
public void EnableCarving(bool enable)
{
_obstacle.carving = enable;
}
}
// 网格外链接设置
using UnityEngine;
using UnityEngine.AI;
public class JumpLink : MonoBehaviour
{
[SerializeField] private Transform startPoint;
[SerializeField] private Transform endPoint;
[SerializeField] private bool bidirectional = false;
private OffMeshLink _link;
private void Awake()
{
_link = gameObject.AddComponent<OffMeshLink>();
_link.startTransform = startPoint;
_link.endTransform = endPoint;
_link.biDirectional = bidirectional;
_link.autoUpdatePositions = true;
}
}
// 导航网格表面运行时烘焙
using UnityEngine;
using Unity.AI.Navigation;
public class RuntimeNavMesh : MonoBehaviour
{
private NavMeshSurface _surface;
private void Awake()
{
_surface = GetComponent<NavMeshSurface>();
}
public void RebuildNavMesh()
{
_surface.BuildNavMesh();
}
public void UpdateNavMesh()
{
_surface.UpdateNavMesh(_surface.navMeshData);
}
}
Unreal Engine导航设置
// AINavigationComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "NavigationSystem.h"
#include "AINavigationComponent.generated.h"
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class MYGAME_API UAINavigationComponent : public UActorComponent
{
GENERATED_BODY()
public:
UAINavigationComponent();
UFUNCTION(BlueprintCallable, Category = "Navigation")
bool MoveToLocation(FVector Destination);
UFUNCTION(BlueprintCallable, Category = "Navigation")
bool MoveToActor(AActor* TargetActor);
UFUNCTION(BlueprintCallable, Category = "Navigation")
void StopMovement();
UFUNCTION(BlueprintCallable, Category = "Navigation")
bool HasReachedDestination() const;
UFUNCTION(BlueprintCallable, Category = "Navigation")
FVector GetRandomReachablePoint(float Radius) const;
protected:
virtual void BeginPlay() override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Navigation")
float AcceptanceRadius = 50.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Navigation")
bool bStopOnOverlap = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Navigation")
bool bUsePathfinding = true;
private:
class AAIController* AIController;
class UNavigationSystemV1* NavSystem;
};
// AINavigationComponent.cpp
#include "AINavigationComponent.h"
#include "AIController.h"
#include "NavigationSystem.h"
#include "NavFilters/NavigationQueryFilter.h"
UAINavigationComponent::UAINavigationComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
void UAINavigationComponent::BeginPlay()
{
Super::BeginPlay();
APawn* Pawn = Cast<APawn>(GetOwner());
if (Pawn)
{
AIController = Cast<AAIController>(Pawn->GetController());
}
NavSystem = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
}
bool UAINavigationComponent::MoveToLocation(FVector Destination)
{
if (!AIController) return false;
FAIMoveRequest MoveRequest;
MoveRequest.SetGoalLocation(Destination);
MoveRequest.SetAcceptanceRadius(AcceptanceRadius);
MoveRequest.SetStopOnOverlap(bStopOnOverlap);
MoveRequest.SetUsePathfinding(bUsePathfinding);
FNavPathSharedPtr Path;
AIController->MoveTo(MoveRequest, &Path);
return Path.IsValid();
}
bool UAINavigationComponent::MoveToActor(AActor* TargetActor)
{
if (!AIController || !TargetActor) return false;
FAIMoveRequest MoveRequest;
MoveRequest.SetGoalActor(TargetActor);
MoveRequest.SetAcceptanceRadius(AcceptanceRadius);
MoveRequest.SetStopOnOverlap(bStopOnOverlap);
MoveRequest.SetUsePathfinding(bUsePathfinding);
FNavPathSharedPtr Path;
AIController->MoveTo(MoveRequest, &Path);
return Path.IsValid();
}
void UAINavigationComponent::StopMovement()
{
if (AIController)
{
AIController->StopMovement();
}
}
bool UAINavigationComponent::HasReachedDestination() const
{
if (!AIController) return false;
return AIController->GetMoveStatus() == EPathFollowingStatus::Idle;
}
FVector UAINavigationComponent::GetRandomReachablePoint(float Radius) const
{
FNavLocation RandomPoint;
if (NavSystem && NavSystem->GetRandomReachablePointInRadius(GetOwner()->GetActorLocation(), Radius, RandomPoint))
{
return RandomPoint.Location;
}
return GetOwner()->GetActorLocation();
}
// 导航网格修改器体积(蓝图友好)
// BTTask_MoveToLocation.h
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTTask_MoveToLocation.generated.h"
UCLASS()
class MYGAME_API UBTTask_MoveToLocation : public UBTTaskNode
{
GENERATED_BODY()
public:
UBTTask_MoveToLocation();
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
protected:
UPROPERTY(EditAnywhere, Category = "Blackboard")
FBlackboardKeySelector TargetLocationKey;
UPROPERTY(EditAnywhere, Category = "Movement")
float AcceptableRadius = 50.0f;
};
Godot导航设置(GDScript)
# navigation_controller.gd
extends CharacterBody2D
class_name NavigationController
## 移动速度,以每秒像素为单位
@export var move_speed: float = 200.0
## 到达距离阈值
@export var arrival_distance: float = 10.0
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D
var _is_navigating: bool = false
signal destination_reached
signal path_changed
func _ready() -> void:
nav_agent.velocity_computed.connect(_on_velocity_computed)
nav_agent.path_changed.connect(_on_path_changed)
nav_agent.target_reached.connect(_on_target_reached)
# 配置代理
nav_agent.path_desired_distance = arrival_distance
nav_agent.target_desired_distance = arrival_distance
func _physics_process(delta: float) -> void:
if not _is_navigating:
return
if nav_agent.is_navigation_finished():
_is_navigating = false
destination_reached.emit()
return
var next_path_position := nav_agent.get_next_path_position()
var direction := global_position.direction_to(next_path_position)
var velocity := direction * move_speed
if nav_agent.avoidance_enabled:
nav_agent.velocity = velocity
else:
_move(velocity)
func set_target_position(target: Vector2) -> void:
nav_agent.target_position = target
_is_navigating = true
func set_target_node(target: Node2D) -> void:
set_target_position(target.global_position)
func stop_navigation() -> void:
_is_navigating = false
velocity = Vector2.ZERO
func is_navigating() -> bool:
return _is_navigating
func get_remaining_distance() -> float:
return nav_agent.distance_to_target()
func _move(vel: Vector2) -> void:
velocity = vel
move_and_slide()
func _on_velocity_computed(safe_velocity: Vector2) -> void:
_move(safe_velocity)
func _on_path_changed() -> void:
path_changed.emit()
func _on_target_reached() -> void:
_is_navigating = false
destination_reached.emit()
# navigation_region_setup.gd
@tool
extends NavigationRegion2D
@export var bake_on_ready: bool = true
@export var auto_rebake_interval: float = 0.0
var _rebake_timer: float = 0.0
func _ready() -> void:
if not Engine.is_editor_hint() and bake_on_ready:
call_deferred("bake_navigation_polygon")
func _process(delta: float) -> void:
if Engine.is_editor_hint():
return
if auto_rebake_interval > 0:
_rebake_timer += delta
if _rebake_timer >= auto_rebake_interval:
_rebake_timer = 0.0
bake_navigation_polygon()
func rebake() -> void:
bake_navigation_polygon()
# dynamic_obstacle.gd
extends Node2D
class_name DynamicNavObstacle
@export var obstacle_vertices: PackedVector2Array
@export var affect_navigation: bool = true
@onready var nav_obstacle: NavigationObstacle2D = $NavigationObstacle2D
func _ready() -> void:
if obstacle_vertices.size() > 0:
nav_obstacle.vertices = obstacle_vertices
nav_obstacle.avoidance_enabled = affect_navigation
func set_vertices(vertices: PackedVector2Array) -> void:
nav_obstacle.vertices = vertices
func enable_avoidance(enabled: bool) -> void:
nav_obstacle.avoidance_enabled = enabled
# navigation_link.gd
extends NavigationLink2D
@export var link_cost: float = 1.0
@export_enum("Bidirectional", "Start to End", "End to Start") var direction: int = 0
func _ready() -> void:
travel_cost = link_cost
bidirectional = (direction == 0)
if direction == 2:
# Swap start and end for "End to Start"
var temp := start_position
start_position = end_position
end_position = temp
bidirectional = false
与Babysitter SDK集成
任务定义示例
const navmeshSetupTask = defineTask({
name: 'navmesh-setup',
description: '为AI寻径配置导航网格',
inputs: {
engine: { type: 'string', required: true }, // unity, unreal, godot
agentType: { type: 'string', required: true },
settings: { type: 'object', required: true },
outputPath: { type: 'string', required: true }
},
outputs: {
configPath: { type: 'string' },
componentFiles: { type: 'array' },
success: { type: 'boolean' }
},
async run(inputs, taskCtx) {
return {
kind: 'skill',
title: `为${inputs.agentType}设置导航网格`,
skill: {
name: 'navmesh',
context: {
operation: 'configure_navigation',
engine: inputs.engine,
agentType: inputs.agentType,
settings: inputs.settings,
outputPath: inputs.outputPath
}
},
io: {
inputJsonPath: `tasks/${taskCtx.effectId}/input.json`,
outputJsonPath: `tasks/${taskCtx.effectId}/result.json`
}
};
}
});
代理配置参数
| 参数 |
描述 |
典型值 |
| 代理半径 |
碰撞半径 |
0.3-0.6米 |
| 代理高度 |
完整代理高度 |
1.5-2.0米 |
| 最大斜率 |
可行走斜坡角度 |
30-45度 |
| 步高 |
可攀爬的台阶 |
0.3-0.5米 |
| 最大速度 |
移动速度 |
3-10米/秒 |
| 加速度 |
速度变化率 |
8-20米/秒^2 |
区域类型和成本
| 区域类型 |
成本 |
用例 |
| 可行走 |
1.0 |
默认地面 |
| 道路 |
0.5 |
优选路径 |
| 草地 |
1.5 |
较慢地形 |
| 水(浅) |
2.0 |
可通过但缓慢 |
| 水(深) |
无穷大 |
不可通过 |
| 危险 |
3.0 |
如可能则避免 |
最佳实践
- 代理尺寸:将代理半径与角色碰撞匹配
- 区域成本:使用成本自然影响路径偏好
- 动态更新:批量导航网格更新以提高性能
- 网格外链接:适当用于跳跃、下落、梯子
- 调试:在开发期间始终启用导航网格可视化
- LOD:为大型开放区域简化导航网格
性能考虑
| 优化 |
描述 |
| 层次寻径 |
为长路径预计算区域图 |
| 路径缓存 |
当目的地未改变时重用路径 |
| 异步寻径 |
不要阻塞主线程 |
| 导航网格瓦片 |
启用增量更新 |
| 查询过滤器 |
限制搜索范围 |
参考资料