navmesh navmesh

游戏AI导航网格生成和寻径技能,支持Unity、Unreal和Godot引擎。

AI应用 0 次安装 0 次浏览 更新于 2/25/2026

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 如可能则避免

最佳实践

  1. 代理尺寸:将代理半径与角色碰撞匹配
  2. 区域成本:使用成本自然影响路径偏好
  3. 动态更新:批量导航网格更新以提高性能
  4. 网格外链接:适当用于跳跃、下落、梯子
  5. 调试:在开发期间始终启用导航网格可视化
  6. LOD:为大型开放区域简化导航网格

性能考虑

优化 描述
层次寻径 为长路径预计算区域图
路径缓存 当目的地未改变时重用路径
异步寻径 不要阻塞主线程
导航网格瓦片 启用增量更新
查询过滤器 限制搜索范围

参考资料