在《刺客信条》的屋顶追逐战中,你是否好奇守卫如何精准预判玩家的逃跑路线?或是《最后生还者》中感染者如何绕过燃烧瓶形成的火墙?这些令人印象深刻的AI行为背后,往往离不开导航网格技术的支持。本文将带你深入Unity的NavMesh系统,构建一个能自主决策、动态应对复杂环境的智能敌人。
NavMeshAgent是AI角色的"大脑",负责处理所有路径计算和移动逻辑。不同于简单的Transform移动,它具备物理感知能力:
csharp复制// 典型的基础设置
NavMeshAgent agent = GetComponent<NavMeshAgent>();
agent.speed = 3.5f; // 移动速度(米/秒)
agent.angularSpeed = 120f; // 转向速度(度/秒)
agent.acceleration = 8f; // 加速度
agent.stoppingDistance = 0.5f; // 停止距离
避障参数优化表:
| 参数 | 推荐值 | 作用说明 |
|---|---|---|
| Radius | 0.25-0.5 | 碰撞检测半径,影响绕行距离 |
| Height | 1.8-2.0 | 决定可通过的障碍物高度 |
| Quality | High | 避障精度,高性能设备可设最高 |
| Priority | 50 | 数值越低避障优先级越高 |
提示:当场景中有大量AI时,适当降低Quality可提升性能,但会导致"穿模"现象
动态障碍物是让游戏世界"活起来"的关键要素。以下是一个移动车辆的实现示例:
csharp复制public class DynamicObstacle : MonoBehaviour {
private NavMeshObstacle obstacle;
void Start() {
obstacle = GetComponent<NavMeshObstacle>();
obstacle.carve = true;
obstacle.moveThreshold = 0.1f; // 移动超过0.1米触发更新
obstacle.timeToStationary = 2f; // 2秒静止后视为固定障碍
}
void Update() {
// 车辆移动逻辑...
}
}
动态雕刻参数对比:
悬崖跳跃点的典型配置流程:
csharp复制// 自定义跳跃动画触发
IEnumerator PlayJumpAnimation(Transform agent) {
// 播放起跳动画...
yield return new WaitForSeconds(0.5f);
// 实际位移到目标点
agent.position = endPoint.position;
// 播放落地动画...
}
基础追击只需设置目标位置,但智能追击需要考虑更多因素:
csharp复制public class AdvancedChase : MonoBehaviour {
public Transform player;
public float updateInterval = 0.3f;
private NavMeshAgent agent;
private float timer;
void Start() {
agent = GetComponent<NavMeshAgent>();
}
void Update() {
timer += Time.deltaTime;
if(timer >= updateInterval) {
timer = 0;
// 不是简单追当前位置,而是预测玩家移动方向
Vector3 predictPos = player.position + player.forward * 2f;
agent.SetDestination(predictPos);
// 根据距离动态调整速度
float distance = Vector3.Distance(transform.position, player.position);
agent.speed = Mathf.Lerp(1f, 5f, distance/10f);
}
}
}
追击策略选择表:
| 情景 | 策略 | 实现方式 |
|---|---|---|
| 开阔地带 | 直线追击 | 直接设置目标位置 |
| 复杂地形 | 路径预测 | 根据玩家速度预测移动方向 |
| 多人围堵 | 包抄路线 | 多个AI分配不同路径点 |
| 潜行模式 | 最后已知位置 | 记录玩家最后可见坐标 |
让AI能够识别并利用环境中的特殊路径:
csharp复制public class EnvironmentInteraction : MonoBehaviour {
public LayerMask interactableLayer;
public float checkRadius = 5f;
void Update() {
Collider[] hits = Physics.OverlapSphere(transform.position, checkRadius, interactableLayer);
foreach(var hit in hits) {
OffMeshLink link = hit.GetComponent<OffMeshLink>();
if(link && ShouldUseShortcut(link)) {
// 计算常规路径和捷径的代价
NavMeshPath normalPath = new NavMeshPath();
agent.CalculatePath(link.endTransform.position, normalPath);
float normalCost = GetPathCost(normalPath);
float shortcutCost = link.costOverride;
if(shortcutCost < normalCost) {
// 触发特殊移动行为
StartCoroutine(TraverseLink(link));
}
}
}
}
float GetPathCost(NavMeshPath path) {
float cost = 0;
for(int i = 0; i < path.corners.Length-1; i++) {
cost += Vector3.Distance(path.corners[i], path.corners[i+1]);
}
return cost;
}
}
将导航系统整合到更复杂的AI决策框架中:
csharp复制public enum AIState { Patrol, Chase, Search, Attack }
public class AIBehavior : MonoBehaviour {
public AIState currentState;
private NavMeshAgent agent;
private Transform player;
void Start() {
agent = GetComponent<NavMeshAgent>();
player = GameObject.FindGameObjectWithTag("Player").transform;
currentState = AIState.Patrol;
}
void Update() {
switch(currentState) {
case AIState.Patrol:
PatrolBehavior();
break;
case AIState.Chase:
ChaseBehavior();
break;
// 其他状态...
}
}
void PatrolBehavior() {
if(CanSeePlayer()) {
currentState = AIState.Chase;
return;
}
if(agent.remainingDistance < 0.5f) {
agent.SetDestination(GetRandomPatrolPoint());
}
}
void ChaseBehavior() {
if(!CanSeePlayer()) {
currentState = AIState.Search;
lastKnownPosition = player.position;
return;
}
agent.SetDestination(player.position);
if(agent.remainingDistance < attackRange) {
currentState = AIState.Attack;
}
}
}
导航区域分层技术:
csharp复制// 设置AI可通行区域
agent.areaMask = NavMesh.GetAreaFromName("Ground") | NavMesh.GetAreaFromName("Water");
动态加载方案:
路径查找失败排查步骤:
NavMesh.SamplePosition调试可视化工具:
csharp复制void OnDrawGizmos() {
if(agent != null && agent.path != null) {
Gizmos.color = Color.red;
for(int i = 0; i < agent.path.corners.Length-1; i++) {
Gizmos.DrawLine(agent.path.corners[i], agent.path.corners[i+1]);
}
}
}
解决导航移动与动画系统的同步问题:
csharp复制public class AnimationBlending : MonoBehaviour {
public Animator animator;
public NavMeshAgent agent;
public float smoothTime = 0.1f;
void Update() {
float speed = agent.velocity.magnitude / agent.speed;
animator.SetFloat("Speed", speed, smoothTime, Time.deltaTime);
if(agent.velocity != Vector3.zero) {
Quaternion targetRotation = Quaternion.LookRotation(agent.velocity);
transform.rotation = Quaternion.Slerp(
transform.rotation,
targetRotation,
agent.angularSpeed * Time.deltaTime
);
}
}
}
动画参数对照表:
| 导航参数 | 动画参数 | 转换公式 |
|---|---|---|
| velocity.magnitude | Speed | 实际速度/最大速度 |
| remainingDistance | Stopping | 距离/停止距离 |
| pathStatus | MotionState | 枚举转换 |
创建包含以下要素的测试场景:
烘焙参数推荐:
csharp复制public class SmartGuard : MonoBehaviour {
[Header("Navigation")]
public NavMeshAgent agent;
public Transform[] patrolPoints;
public float chaseSpeed = 6f;
[Header("Detection")]
public float visionRange = 10f;
public float visionAngle = 90f;
public LayerMask obstacleMask;
[Header("Behavior")]
public float patrolWaitTime = 2f;
public float searchDuration = 5f;
private int currentPatrolIndex;
private float stateTimer;
private Vector3 lastKnownPosition;
void Start() {
currentPatrolIndex = 0;
agent.SetDestination(patrolPoints[currentPatrolIndex].position);
}
void Update() {
if(DetectPlayer()) {
HandlePlayerDetection();
return;
}
switch(currentState) {
case AIState.Patrol:
PatrolUpdate();
break;
case AIState.Search:
SearchUpdate();
break;
// 其他状态...
}
}
bool DetectPlayer() {
Vector3 directionToPlayer = player.position - transform.position;
if(directionToPlayer.magnitude > visionRange) return false;
if(Vector3.Angle(transform.forward, directionToPlayer) > visionAngle/2)
return false;
if(Physics.Raycast(transform.position, directionToPlayer, out var hit, visionRange, obstacleMask)) {
return hit.transform == player;
}
return false;
}
void PatrolUpdate() {
if(agent.remainingDistance < 0.5f) {
stateTimer += Time.deltaTime;
if(stateTimer >= patrolWaitTime) {
stateTimer = 0;
currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length;
agent.SetDestination(patrolPoints[currentPatrolIndex].position);
}
}
}
}
团队协作追击:
csharp复制public class GroupChase : MonoBehaviour {
public SmartGuard[] guards;
public float surroundRadius = 3f;
public void InitiateGroupChase(Vector3 targetPos) {
for(int i = 0; i < guards.Length; i++) {
// 计算包围位置
float angle = i * Mathf.PI * 2 / guards.Length;
Vector3 offset = new Vector3(Mathf.Cos(angle), 0, Mathf.Sin(angle)) * surroundRadius;
guards[i].SetChaseTarget(targetPos + offset);
}
}
}
环境互动记忆:
csharp复制public class EnvironmentMemory : MonoBehaviour {
public Dictionary<Vector3, float> dangerSpots = new Dictionary<Vector3, float>();
public float memoryDecayRate = 0.1f;
void Update() {
// 记忆衰减
var keys = new List<Vector3>(dangerSpots.Keys);
foreach(var key in keys) {
dangerSpots[key] -= memoryDecayRate * Time.deltaTime;
if(dangerSpots[key] <= 0) dangerSpots.Remove(key);
}
}
public void RecordDangerSpot(Vector3 position, float dangerLevel) {
if(dangerSpots.ContainsKey(position)) {
dangerSpots[position] = Mathf.Max(dangerSpots[position], dangerLevel);
} else {
dangerSpots.Add(position, dangerLevel);
}
}
public bool IsPositionDangerous(Vector3 position, float threshold = 0.5f) {
foreach(var spot in dangerSpots) {
if(Vector3.Distance(position, spot.Key) < 2f && spot.Value > threshold) {
return true;
}
}
return false;
}
}