跳到主要内容

寻路系统

Unity 的寻路系统(AI Navigation)基于 NavMesh(导航网格)实现,让游戏中的 AI 角色能够智能地在场景中移动,自动避开障碍物找到最优路径。这是开发游戏中 NPC、敌人 AI、 RTS 游戏单位移动等功能的必备系统。

寻路系统概述

什么是 NavMesh?

NavMesh(Navigation Mesh,导航网格)是一种用于描述场景中可行走区域的数据结构。它将场景中复杂的几何体简化为多边形网格,AI 角色只能在这些网格上移动。

NavMesh 的优势

  • 高效寻路:预先计算可行走区域,运行时寻路效率高
  • 自动避障:结合 NavMeshObstacle 实现动态避障
  • 灵活配置:支持不同区域类型、成本设置
  • 可视化编辑:在 Scene 视图中直接预览可行走区域

核心组件

Unity 寻路系统由以下核心组件构成:

组件作用
NavMeshSurface定义需要生成 NavMesh 的区域,执行烘焙
NavMeshAgent附加到 AI 角色上,控制移动和寻路
NavMeshModifier修改特定区域的导航属性
NavMeshLink连接两个不连续的 NavMesh 区域(跳跃、攀爬)
NavMeshObstacle动态障碍物,让 Agent 避开移动的物体

AI Navigation 包

Unity 2022.3 及以上版本使用 AI Navigation 包替代了旧版内置的导航系统。新版本的导航系统提供了更灵活的工作流程和更多功能。

安装 AI Navigation 包

  1. 打开 Window > Package Manager
  2. 选择 Unity Registry
  3. 搜索 AI Navigation
  4. 点击 Install

创建 NavMesh

使用 NavMeshSurface 组件

NavMeshSurface 是新版本寻路系统的核心组件,用于定义和生成导航网格。

基本步骤

  1. 在场景中创建一个空对象,命名为 "Navigation"
  2. 添加 NavMeshSurface 组件
  3. 配置烘焙参数
  4. 点击 Bake 按钮生成 NavMesh
// NavMeshSurface 组件的主要参数
public class NavMeshSurfaceSettings : MonoBehaviour
{
void Start()
{
NavMeshSurface surface = GetComponent<NavMeshSurface>();

// ========== Agent 设置 ==========
// agentTypeID: Agent 类型 ID,不同类型可以有不同尺寸
// 可以在 Navigation 窗口中自定义 Agent 类型

// ========== 收集对象设置 ==========
// collectObjects: 决定哪些对象参与烘焙
// - CollectAllChildren: 收集所有子对象(默认)
// - CollectVolume: 收集 Bounds 内的对象
// - CollectAllSceneObjects: 收集场景中所有对象

// includeLayers: 参与烘焙的层级
// useGeometry: 使用哪种几何体
// - RenderMeshes: 使用渲染网格
// - PhysicsColliders: 使用物理碰撞体

// ========== 高级设置 ==========
// overrideVoxelSize: 是否覆盖体素大小
// voxelSize: 体素尺寸(米),越小精度越高但烘焙越慢
// default voxelSize = agentRadius / 3
}
}

Agent 类型设置

Agent 类型决定了烘焙 NavMesh 时的角色尺寸参数,不同尺寸的角色会生成不同的 NavMesh。

Navigation 窗口配置

  1. 打开 Window > AI > Navigation
  2. 选择 Agents 标签
  3. 点击 "+" 添加新的 Agent 类型

Agent 参数

参数说明默认值
NameAgent 类型名称Humanoid
RadiusAgent 半径(影响与障碍物的距离)0.5m
HeightAgent 高度(影响可通行的高度)2.0m
Step Height最大台阶高度0.4m
Max Slope最大坡度(度)45°

不同角色类型的建议设置

// 小型角色(如老鼠、小狗)
Radius: 0.2m
Height: 0.3m
Step Height: 0.1m
Max Slope: 60°

// 普通人形角色
Radius: 0.5m
Height: 1.8m
Step Height: 0.4m
Max Slope: 45°

// 大型角色(如巨人、载具)
Radius: 2.0m
Height: 3.0m
Step Height: 0.5m
Max Slope: 30°

运行时烘焙 NavMesh

有些游戏需要在运行时动态生成 NavMesh,例如程序化生成的地形或可破坏的环境。

using UnityEngine;
using UnityEngine.AI;

public class RuntimeNavMeshBaker : MonoBehaviour
{
public NavMeshSurface surface;

void Start()
{
// 生成地形或其他游戏对象
GenerateTerrain();

// 运行时烘焙 NavMesh
surface.BuildNavMesh();
}

// 当地形变化时更新 NavMesh
public void UpdateNavMesh()
{
// 移除旧的 NavMesh 数据
surface.RemoveData();

// 重新烘焙
surface.BuildNavMesh();
}

// 异步烘焙(适合大型场景)
public void BakeAsync()
{
StartCoroutine(BakeNavMeshAsync());
}

IEnumerator BakeNavMeshAsync()
{
// 使用 BuildNavMeshAsync 进行异步烘焙
AsyncOperation operation = surface.BuildNavMeshAsync();

while (!operation.isDone)
{
Debug.Log($"NavMesh 烘焙进度: {operation.progress * 100}%");
yield return null;
}

Debug.Log("NavMesh 烘焙完成");
}

void GenerateTerrain()
{
// 生成地形的逻辑
}
}

NavMeshAgent 是附加到游戏对象上的组件,让角色能够在 NavMesh 上移动。

基本配置

using UnityEngine;
using UnityEngine.AI;

[RequireComponent(typeof(NavMeshAgent))]
public class AIController : MonoBehaviour
{
private NavMeshAgent agent;

void Start()
{
agent = GetComponent<NavMeshAgent>();

// ========== 移动参数 ==========
agent.speed = 3.5f; // 移动速度(米/秒)
agent.angularSpeed = 120f; // 转向速度(度/秒)
agent.acceleration = 8f; // 加速度(米/秒²)
agent.stoppingDistance = 0.5f; // 停止距离

// ========== 避障参数 ==========
agent.radius = 0.5f; // Agent 半径
agent.height = 2f; // Agent 高度
agent.obstacleAvoidanceType = ObstacleAvoidanceType.HighQuality;

// ========== 路径寻找 ==========
agent.autoRepath = true; // 目标变化时自动重新寻路
agent.areaMask = NavMesh.AllAreas; // 可行走的区域

// ========== 其他设置 ==========
agent.autoBraking = true; // 到达目标时自动减速
agent.updatePosition = true; // 自动更新位置
agent.updateRotation = true; // 自动更新旋转
}
}
属性类型说明
speedfloat最大移动速度
angularSpeedfloat最大转向速度
accelerationfloat加速度
stoppingDistancefloat到目标的停止距离
radiusfloatAgent 半径(用于避障)
heightfloatAgent 高度
velocityVector3当前速度(只读)
desiredVelocityVector3期望速度(只读)
remainingDistancefloat剩余路径距离(只读)
isStoppedbool是否停止
pathPendingbool是否正在计算路径
pathStatusNavMeshPathStatus路径状态
hasPathbool是否有路径
destinationVector3目标位置

设置目标位置

public class AgentMovement : MonoBehaviour
{
public NavMeshAgent agent;
public Transform target;

void Update()
{
// 方式1:设置目标位置(自动计算路径)
agent.SetDestination(target.position);

// 方式2:直接设置 destination 属性
agent.destination = target.position;

// 检查是否到达目标
if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance)
{
Debug.Log("已到达目标");
}
}

// 移动到点击位置
void MoveToClickPosition()
{
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

if (Physics.Raycast(ray, out RaycastHit hit))
{
// 检查点击位置是否在 NavMesh 上
if (NavMesh.SamplePosition(hit.point, out NavMeshHit navHit, 1f, NavMesh.AllAreas))
{
agent.SetDestination(navHit.position);
}
}
}
}
}

停止和恢复移动

public class AgentControl : MonoBehaviour
{
private NavMeshAgent agent;

void Start()
{
agent = GetComponent<NavMeshAgent>();
}

// 暂停移动(保留路径)
public void Pause()
{
agent.isStopped = true;
}

// 恢复移动
public void Resume()
{
agent.isStopped = false;
}

// 完全停止并清除路径
public void Stop()
{
agent.isStopped = true;
agent.ResetPath();
}

// 立即传送到新位置
public void Teleport(Vector3 position)
{
agent.Warp(position);
}
}

手动控制移动

有时候需要对 Agent 进行更精细的控制,可以使用 Move 方法。

public class ManualAgentControl : MonoBehaviour
{
private NavMeshAgent agent;

void Start()
{
agent = GetComponent<NavMeshAgent>();

// 禁用自动更新
agent.updatePosition = false;
agent.updateRotation = false;
}

void Update()
{
// 手动控制移动
Vector3 moveDirection = agent.desiredVelocity.normalized;
float moveSpeed = agent.desiredVelocity.magnitude;

// 应用自定义移动逻辑
transform.position += moveDirection * moveSpeed * Time.deltaTime;

// 手动控制转向
if (moveDirection != Vector3.zero)
{
Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
transform.rotation = Quaternion.Slerp(
transform.rotation,
targetRotation,
10f * Time.deltaTime
);
}

// 同步 Agent 内部位置
agent.nextPosition = transform.position;
}
}

路径状态检查

public class PathStatusChecker : MonoBehaviour
{
private NavMeshAgent agent;

void Start()
{
agent = GetComponent<NavMeshAgent>();
}

public void CheckPathStatus()
{
// 检查路径状态
switch (agent.pathStatus)
{
case NavMeshPathStatus.PathComplete:
Debug.Log("路径完整,可以到达目标");
break;

case NavMeshPathStatus.PathPartial:
Debug.Log("路径不完整,目标可能无法到达");
break;

case NavMeshPathStatus.PathInvalid:
Debug.Log("路径无效,无法到达目标");
break;
}

// 检查是否卡住
if (agent.velocity.sqrMagnitude < 0.1f && agent.remainingDistance > agent.stoppingDistance)
{
Debug.Log("Agent 可能卡住了");
}
}
}

区域和成本

NavMesh 支持定义不同类型的区域,每种区域可以设置不同的通行成本。这让你可以控制 Agent 偏好某些路径。

区域类型

Unity 预定义了 29 个内置区域,你可以自定义它们的名称和成本:

区域名称默认成本用途
Walkable1普通可行走区域
Not Walkable1不可行走区域
Jump2跳跃区域
Water3水域
User Defined 4-311自定义区域

设置区域成本

public class AreaCostManager : MonoBehaviour
{
void Start()
{
// 设置区域成本(全局)
// 参数:区域名称,成本值
NavMesh.SetAreaCost(NavMesh.GetAreaFromName("Water"), 5f);
NavMesh.SetAreaCost(NavMesh.GetAreaFromName("Jump"), 2f);

// 获取区域索引
int waterAreaIndex = NavMesh.GetAreaFromName("Water");
int jumpAreaIndex = NavMesh.GetAreaFromName("Jump");

Debug.Log($"Water 区域索引: {waterAreaIndex}");
Debug.Log($"Jump 区域索引: {jumpAreaIndex}");
}
}

Agent 区域遮罩

控制 Agent 可以行走的区域类型:

public class AgentAreaMask : MonoBehaviour
{
private NavMeshAgent agent;

void Start()
{
agent = GetComponent<NavMeshAgent>();

// 方式1:使用位遮罩
int walkable = 1 << NavMesh.GetAreaFromName("Walkable");
int water = 1 << NavMesh.GetAreaFromName("Water");

// Agent 只能走在 Walkable 和 Water 区域
agent.areaMask = walkable | water;

// 方式2:设置区域成本(影响路径选择)
// 可以走在所有区域,但会优先选择低成本区域
agent.areaMask = NavMesh.AllAreas;
agent.SetAreaCost(NavMesh.GetAreaFromName("Water"), 10f); // Agent 会尽量避免走水
}

// 动态改变可行走区域
public void EnableWaterTraversal(bool enable)
{
int walkable = 1 << NavMesh.GetAreaFromName("Walkable");
int water = 1 << NavMesh.GetAreaFromName("Water");

if (enable)
{
agent.areaMask = walkable | water;
}
else
{
agent.areaMask = walkable;
}

// 重新计算路径
agent.ResetPath();
}
}

NavMeshModifier 组件可以修改特定游戏对象的区域类型:

using UnityEngine.AI;

public class AreaModifierExample : MonoBehaviour
{
void Start()
{
// 通过代码添加 NavMeshModifier
NavMeshModifier modifier = gameObject.AddComponent<NavMeshModifier>();

// 忽略该对象(不参与 NavMesh 烘焙)
modifier.ignoreFromBuild = true;

// 或者修改该对象的区域类型
modifier.overrideArea = true;
modifier.area = NavMesh.GetAreaFromName("Water");
}
}

在编辑器中配置 NavMeshModifier

  1. 选择需要修改的游戏对象
  2. 添加 NavMeshModifier 组件
  3. 勾选 Override Area
  4. 选择要应用的区域类型
  5. 重新烘焙 NavMesh

NavMeshLink(也称为 Off-Mesh Link)用于连接两个不连续的 NavMesh 区域,让 Agent 可以执行跳跃、攀爬、下落等动作。

方式1:编辑器创建

  1. 选择要添加链接的对象
  2. 添加 NavMeshLink 组件
  3. 设置起点和终点

方式2:代码创建

using UnityEngine.AI;

public class LinkCreator : MonoBehaviour
{
public Transform startPoint;
public Transform endPoint;

void Start()
{
NavMeshLink link = gameObject.AddComponent<NavMeshLink>();

// 设置起点和终点
link.startPoint = startPoint.localPosition;
link.endPoint = endPoint.localPosition;

// 设置宽度(多个 Agent 可以同时使用)
link.width = 1f;

// 成本修正(影响路径选择)
link.costModifier = 1f;

// 双向链接
link.bidirectional = true;

// 自动更新位置
link.autoUpdate = true;

// 区域类型
link.area = NavMesh.GetAreaFromName("Jump");
}
}
参数说明
startPoint链接起点(本地坐标)
endPoint链接终点(本地坐标)
width链接宽度
costModifier成本修正值
bidirectional是否双向通行
autoUpdate自动更新位置
area链接的区域类型

跳跃平台示例

public class JumpPlatform : MonoBehaviour
{
public Transform landingPoint;
public float jumpDuration = 1f;

private NavMeshLink link;

void Start()
{
// 创建跳跃链接
link = gameObject.AddComponent<NavMeshLink>();
link.startPoint = Vector3.zero;
link.endPoint = transform.InverseTransformPoint(landingPoint.position);
link.width = 2f;
link.bidirectional = false; // 只能单向跳
link.area = NavMesh.GetAreaFromName("Jump");
}
}

// Agent 使用跳跃链接时的动画控制
public class AgentJumpController : MonoBehaviour
{
private NavMeshAgent agent;
private bool isJumping = false;

void Start()
{
agent = GetComponent<NavMeshAgent>();

// 订阅链接事件
agent.autoTraverseOffMeshLink = false; // 禁用自动穿越
}

IEnumerator Update()
{
while (true)
{
// 检测是否到达 Off-Mesh Link
if (agent.isOnOffMeshLink && !isJumping)
{
yield return StartCoroutine(JumpAcrossLink());
}
yield return null;
}
}

IEnumerator JumpAcrossLink()
{
isJumping = true;

// 获取链接信息
OffMeshLinkData linkData = agent.currentOffMeshLinkData;
Vector3 startPos = transform.position;
Vector3 endPos = linkData.endPos;

float duration = 1f;
float time = 0f;

// 执行跳跃动画
while (time < duration)
{
float t = time / duration;

// 水平移动
transform.position = Vector3.Lerp(startPos, endPos, t);

// 添加垂直弧线
float height = Mathf.Sin(t * Mathf.PI) * 2f;
transform.position += Vector3.up * height;

time += Time.deltaTime;
yield return null;
}

// 完成跳跃
transform.position = endPos;
agent.CompleteOffMeshLink();
isJumping = false;
}
}

NavMeshObstacle 用于创建动态障碍物,让 Agent 能够避开移动中的物体。

基本使用

using UnityEngine.AI;

public class DynamicObstacle : MonoBehaviour
{
private NavMeshObstacle obstacle;

void Start()
{
obstacle = GetComponent<NavMeshObstacle>();

// 障碍物形状
obstacle.shape = NavMeshObstacleShape.Capsule; // 或 Box

// 尺寸
obstacle.radius = 1f;
obstacle.height = 2f;

// 移动时是否雕刻 NavMesh
obstacle.carve = true;

// 雕刻参数
obstacle.carveOnlyStationary = false; // 只有静止时才雕刻
obstacle.carvingTimeToStationary = 0.5f; // 认为静止的时间阈值
obstacle.carvingMoveThreshold = 0.1f; // 移动阈值
}
}

Carve 模式详解

NavMeshObstacle 有两种工作模式:

非雕刻模式(carve = false)

  • 使用避障算法绕过障碍物
  • 性能开销较低
  • Agent 可能会绕远路

雕刻模式(carve = true)

  • 在 NavMesh 上"挖洞",创建真正的障碍区域
  • Agent 寻路时会考虑这些区域
  • 性能开销较高,适合静止或缓慢移动的障碍物
public class ObstacleController : MonoBehaviour
{
private NavMeshObstacle obstacle;
private Rigidbody rb;

void Start()
{
obstacle = GetComponent<NavMeshObstacle>();
rb = GetComponent<Rigidbody>();
}

void Update()
{
// 根据速度决定是否雕刻
bool isMoving = rb.velocity.magnitude > 0.1f;

if (isMoving)
{
// 移动时不雕刻,使用避障
obstacle.carve = false;
}
else
{
// 静止时雕刻 NavMesh
obstacle.carve = true;
}
}
}

推动物体示例

public class PushableObstacle : MonoBehaviour
{
private NavMeshObstacle obstacle;
private Rigidbody rb;

void Start()
{
obstacle = GetComponent<NavMeshObstacle>();
rb = GetComponent<Rigidbody>();

// 配置为雕刻模式
obstacle.carve = true;
obstacle.carveOnlyStationary = true;
obstacle.carvingTimeToStationary = 0.3f;
}

void OnCollisionEnter(Collision collision)
{
// 被玩家推动时
if (collision.gameObject.CompareTag("Player"))
{
rb.isKinematic = false;
}
}

void OnCollisionExit(Collision collision)
{
if (collision.gameObject.CompareTag("Player"))
{
// 停止推动后重新变为静止障碍物
StartCoroutine(StopMoving());
}
}

IEnumerator StopMoving()
{
yield return new WaitForSeconds(1f);
rb.velocity = Vector3.zero;
rb.isKinematic = true;
}
}

路径计算 API

除了使用 NavMeshAgent 自动寻路,Unity 还提供了底层 API 让你手动计算和操作路径。

手动计算两点之间的路径:

public class PathCalculator : MonoBehaviour
{
public Transform target;

void CalculatePath()
{
NavMeshPath path = new NavMeshPath();

// 计算路径
bool success = NavMesh.CalculatePath(
transform.position, // 起点
target.position, // 终点
NavMesh.AllAreas, // 区域遮罩
path // 输出路径
);

if (success && path.status == NavMeshPathStatus.PathComplete)
{
Debug.Log($"路径包含 {path.corners.Length} 个拐点");

// 遍历路径拐点
for (int i = 0; i < path.corners.Length; i++)
{
Debug.Log($"拐点 {i}: {path.corners[i]}");
}

// 绘制路径
for (int i = 0; i < path.corners.Length - 1; i++)
{
Debug.DrawLine(path.corners[i], path.corners[i + 1], Color.green, 5f);
}
}
else
{
Debug.Log("无法计算路径");
}
}

// 可视化路径
void OnDrawGizmos()
{
NavMeshPath path = new NavMeshPath();
if (NavMesh.CalculatePath(transform.position, target.position, NavMesh.AllAreas, path))
{
Gizmos.color = Color.green;
for (int i = 0; i < path.corners.Length - 1; i++)
{
Gizmos.DrawLine(path.corners[i], path.corners[i + 1]);
}
}
}
}

在 NavMesh 上查找最近的可行走点:

public class PositionSampler : MonoBehaviour
{
public bool FindValidPosition(Vector3 targetPosition, out Vector3 validPosition)
{
// 在半径 2 米范围内查找 NavMesh 上的点
if (NavMesh.SamplePosition(targetPosition, out NavMeshHit hit, 2f, NavMesh.AllAreas))
{
validPosition = hit.position;
return true;
}

validPosition = targetPosition;
return false;
}

// 生成随机巡逻点
public Vector3 GetRandomPatrolPoint(Vector3 center, float radius)
{
// 生成随机方向
Vector2 randomCircle = Random.insideUnitCircle * radius;
Vector3 randomPoint = center + new Vector3(randomCircle.x, 0, randomCircle.y);

// 查找 NavMesh 上的有效点
if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, radius, NavMesh.AllAreas))
{
return hit.position;
}

return center;
}
}

在 NavMesh 上进行射线检测:

public class NavMeshRaycast : MonoBehaviour
{
public bool CanReachDestination(Vector3 from, Vector3 to)
{
// 从起点向终点发射射线
if (NavMesh.Raycast(from, to, out NavMeshHit hit, NavMesh.AllAreas))
{
Debug.Log($"路径被阻挡,阻挡位置: {hit.position}");
Debug.Log($"阻挡距离: {hit.distance}");
return false;
}

return true;
}

// 检测前方是否有障碍物
void CheckForwardObstacle()
{
Vector3 forward = transform.forward * 5f;
Vector3 startPos = transform.position;
Vector3 endPos = startPos + forward;

if (NavMesh.Raycast(startPos, endPos, out NavMeshHit hit, NavMesh.AllAreas))
{
Debug.Log($"前方 {hit.distance} 米处有障碍");

// 查找最近的可通行边缘
if (NavMesh.FindClosestEdge(hit.position, out NavMeshHit edgeHit, NavMesh.AllAreas))
{
Debug.Log($"最近边缘位置: {edgeHit.position}");
}
}
}
}

AI 行为实现

巡逻行为

让 AI 在一组巡逻点之间移动:

public class PatrolBehavior : MonoBehaviour
{
public Transform[] patrolPoints;
public float waitTimeAtPoint = 2f;

private NavMeshAgent agent;
private int currentPointIndex = 0;
private bool isWaiting = false;

void Start()
{
agent = GetComponent<NavMeshAgent>();
MoveToNextPoint();
}

void Update()
{
// 检查是否到达当前巡逻点
if (!agent.pathPending && agent.remainingDistance < 0.5f)
{
if (!isWaiting)
{
StartCoroutine(WaitAndMoveNext());
}
}
}

void MoveToNextPoint()
{
if (patrolPoints.Length == 0) return;

agent.destination = patrolPoints[currentPointIndex].position;
}

IEnumerator WaitAndMoveNext()
{
isWaiting = true;

yield return new WaitForSeconds(waitTimeAtPoint);

// 移动到下一个巡逻点
currentPointIndex = (currentPointIndex + 1) % patrolPoints.Length;
MoveToNextPoint();

isWaiting = false;
}

// 随机巡逻
void RandomPatrol()
{
currentPointIndex = Random.Range(0, patrolPoints.Length);
MoveToNextPoint();
}
}

追逐玩家

public class ChaseBehavior : MonoBehaviour
{
public Transform player;
public float chaseRange = 10f;
public float attackRange = 2f;

private NavMeshAgent agent;
private float updatePathInterval = 0.5f;
private float lastUpdateTime;

void Start()
{
agent = GetComponent<NavMeshAgent>();
}

void Update()
{
float distanceToPlayer = Vector3.Distance(transform.position, player.position);

if (distanceToPlayer <= chaseRange)
{
if (distanceToPlayer <= attackRange)
{
// 攻击范围内,停止移动
agent.isStopped = true;
Attack();
}
else
{
// 追逐玩家(不每帧更新路径以提高性能)
if (Time.time - lastUpdateTime > updatePathInterval)
{
agent.SetDestination(player.position);
lastUpdateTime = Time.time;
}
agent.isStopped = false;
}
}
else
{
// 玩家超出追逐范围
agent.isStopped = true;
}
}

void Attack()
{
// 面向玩家
Vector3 lookDir = (player.position - transform.position).normalized;
lookDir.y = 0;
transform.rotation = Quaternion.LookRotation(lookDir);

// 攻击逻辑
Debug.Log("攻击玩家");
}
}

逃跑行为

public class FleeBehavior : MonoBehaviour
{
public Transform threat;
public float fleeDistance = 10f;
public float safeDistance = 15f;

private NavMeshAgent agent;

void Start()
{
agent = GetComponent<NavMeshAgent>();
}

void Update()
{
float distanceToThreat = Vector3.Distance(transform.position, threat.position);

if (distanceToThreat < safeDistance)
{
Flee();
}
}

void Flee()
{
// 计算逃跑方向(远离威胁)
Vector3 fleeDirection = (transform.position - threat.position).normalized;
Vector3 fleePosition = transform.position + fleeDirection * fleeDistance;

// 查找 NavMesh 上的有效位置
if (NavMesh.SamplePosition(fleePosition, out NavMeshHit hit, fleeDistance, NavMesh.AllAreas))
{
agent.SetDestination(hit.position);
}
}
}

群体行为

使用避免碰撞实现群体移动:

public class GroupBehavior : MonoBehaviour
{
public Transform leader;
public float followDistance = 3f;
public float separationRadius = 2f;

private NavMeshAgent agent;

void Start()
{
agent = GetComponent<NavMeshAgent>();
agent.obstacleAvoidanceType = ObstacleAvoidanceType.HighQuality;
}

void Update()
{
if (leader == null) return;

// 计算跟随位置(在领导者后方)
Vector3 followOffset = -leader.forward * followDistance;
Vector3 targetPosition = leader.position + followOffset;

// 分离:避免与其他 Agent 重叠
Vector3 separation = CalculateSeparation();
targetPosition += separation;

// 设置目标
if (NavMesh.SamplePosition(targetPosition, out NavMeshHit hit, 5f, NavMesh.AllAreas))
{
agent.SetDestination(hit.position);
}
}

Vector3 CalculateSeparation()
{
Vector3 separation = Vector3.zero;
int neighborCount = 0;

// 查找附近的 Agent
Collider[] neighbors = Physics.OverlapSphere(transform.position, separationRadius);

foreach (var neighbor in neighbors)
{
if (neighbor.gameObject != gameObject && neighbor.GetComponent<NavMeshAgent>() != null)
{
Vector3 diff = transform.position - neighbor.transform.position;
float distance = diff.magnitude;

if (distance > 0 && distance < separationRadius)
{
// 距离越近,分离力越大
separation += diff.normalized / distance;
neighborCount++;
}
}
}

if (neighborCount > 0)
{
separation /= neighborCount;
}

return separation;
}
}

行为状态机

组合多种行为的状态机实现:

public enum AIState
{
Idle,
Patrol,
Chase,
Attack,
Flee
}

public class AIStateMachine : MonoBehaviour
{
public AIState currentState;
public Transform player;
public Transform[] patrolPoints;

[Header("状态参数")]
public float detectionRange = 10f;
public float attackRange = 2f;
public float fleeHealthPercent = 0.2f;

private NavMeshAgent agent;
private int patrolIndex = 0;
private float health = 100f;

void Start()
{
agent = GetComponent<NavMeshAgent>();
currentState = AIState.Patrol;
}

void Update()
{
// 状态机主循环
switch (currentState)
{
case AIState.Idle:
UpdateIdle();
break;
case AIState.Patrol:
UpdatePatrol();
break;
case AIState.Chase:
UpdateChase();
break;
case AIState.Attack:
UpdateAttack();
break;
case AIState.Flee:
UpdateFlee();
break;
}

// 全局状态转换检查
CheckStateTransitions();
}

void CheckStateTransitions()
{
float distanceToPlayer = Vector3.Distance(transform.position, player.position);
float healthPercent = health / 100f;

// 低血量时逃跑
if (healthPercent < fleeHealthPercent && currentState != AIState.Flee)
{
TransitionToState(AIState.Flee);
return;
}

// 检测玩家
if (distanceToPlayer < detectionRange)
{
if (distanceToPlayer < attackRange && currentState != AIState.Attack)
{
TransitionToState(AIState.Attack);
}
else if (currentState != AIState.Chase && currentState != AIState.Attack)
{
TransitionToState(AIState.Chase);
}
}
else if (currentState == AIState.Chase || currentState == AIState.Attack)
{
TransitionToState(AIState.Patrol);
}
}

void TransitionToState(AIState newState)
{
// 退出当前状态
OnStateExit(currentState);

// 进入新状态
currentState = newState;
OnStateEnter(newState);
}

void OnStateEnter(AIState state)
{
switch (state)
{
case AIState.Idle:
agent.isStopped = true;
break;
case AIState.Patrol:
agent.isStopped = false;
break;
case AIState.Chase:
agent.speed = 5f;
break;
case AIState.Attack:
agent.isStopped = true;
break;
case AIState.Flee:
agent.speed = 6f;
break;
}
}

void OnStateExit(AIState state)
{
// 重置状态相关参数
agent.speed = 3.5f;
}

// 各状态更新方法
void UpdateIdle()
{
// 空闲状态逻辑
}

void UpdatePatrol()
{
if (!agent.pathPending && agent.remainingDistance < 0.5f)
{
patrolIndex = (patrolIndex + 1) % patrolPoints.Length;
agent.SetDestination(patrolPoints[patrolIndex].position);
}
}

void UpdateChase()
{
agent.SetDestination(player.position);
}

void UpdateAttack()
{
// 面向玩家
Vector3 lookDir = (player.position - transform.position).normalized;
lookDir.y = 0;
transform.rotation = Quaternion.LookRotation(lookDir);

// 攻击逻辑
}

void UpdateFlee()
{
Vector3 fleeDir = (transform.position - player.position).normalized;
Vector3 fleePos = transform.position + fleeDir * 10f;

if (NavMesh.SamplePosition(fleePos, out NavMeshHit hit, 10f, NavMesh.AllAreas))
{
agent.SetDestination(hit.position);
}
}
}

性能优化

Agent 数量优化

public class AgentManager : MonoBehaviour
{
public int maxActiveAgents = 50;
public float activationDistance = 30f;
public Transform player;

private List<NavMeshAgent> allAgents = new List<NavMeshAgent>();

void Update()
{
foreach (var agent in allAgents)
{
float distanceToPlayer = Vector3.Distance(agent.transform.position, player.position);

if (distanceToPlayer < activationDistance)
{
agent.enabled = true;
}
else
{
agent.enabled = false;
}
}
}

// 限制同时寻路的 Agent 数量
public void SetDestinationWithThrottling(NavMeshAgent agent, Vector3 destination)
{
if (activePathfindingCount < maxActiveAgents)
{
agent.SetDestination(destination);
activePathfindingCount++;
}
else
{
// 延迟寻路请求
StartCoroutine(DelayedSetDestination(agent, destination));
}
}

private int activePathfindingCount = 0;

IEnumerator DelayedSetDestination(NavMeshAgent agent, Vector3 destination)
{
yield return new WaitUntil(() => activePathfindingCount < maxActiveAgents);
agent.SetDestination(destination);
activePathfindingCount++;
}
}

寻路设置优化

// 全局寻路性能设置
void ConfigureNavMeshPerformance()
{
// 每帧最大寻路迭代次数
NavMesh.pathfindingIterationsPerFrame = 100; // 默认值,可根据需要调整

// 避障预测时间
NavMesh.avoidancePredictionTime = 1f; // 默认值

// 设置路径更新频率
InvokeRepeating("UpdateAllAgentPaths", 0f, 0.5f); // 每 0.5 秒更新一次
}

void UpdateAllAgentPaths()
{
// 批量更新 Agent 路径
NavMeshAgent[] agents = FindObjectsOfType<NavMeshAgent>();
foreach (var agent in agents)
{
if (agent.isActiveAndEnabled && agent.hasPath)
{
agent.ResetPath();
}
}
}
// 优化 NavMesh 烘焙质量与性能的平衡
public class NavMeshOptimization : MonoBehaviour
{
public NavMeshSurface surface;

[Header("优化设置")]
public bool useLowerVoxelSize = false;
public bool useSimplifiedMesh = true;

void OptimizeBakeSettings()
{
// 使用较大的体素尺寸可以提高烘焙速度
if (useLowerVoxelSize)
{
surface.overrideVoxelSize = true;
surface.voxelSize = 0.2f; // 默认是 agentRadius / 3
}

// 只使用物理碰撞体而非渲染网格
if (useSimplifiedMesh)
{
surface.useGeometry = NavMeshCollectGeometry.PhysicsColliders;
}
}

// 分区域烘焙(适合大型场景)
public NavMeshSurface[] regionSurfaces;

IEnumerator BakeRegionsAsync()
{
foreach (var surface in regionSurfaces)
{
AsyncOperation operation = surface.BuildNavMeshAsync();
yield return operation;
Debug.Log($"区域 {surface.name} 烘焙完成");
}
}
}

实践示例:完整的敌人 AI

using UnityEngine;
using UnityEngine.AI;

/// <summary>
/// 完整的敌人 AI 控制器,包含巡逻、追逐、攻击、逃跑等行为
/// </summary>
[RequireComponent(typeof(NavMeshAgent))]
public class EnemyAI : MonoBehaviour
{
[Header("组件引用")]
public Transform player;
public Animator animator;

[Header("巡逻设置")]
public Transform[] patrolPoints;
public float patrolSpeed = 2f;
public float waitTimeAtPatrolPoint = 2f;

[Header("检测设置")]
public float sightRange = 15f;
public float sightAngle = 90f;
public float hearingRange = 8f;
public LayerMask sightObstructionLayers;

[Header("追逐设置")]
public float chaseSpeed = 4f;
public float chaseUpdateInterval = 0.3f;
public float losePlayerTime = 5f;

[Header("攻击设置")]
public float attackRange = 2f;
public float attackCooldown = 1.5f;
public int attackDamage = 10;

[Header("逃跑设置")]
public float fleeHealthThreshold = 20f;
public float fleeDistance = 15f;

// 内部状态
private NavMeshAgent agent;
private EnemyState currentState;
private int patrolIndex;
private float lastSeenPlayerTime;
private float lastAttackTime;
private float health;
private Vector3 lastKnownPlayerPosition;

// 动画参数哈希
private static readonly int SpeedHash = Animator.StringToHash("Speed");
private static readonly int AttackHash = Animator.StringToHash("Attack");
private static readonly int HitHash = Animator.StringToHash("Hit");
private static readonly int DieHash = Animator.StringToHash("Die");

enum EnemyState
{
Patrol,
Chase,
Attack,
Flee,
Dead
}

void Awake()
{
agent = GetComponent<NavMeshAgent>();
health = 100f;
currentState = EnemyState.Patrol;
}

void Start()
{
if (player == null)
{
player = GameObject.FindGameObjectWithTag("Player").transform;
}

EnterPatrolState();
}

void Update()
{
if (currentState == EnemyState.Dead) return;

UpdateCurrentState();
UpdateAnimator();

// 全局状态检查
CheckForStateTransitions();
}

#region 状态管理

void UpdateCurrentState()
{
switch (currentState)
{
case EnemyState.Patrol:
UpdatePatrol();
break;
case EnemyState.Chase:
UpdateChase();
break;
case EnemyState.Attack:
UpdateAttack();
break;
case EnemyState.Flee:
UpdateFlee();
break;
}
}

void CheckForStateTransitions()
{
// 低血量逃跑
if (health < fleeHealthThreshold && currentState != EnemyState.Flee && currentState != EnemyState.Dead)
{
ChangeState(EnemyState.Flee);
return;
}

bool canSeePlayer = CanSeePlayer();

switch (currentState)
{
case EnemyState.Patrol:
if (canSeePlayer)
{
lastKnownPlayerPosition = player.position;
ChangeState(EnemyState.Chase);
}
break;

case EnemyState.Chase:
if (canSeePlayer)
{
lastSeenPlayerTime = Time.time;
lastKnownPlayerPosition = player.position;

// 检查是否进入攻击范围
if (GetDistanceToPlayer() <= attackRange)
{
ChangeState(EnemyState.Attack);
}
}
else if (Time.time - lastSeenPlayerTime > losePlayerTime)
{
// 失去玩家太长时间,返回巡逻
ChangeState(EnemyState.Patrol);
}
break;

case EnemyState.Attack:
if (!canSeePlayer || GetDistanceToPlayer() > attackRange * 1.5f)
{
ChangeState(EnemyState.Chase);
}
break;
}
}

void ChangeState(EnemyState newState)
{
if (currentState == newState) return;

ExitState(currentState);
currentState = newState;
EnterState(newState);
}

void EnterState(EnemyState state)
{
switch (state)
{
case EnemyState.Patrol:
EnterPatrolState();
break;
case EnemyState.Chase:
EnterChaseState();
break;
case EnemyState.Attack:
EnterAttackState();
break;
case EnemyState.Flee:
EnterFleeState();
break;
}
}

void ExitState(EnemyState state)
{
switch (state)
{
case EnemyState.Patrol:
CancelInvoke("MoveToNextPatrolPoint");
break;
}
}

#endregion

#region 巡逻状态

void EnterPatrolState()
{
agent.speed = patrolSpeed;
MoveToNextPatrolPoint();
}

void UpdatePatrol()
{
if (!agent.pathPending && agent.remainingDistance < 0.5f)
{
Invoke("MoveToNextPatrolPoint", waitTimeAtPatrolPoint);
}
}

void MoveToNextPatrolPoint()
{
if (patrolPoints.Length == 0) return;

patrolIndex = (patrolIndex + 1) % patrolPoints.Length;
agent.SetDestination(patrolPoints[patrolIndex].position);
}

#endregion

#region 追逐状态

void EnterChaseState()
{
agent.speed = chaseSpeed;
agent.isStopped = false;
StartCoroutine(ChaseUpdateCoroutine());
}

void UpdateChase()
{
// 主要逻辑在协程中
}

IEnumerator ChaseUpdateCoroutine()
{
while (currentState == EnemyState.Chase)
{
if (CanSeePlayer())
{
agent.SetDestination(player.position);
}
else
{
// 移动到最后已知位置
agent.SetDestination(lastKnownPlayerPosition);
}

yield return new WaitForSeconds(chaseUpdateInterval);
}
}

#endregion

#region 攻击状态

void EnterAttackState()
{
agent.isStopped = true;
lastAttackTime = -attackCooldown; // 允许立即攻击
}

void UpdateAttack()
{
// 面向玩家
Vector3 lookDir = player.position - transform.position;
lookDir.y = 0;
transform.rotation = Quaternion.RotateTowards(
transform.rotation,
Quaternion.LookRotation(lookDir),
360f * Time.deltaTime
);

// 攻击
if (Time.time >= lastAttackTime + attackCooldown)
{
PerformAttack();
lastAttackTime = Time.time;
}
}

void PerformAttack()
{
animator.SetTrigger(AttackHash);

// 实际伤害在动画事件中触发
}

// 动画事件调用
void OnAttackHit()
{
if (GetDistanceToPlayer() <= attackRange)
{
// 对玩家造成伤害
var playerHealth = player.GetComponent<IHealth>();
playerHealth?.TakeDamage(attackDamage);
}
}

#endregion

#region 逃跑状态

void EnterFleeState()
{
agent.speed = chaseSpeed * 1.2f;
agent.isStopped = false;

// 计算逃跑方向
Vector3 fleeDirection = (transform.position - player.position).normalized;
Vector3 fleePosition = transform.position + fleeDirection * fleeDistance;

// 查找有效的逃跑位置
if (NavMesh.SamplePosition(fleePosition, out NavMeshHit hit, fleeDistance, NavMesh.AllAreas))
{
agent.SetDestination(hit.position);
}
}

void UpdateFlee()
{
// 到达逃跑点后,禁用 AI
if (!agent.pathPending && agent.remainingDistance < 1f)
{
// 可以触发逃跑成功事件
Debug.Log("敌人成功逃跑");
}
}

#endregion

#region 公共方法

public void TakeDamage(float damage)
{
if (currentState == EnemyState.Dead) return;

health -= damage;
animator.SetTrigger(HitHash);

// 被攻击时记住玩家位置
lastKnownPlayerPosition = player.position;
lastSeenPlayerTime = Time.time;

if (health <= 0)
{
Die();
}
}

void Die()
{
currentState = EnemyState.Dead;
agent.isStopped = true;
animator.SetTrigger(DieHash);

// 禁用碰撞器
GetComponent<Collider>().enabled = false;

// 延迟销毁
Destroy(gameObject, 5f);
}

#endregion

#region 辅助方法

bool CanSeePlayer()
{
if (player == null) return false;

Vector3 directionToPlayer = player.position - transform.position;
float distanceToPlayer = directionToPlayer.magnitude;

// 检查距离
if (distanceToPlayer > sightRange) return false;

// 检查角度
float angleToPlayer = Vector3.Angle(transform.forward, directionToPlayer);
if (angleToPlayer > sightAngle * 0.5f) return false;

// 检查视线阻挡
directionToPlayer.Normalize();
if (Physics.Raycast(
transform.position + Vector3.up * 1.5f,
directionToPlayer,
distanceToPlayer,
sightObstructionLayers))
{
return false;
}

return true;
}

float GetDistanceToPlayer()
{
return Vector3.Distance(transform.position, player.position);
}

void UpdateAnimator()
{
animator.SetFloat(SpeedHash, agent.velocity.magnitude);
}

#endregion

#region 调试可视化

void OnDrawGizmosSelected()
{
// 视野范围
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, sightRange);

// 攻击范围
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, attackRange);

// 视野锥形
Vector3 leftBoundary = Quaternion.Euler(0, -sightAngle * 0.5f, 0) * transform.forward * sightRange;
Vector3 rightBoundary = Quaternion.Euler(0, sightAngle * 0.5f, 0) * transform.forward * sightRange;

Gizmos.color = Color.cyan;
Gizmos.DrawLine(transform.position, transform.position + leftBoundary);
Gizmos.DrawLine(transform.position, transform.position + rightBoundary);
}

#endregion
}

// 健康接口
public interface IHealth
{
void TakeDamage(float damage);
}

最佳实践

1. NavMesh 设计原则

  • 合理设置 Agent 尺寸:过大的半径会导致 Agent 绕远路,过小会穿墙
  • 控制烘焙精度:体素越小精度越高,但烘焙时间更长
  • 使用 NavMeshModifier:针对特定区域设置不同的通行属性
  • 分层烘焙:大型场景按区域烘焙,避免一次性处理过多数据

2. Agent 性能优化

  • 控制同时活跃的 Agent 数量:距离玩家远的 Agent 禁用寻路
  • 降低路径更新频率:追逐时不需要每帧更新路径
  • 使用合适的避障类型LowQuality 性能更好,HighQuality 效果更好
  • 批量处理路径请求:避免同一帧大量 Agent 请求路径

3. 避免常见问题

  • Agent 卡在墙角:增加 Agent 半径或调整场景几何体
  • 路径抖动:增加 stoppingDistance,避免频繁切换目标
  • 穿墙问题:检查 NavMesh 烘焙设置,确保墙体的碰撞体正确
  • 性能问题:减少 Agent 数量,使用 LOD 系统管理 AI

4. 调试技巧

// 显示 NavMesh 边界
void OnDrawGizmos()
{
NavMeshTriangulation triangulation = NavMesh.CalculateTriangulation();

Gizmos.color = Color.green;
for (int i = 0; i < triangulation.indices.Length; i += 3)
{
Vector3 p1 = triangulation.vertices[triangulation.indices[i]];
Vector3 p2 = triangulation.vertices[triangulation.indices[i + 1]];
Vector3 p3 = triangulation.vertices[triangulation.indices[i + 2]];

Gizmos.DrawLine(p1, p2);
Gizmos.DrawLine(p2, p3);
Gizmos.DrawLine(p3, p1);
}
}

参考资源