寻路系统
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 包:
- 打开 Window > Package Manager
- 选择 Unity Registry
- 搜索 AI Navigation
- 点击 Install
创建 NavMesh
使用 NavMeshSurface 组件
NavMeshSurface 是新版本寻路系统的核心组件,用于定义和生成导航网格。
基本步骤:
- 在场景中创建一个空对象,命名为 "Navigation"
- 添加 NavMeshSurface 组件
- 配置烘焙参数
- 点击 Bake 按钮生成 NavMesh
NavMeshSurface 参数详解
// 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 窗口配置:
- 打开 Window > AI > Navigation
- 选择 Agents 标签
- 点击 "+" 添加新的 Agent 类型
Agent 参数:
| 参数 | 说明 | 默认值 |
|---|---|---|
| Name | Agent 类型名称 | Humanoid |
| Radius | Agent 半径(影响与障碍物的距离) | 0.5m |
| Height | Agent 高度(影响可通行的高度) | 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 角色控制
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; // 自动更新旋转
}
}
NavMeshAgent 属性详解
| 属性 | 类型 | 说明 |
|---|---|---|
| speed | float | 最大移动速度 |
| angularSpeed | float | 最大转向速度 |
| acceleration | float | 加速度 |
| stoppingDistance | float | 到目标的停止距离 |
| radius | float | Agent 半径(用于避障) |
| height | float | Agent 高度 |
| velocity | Vector3 | 当前速度(只读) |
| desiredVelocity | Vector3 | 期望速度(只读) |
| remainingDistance | float | 剩余路径距离(只读) |
| isStopped | bool | 是否停止 |
| pathPending | bool | 是否正在计算路径 |
| pathStatus | NavMeshPathStatus | 路径状态 |
| hasPath | bool | 是否有路径 |
| destination | Vector3 | 目标位置 |
设置目标位置
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 个内置区域,你可以自定义它们的名称和成本:
| 区域名称 | 默认成本 | 用途 |
|---|---|---|
| Walkable | 1 | 普通可行走区域 |
| Not Walkable | 1 | 不可行走区域 |
| Jump | 2 | 跳跃区域 |
| Water | 3 | 水域 |
| User Defined 4-31 | 1 | 自定义区域 |
设置区域成本
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 修改区域
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:
- 选择需要修改的游戏对象
- 添加 NavMeshModifier 组件
- 勾选 Override Area
- 选择要应用的区域类型
- 重新烘焙 NavMesh
NavMeshLink 连接区域
NavMeshLink(也称为 Off-Mesh Link)用于连接两个不连续的 NavMesh 区域,让 Agent 可以执行跳跃、攀爬、下落等动作。
创建 NavMeshLink
方式1:编辑器创建
- 选择要添加链接的对象
- 添加 NavMeshLink 组件
- 设置起点和终点
方式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");
}
}
NavMeshLink 参数
| 参数 | 说明 |
|---|---|
| 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 动态障碍物
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 让你手动计算和操作路径。
NavMesh.CalculatePath
手动计算两点之间的路径:
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.SamplePosition
在 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.Raycast
在 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 烘焙优化
// 优化 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);
}
}