Unity游戏AI开发:基于NavMeshAgent与状态机实现敌人巡逻与追踪系统
1. 项目概述从“傻站着”到“聪明追”的AI进化在独立游戏开发或者小型团队项目中敌人AI的行为逻辑往往是决定游戏体验好坏的关键一环。想象一下你精心设计了一个充满细节的关卡结果敌人要么像木头一样杵在原地要么一发现玩家就无视地形、穿墙而过这种“出戏感”会瞬间摧毁玩家的沉浸体验。早期很多开发者会使用简单的Transform.Translate配合射线检测来实现移动和追踪但这种方法在复杂地形中极易“翻车”——敌人可能会卡在墙角或者从悬崖边直挺挺地走下去。Unity引擎内置的NavMeshAgent导航网格代理组件就是为了解决这类寻路问题而生的“官方外挂”。它不再是让物体简单地朝一个坐标点直线移动而是让角色能够理解你所构建的虚拟世界的“可行走区域”。通过预先烘焙Bake的导航网格NavMeshNavMeshAgent可以自动计算从A点到B点的最优路径并绕开障碍物、攀爬斜坡、跨越沟壑在网格允许的情况下。这对于实现诸如敌人巡逻、追踪玩家、包围、撤退等复杂AI行为来说是基石般的存在。本次分享我将以一个典型的第三人称或俯视角动作/冒险游戏中的敌人AI为例手把手带你实现一个兼具“日常巡逻”和“警觉追踪”双状态的敌人。我们将深入NavMeshAgent的核心参数理解每一行代码背后的逻辑并附上可直接集成到你项目中的完整代码模块。无论你是刚接触Unity不久的新手还是想优化现有AI系统的开发者这篇内容都能提供清晰的路径和实用的技巧。2. 核心思路与系统设计状态机驱动AI行为在开始写代码之前我们必须为敌人的AI设计一个清晰的大脑。最直观、最易于管理和扩展的方法就是使用有限状态机Finite State Machine, FSM。你可以把FSM理解为敌人AI的“情绪模式”它在任一时刻只处于一种状态并根据特定条件在不同状态间切换。对于我们的巡逻与追踪敌人至少需要设计以下三个核心状态巡逻状态PatrolState敌人的默认状态。在此状态下敌人会在预先设定的一系列路径点Waypoints之间按顺序或随机移动模拟日常的巡视行为。追踪状态ChaseState当敌人发现玩家例如玩家进入其视野范围或触发区域时立即切换到此状态。在此状态下敌人的唯一目标就是尽可能快地接近玩家。返回状态ReturnState这是一个容易被忽略但至关重要的状态。当玩家脱离敌人的追踪范围例如跑得太远后敌人不应该永远卡在最后一次看到玩家的位置也不应该瞬间“传送”回巡逻点。更合理的行为是让敌人先回到它最后失去玩家踪迹的位置Last Known Position稍作停留模拟困惑或搜寻然后再有序地回归到巡逻路径上。为什么选择状态机因为它结构清晰将复杂的行为逻辑分解为独立的模块。每个状态只关心自己该做什么Update逻辑和什么条件下应该退出切换条件。这比把所有if-else判断都堆在一个Update函数里要易于维护和调试得多。未来如果你想增加一个“攻击状态”或“受伤逃跑状态”只需要新增一个状态类并修改切换条件即可不会影响原有逻辑。NavMeshAgent的角色 在我们的设计中NavMeshAgent是状态机的“执行器”。每个状态通过调用NavMeshAgent.SetDestination()来告诉Agent它应该去哪里。状态机负责决策“目标点是什么”而NavMeshAgent则负责解决“如何到达目标点”这个复杂的路径寻找和移动问题。这种职责分离让我们的AI逻辑既强大又简洁。3. 环境准备与NavMesh烘焙3.1 场景搭建与静态物体标记首先你需要一个测试场景。创建一个平面Plane作为地面并摆放一些立方体Cube或导入的模型作为障碍物比如墙壁、箱子、柱子等。关键的一步来了所有不希望被敌人穿过的静态障碍物都必须被标记为Navigation Static。在Hierarchy中选择你的地面和所有障碍物。在Inspector窗口的右上角找到“Static”下拉复选框点击它。在弹出的菜单中确保“Navigation Static”被勾选。这意味着Unity在烘焙导航网格时会将这些物体的几何形状考虑在内将其排除在可行走区域之外。注意对于会移动的物体如可推动的箱子、开关门不能标记为Navigation Static。对于这类动态障碍物需要使用NavMeshObstacle组件这属于更高级的用法本篇暂不展开。3.2 导航网格烘焙参数详解导航网格NavMesh本质上是一张覆盖在场景可行走区域上的蓝色“网格”。NavMeshAgent只能在这张蓝色的网格上移动。打开导航窗口Window-AI-Navigation。切换到“Bake”标签页。这里有很多参数但初期我们重点关注这几个Agent Radius代理半径想象成敌人的“身体宽度”。这个值决定了路径与障碍物之间会保持多大距离。如果敌人老是在拐角处卡住可以适当调小此值如果敌人之间容易重叠可以调大。Agent Height代理高度敌人能通过的最低通道高度。如果你在场景中设计了一个需要蹲下才能通过的洞口这个高度值就决定了敌人是否会尝试通过。Max Slope最大坡度敌人能爬上的最大斜坡角度单位度。超过这个角度的斜坡会被视为不可行走的“墙”。Step Height台阶高度敌人能直接迈上去的最大台阶高度。这对于楼梯或门槛类地形非常有用。设置好参数后点击右下角的“Bake”按钮。烘焙完成后Scene视图中会出现蓝色的NavMesh覆盖区域。确保你的巡逻路径点都设置在蓝色区域内部。实操心得 烘焙是一个迭代过程。第一次烘焙后经常需要回到场景中调整障碍物位置或烘焙参数然后重新烘焙。一个常见的技巧是在复杂场景中可以分层级烘焙多个NavMesh通过Layer分别用于不同体型的敌人如人类士兵和大型怪物但这需要用到NavMesh的进阶功能。4. 敌人预制体与组件配置创建一个敌人模型或用一个胶囊体Capsule代替将其命名为“Enemy”。添加刚体Rigidbody虽然NavMeshAgent自身会处理移动但添加一个刚体组件勾选Is Kinematic可以方便地与其他物理系统交互比如被击退。添加并配置NavMeshAgent组件这是核心组件。Speed移动速度。在巡逻状态和追踪状态可以使用不同的速度值通过代码动态修改。Angular Speed转向速度。值太低会导致敌人转弯时显得迟钝。Acceleration加速度。值越大启动和停止越快移动感更“灵敏”。Stopping Distance停止距离。当敌人到达目标点多远时就认为“已到达”。对于追踪玩家这个值可以设为攻击距离对于巡逻点可以设为一个较小的值如0.5。Auto Braking自动制动。勾选后接近目标时会自动减速移动更平滑。通常建议勾选。添加自定义脚本我们将创建一个名为EnemyAI的脚本挂载到敌人身上它将承载我们整个状态机逻辑。5. 完整代码实现与逐行解析下面是我们EnemyAI脚本的完整实现。代码中包含了详细的注释解释了每一部分的作用。using UnityEngine; using UnityEngine.AI; // 引入AI命名空间 using System.Collections.Generic; public class EnemyAI : MonoBehaviour { // ---------- 状态机相关 ---------- public enum AIState { Patrol, Chase, Return } private AIState _currentState AIState.Patrol; // ---------- 巡逻相关 ---------- public ListTransform patrolPoints; // 在Inspector中拖拽赋值 private int _currentPatrolIndex 0; public float patrolSpeed 3.5f; public float waitTimeAtPoint 2f; // 在每个巡逻点停留的时间 private float _waitCounter 0f; private bool _isWaiting false; // ---------- 视觉/感知相关 ---------- public Transform playerTarget; // 玩家Transform可拖拽或代码查找 public float sightRange 10f; // 视觉范围 public float chaseRange 15f; // 最大追踪范围超过则丢失目标 public float fieldOfViewAngle 90f; // 前方视野角度 private Vector3 _lastKnownPlayerPosition; private bool _hasLastKnownPosition false; // ---------- 追踪相关 ---------- public float chaseSpeed 5f; public float stoppingDistanceToPlayer 1.5f; // 追踪时希望保持的距离 // ---------- 返回相关 ---------- public float returnSpeed 4f; private Vector3 _startPosition; // 敌人的初始位置作为回归的备选点 private Transform _originalPatrolParent; // 巡逻点的父物体用于计算本地位置 // ---------- 组件引用 ---------- private NavMeshAgent _agent; private Animator _animator; // 可选用于控制动画 void Start() { _agent GetComponentNavMeshAgent(); _animator GetComponentAnimator(); // 如果有的话 _startPosition transform.position; // 初始化巡逻点如果未手动赋值尝试查找标签为“PatrolPoint”的物体 if (patrolPoints null || patrolPoints.Count 0) { GameObject[] points GameObject.FindGameObjectsWithTag(PatrolPoint); foreach (GameObject point in points) { patrolPoints.Add(point.transform); } // 按名字排序确保巡逻顺序 patrolPoints.Sort((a, b) a.name.CompareTo(b.name)); } if (patrolPoints.Count 0) { // 记录巡逻点的原始父物体用于处理动态移动的巡逻路径可选 _originalPatrolParent patrolPoints[0].parent; SetNextPatrolPoint(); } else { Debug.LogWarning(EnemyAI: No patrol points assigned. Enemy will idle at start position.); } // 如果未指定玩家尝试按标签查找 if (playerTarget null) { GameObject playerObj GameObject.FindGameObjectWithTag(Player); if (playerObj ! null) playerTarget playerObj.transform; } } void Update() { // 每帧检查状态切换条件 CheckStateTransitions(); // 执行当前状态的行为 switch (_currentState) { case AIState.Patrol: UpdatePatrolState(); break; case AIState.Chase: UpdateChaseState(); break; case AIState.Return: UpdateReturnState(); break; } // 可选更新动画参数 UpdateAnimation(); } // 检查是否满足切换状态的条件 void CheckStateTransitions() { bool canSeePlayer CanSeePlayer(); switch (_currentState) { case AIState.Patrol: if (canSeePlayer) { SwitchState(AIState.Chase); Debug.Log(${gameObject.name} spotted player! Switching to Chase.); } break; case AIState.Chase: if (!canSeePlayer) { // 看不见玩家但可能还在追踪范围内 float distanceToPlayer Vector3.Distance(transform.position, playerTarget.position); if (distanceToPlayer chaseRange) { // 玩家超出最大追踪范围记录最后已知位置并返回 if (_hasLastKnownPosition) { SwitchState(AIState.Return); Debug.Log(${gameObject.name} lost player beyond range. Returning to last known position.); } else { // 罕见情况没看到就直接超出范围直接回巡逻 SwitchState(AIState.Patrol); } } // 如果只是暂时看不见但还在范围内则保持Chase状态继续前往_lastKnownPlayerPosition } break; case AIState.Return: // 如果返回过程中又看到了玩家立即中断返回继续追踪 if (canSeePlayer) { SwitchState(AIState.Chase); } // 返回状态是否完成在UpdateReturnState中判断 break; } } // 巡逻状态更新逻辑 void UpdatePatrolState() { _agent.speed patrolSpeed; if (_isWaiting) { _waitCounter - Time.deltaTime; if (_waitCounter 0) { _isWaiting false; SetNextPatrolPoint(); } // 等待时可以播放 idle 动画 return; } // 检查是否到达当前巡逻点 if (!_agent.pathPending _agent.remainingDistance _agent.stoppingDistance) { if (!_agent.hasPath || _agent.velocity.sqrMagnitude 0f) { // 已到达开始等待 StartWaiting(); } } } // 追踪状态更新逻辑 void UpdateChaseState() { _agent.speed chaseSpeed; _agent.stoppingDistance stoppingDistanceToPlayer; if (playerTarget ! null) { // 持续更新目标为玩家当前位置 _agent.SetDestination(playerTarget.position); _lastKnownPlayerPosition playerTarget.position; _hasLastKnownPosition true; // 可选如果追上了距离足够近可以在这里触发攻击逻辑 // if (_agent.remainingDistance attackRange) { /* Attack */ } } } // 返回状态更新逻辑 void UpdateReturnState() { _agent.speed returnSpeed; _agent.stoppingDistance 0.5f; // 回到点位时停止距离可以小一些 // 如果还没有设置目标则设置为最后已知位置或起始位置 if (_agent.destination ! _lastKnownPlayerPosition _hasLastKnownPosition) { _agent.SetDestination(_lastKnownPlayerPosition); } // 检查是否到达最后已知位置 if (!_agent.pathPending _agent.remainingDistance _agent.stoppingDistance) { if (!_agent.hasPath || _agent.velocity.sqrMagnitude 0f) { // 到达最后已知位置可以在这里添加一个“搜寻”的短暂停顿 Debug.Log(${gameObject.name} reached last known position. Resuming patrol.); // 停顿2秒后切换回巡逻状态 Invoke(ResumePatrolFromReturn, 2f); } } } void ResumePatrolFromReturn() { // 回归巡逻时可以随机选择一个巡逻点或者回到最近的巡逻点 // 这里选择回到起始的巡逻序列 _currentPatrolIndex 0; // 或 FindNearestPatrolPointIndex(); SwitchState(AIState.Patrol); SetNextPatrolPoint(); } // 辅助方法设置下一个巡逻点 void SetNextPatrolPoint() { if (patrolPoints.Count 0) return; _agent.SetDestination(patrolPoints[_currentPatrolIndex].position); _currentPatrolIndex (_currentPatrolIndex 1) % patrolPoints.Count; // 循环索引 } void StartWaiting() { _isWaiting true; _waitCounter waitTimeAtPoint; } // 核心感知方法判断敌人是否“看到”玩家 bool CanSeePlayer() { if (playerTarget null) return false; Vector3 directionToPlayer playerTarget.position - transform.position; float distanceToPlayer directionToPlayer.magnitude; // 1. 距离检查 if (distanceToPlayer sightRange) return false; // 2. 视野角度检查 float angle Vector3.Angle(transform.forward, directionToPlayer); if (angle fieldOfViewAngle / 2) return false; // 3. 射线检测排除墙壁遮挡 RaycastHit hit; if (Physics.Raycast(transform.position Vector3.up * 0.5f, // 从胸口高度发射 directionToPlayer.normalized, out hit, sightRange)) { if (hit.transform playerTarget || hit.transform.IsChildOf(playerTarget)) { return true; } } return false; } // 状态切换方法 void SwitchState(AIState newState) { // 退出当前状态的清理工作如果有 OnExitState(_currentState); _currentState newState; Debug.Log(${gameObject.name} state changed to: {_currentState}); // 进入新状态的初始化工作 OnEnterState(newState); } void OnEnterState(AIState state) { switch (state) { case AIState.Patrol: _agent.isStopped false; _agent.stoppingDistance 0.5f; break; case AIState.Chase: _agent.isStopped false; break; case AIState.Return: _agent.isStopped false; break; } } void OnExitState(AIState state) { // 例如离开Chase状态时可以停止播放追击音效 } // 可选更新Animator参数 void UpdateAnimation() { if (_animator ! null) { // 根据Agent的速度设置动画的Speed参数 _animator.SetFloat(Speed, _agent.velocity.magnitude); // 可以根据状态设置不同的动画Bool如“IsChasing” _animator.SetBool(IsChasing, _currentState AIState.Chase); } } // 在Scene视图中绘制调试信息非常有用 void OnDrawGizmosSelected() { // 绘制视觉范围 Gizmos.color Color.yellow; Gizmos.DrawWireSphere(transform.position, sightRange); // 绘制追踪范围 Gizmos.color Color.red; Gizmos.DrawWireSphere(transform.position, chaseRange); // 绘制视野锥形 Vector3 leftBoundary Quaternion.Euler(0, -fieldOfViewAngle / 2, 0) * transform.forward; Vector3 rightBoundary Quaternion.Euler(0, fieldOfViewAngle / 2, 0) * transform.forward; Gizmos.color Color.cyan; Gizmos.DrawRay(transform.position, leftBoundary * sightRange); Gizmos.DrawRay(transform.position, rightBoundary * sightRange); // 绘制最后已知位置 if (_hasLastKnownPosition) { Gizmos.color Color.magenta; Gizmos.DrawSphere(_lastKnownPlayerPosition, 0.5f); Gizmos.DrawLine(transform.position, _lastKnownPlayerPosition); } } }代码核心解析与技巧感知系统CanSeePlayer方法这是AI的“眼睛”。我们做了三层过滤距离、角度、物理射线。Vector3.up * 0.5f是为了从角色腰部高度发射射线更符合视觉逻辑。射线检测是性能消耗点对于大量敌人可以考虑使用Physics.SphereCast或分帧检测进行优化。状态切换的粒度注意在CheckStateTransitions中从Chase切换到Return的条件是“看不见且超出chaseRange”。如果只是看不见但距离很近敌人会继续朝_lastKnownPlayerPosition移动模拟一种“搜寻”行为这比立刻放弃追踪要真实得多。NavMeshAgent的remainingDistance与pathPending在判断是否到达目的地时必须同时检查!agent.pathPending和agent.remainingDistance agent.stoppingDistance。因为SetDestination是异步的路径计算需要时间在计算完成前remainingDistance可能不准确。调试可视化OnDrawGizmosSelected这是开发AI时的神器。它能让你在Scene视图中直观地看到敌人的视觉范围、视野角度和最后已知位置极大方便了参数调整和逻辑调试。6. 参数调优与高级技巧一套代码写出来只是开始让AI行为显得“聪明”和“自然”全靠参数调优和一些细节处理。6.1 关键参数调优指南NavMeshAgent.radiusvsstoppingDistanceradius影响路径规划确保敌人不会贴着墙走。stoppingDistance影响最终停下的位置。对于追踪玩家stoppingDistance应等于你希望敌人保持的攻击距离。速度与角速度chaseSpeed应明显大于patrolSpeed制造紧迫感。Angular Speed不宜过低否则敌人在拐弯追你时会显得很笨拙像在“漂移”。sightRange与fieldOfViewAngle这两个值共同决定了敌人的警觉性。一个负责外围哨兵大范围、小角度一个负责正面警卫小范围、大角度。通常正面视野角度fieldOfViewAngle设置在90-120度侧面和背后的感知可以通过其他触发器Trigger Collider或听觉系统来补充。chaseRange这个值应该远大于sightRange。它决定了敌人的“执着”程度。一旦开始追踪即使玩家暂时跑出视野只要没超过这个范围敌人就会尝试追到最后的已知位置。这避免了玩家利用一个拐角就轻易甩掉敌人的情况。6.2 行为自然化技巧随机化巡逻等待时间不要使用固定的waitTimeAtPoint。可以改为Random.Range(minWaitTime, maxWaitTime)让敌人的停顿时间有变化更不易被玩家摸清规律。巡逻点随机顺序除了顺序巡逻可以增加一个“随机巡逻”模式。在SetNextPatrolPoint中不采用循环递增而是从列表中随机选择一个非当前点的目标。丢失目标后的“困惑”行为在Return状态中敌人到达_lastKnownPlayerPosition后不要立刻直线返回巡逻点。可以让他原地旋转360度看看播放一个搜寻动画或者以该点为圆心在一个小范围内随机走动几步模拟“刚才明明在这里”的困惑感然后再回归巡逻。我们在代码中使用Invoke延迟2秒切换状态就是一种简单的实现。分层追迹速度不要让追踪速度一成不变。可以实现当玩家在视野内时用最大速度chaseSpeed当玩家刚离开视野但还在chaseRange内时速度降为chaseSpeed * 0.7模拟一种不确定的搜寻状态。6.3 性能优化考量感知系统分帧更新如果你的场景中有几十上百个敌人每帧每个敌人都执行一次CanSeePlayer内含Raycast将是巨大的性能开销。一个标准的优化方案是将敌人分组每帧只更新其中一部分的感知状态。例如使用一个管理器在Update中按索引轮流更新敌人确保每个敌人每秒更新感知数次如5-10次而不是每秒60次。禁用远处敌人的NavMeshAgent对于距离玩家非常远、根本不可能与玩家交互的敌人可以直接禁用其NavMeshAgent组件和EnemyAI脚本等到玩家进入一定范围后再启用。这能节省大量的路径计算资源。使用NavMeshAgent.autoRepath这个属性默认为true当路径被动态障碍物如其他移动的NPC或玩家打开的们阻挡时Agent会自动重新计算路径。在大多数情况下保持开启即可。7. 常见问题与排查实录即使代码逻辑正确在实际运行中还是会遇到各种奇怪的问题。下面是我在项目中踩过的一些坑和解决方案。问题现象可能原因排查与解决方案敌人原地抖动或打转1. 目标点设置在NavMesh边缘或不可行走区域。2.stoppingDistance设置过小Agent在试图精确到达一个无法到达的点。3. 两个路径点距离太近且stoppingDistance大于其间距。1. 在Scene视图的Navigation窗口显示NavMesh确保所有目标点都在蓝色区域内。2. 适当增大stoppingDistance或检查目标点坐标的Y值是否与Agent所在平面一致。3. 检查路径点位置或使用Debug.DrawLine绘制路径。敌人不追踪玩家或追踪一下又停下1.CanSeePlayer方法条件太苛刻玩家短暂离开视野就立即丢失目标。2.chaseRange设置过小玩家稍微跑远就触发返回。3. 状态切换逻辑有误在Chase状态的Update中错误地调用了CheckStateTransitions导致刚切到Chase又被切走。1. 使用OnDrawGizmosSelected可视化视野范围确认玩家是否真的在范围内。可以临时放宽条件测试。2. 适当增大chaseRange或引入一个“丢失目标计时器”短暂丢失后不立即切换状态。3. 仔细检查状态机切换条件特别是Chase切换到Return的逻辑。添加详细的Debug.Log输出状态和切换原因。敌人穿墙或掉下悬崖1. NavMesh烘焙不正确墙壁或悬崖边没有被标记为障碍。2. 障碍物未标记为Navigation Static。3. Agent的Base Offset或角色碰撞体设置不当。1. 重新检查并烘焙NavMesh确保所有障碍物下方都有清晰的蓝色网格断开。2. 确认所有静态障碍物的Navigation Static已勾选。3. 检查Agent组件的Base OffsetY轴偏移确保其与角色模型底部对齐。检查角色是否拥有合适的碰撞体如Capsule Collider。多个敌人追踪时堆叠在一起Agent之间没有避让逻辑。NavMeshAgent本身具有简单的避障能力但效果有限。对于密集群体需要考虑1. 使用Unity的AI Navigation Obstacle组件NavMeshObstacle并设置为动态避障。2. 在代码中实现简单的分离力Separation Force每个Agent检查周围一定距离内的其他Agent并施加一个远离的力。3. 使用更高级的第三方AI插件如A* Pathfinding Project或RAIN AI它们内置了更完善的群体移动和避障算法。帧率下降尤其是敌人多的时候1. 每帧所有敌人都进行昂贵的感知计算如射线检测。2. 复杂的场景导致NavMesh路径计算负担重。3. 敌人数量太多。1. 实现感知分帧更新如前文所述。2. 优化NavMesh减少网格精度在Bake面板调整Voxel Size和Cell Size但会降低路径精细度。3. 实现LODLevel of Detail系统远离摄像机的敌人使用更简单的AI逻辑或更低的更新频率。4. 考虑使用对象池管理敌人而非频繁实例化/销毁。一个典型的调试流程 当敌人行为异常时我首先会打开OnDrawGizmosSelected的视觉辅助确认感知范围是否正确。然后在SwitchState方法里加入Debug.Log打印出每次状态切换的原因和时间。接着在Update函数里我会打印当前的目标点坐标和NavMeshAgent的pathStatus看看路径计算是否成功。90%的问题都能通过这三步定位到。8. 功能扩展与思路启发基础的巡逻与追踪系统搭建完成后你可以以此为骨架添加更多血肉让敌人AI更具个性和挑战性。引入听觉系统除了视觉敌人还可以有“听觉”。为玩家脚步、枪声等添加一个发出噪音的脚本。在敌人AI中增加一个HearSound(Vector3 soundPosition)方法。当噪音源在一定距离内触发时即使玩家不在视野内敌人也会将_lastKnownPlayerPosition设置为声源位置并进入Return或Chase状态取决于声音大小。这能让玩家体验“潜行”玩法。增加攻击状态在ChaseState中当_agent.remainingDistance attackRange时切换到AttackState。在攻击状态下停止移动播放攻击动画并调用伤害逻辑。攻击结束后根据玩家是否还在视野/范围内决定返回Chase还是Patrol。组队与通信实现一个简单的“警报”系统。当一名敌人发现玩家后可以通知一定范围内的其他同伴通过Physics.OverlapSphere查找同类。接收到警报的敌人即使没看到玩家也会直接朝玩家最后已知位置或警报发起者的位置移动。这能瞬间提升游戏的紧张感。差异化AI不要所有敌人都用同一套参数。可以创建不同的ScriptableObject作为“AI配置资产”为“敏捷型”、“坦克型”、“侦察兵型”敌人配置不同的速度、视野、追踪距离等。这样能丰富敌人的种类和行为。与动画系统深度集成目前我们只是简单设置了Speed参数。你可以做得更细根据_agent.velocity的大小和方向在Animator Controller中混合“走”、“跑”、“急转弯”等动画。还可以根据当前状态播放不同的姿态动画如巡逻时放松追踪时身体前倾。这套基于NavMeshAgent和状态机的AI框架其优势在于清晰的逻辑和良好的扩展性。它可能不是性能极致或行为最复杂的解决方案但对于绝大多数中小型Unity项目来说它提供了在功能、性能和开发效率之间一个极佳的平衡点。从实现第一个能巡逻和追你的“傻”敌人开始逐步为他添加听觉、攻击、协作等能力这个过程本身就是游戏开发中最有乐趣的环节之一。
