跳到主要内容

寻路系统

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);
}
}

参考资源​