
1. 项目概述从“木桩”到“猎手”的AI进化在独立游戏开发或者任何带有NPC非玩家角色的游戏项目中最让玩家出戏的莫过于那些行为呆板的敌人。你走过去它没反应你开枪了它还在原地“思考人生”。这种体验瞬间就能把沉浸感击得粉碎。反过来一个能对环境做出合理反应会巡逻、会警觉、会穷追不舍的敌人即使模型简单也能极大提升游戏的紧张感和趣味性。今天要聊的就是如何利用Unity内置的NavMeshAgent组件配合一些简单的状态机逻辑将敌人从“木桩”升级为“智能猎手”实现一套完整的巡逻与追踪AI。NavMeshAgent是Unity导航系统Navigation System的核心组件。你可以把它理解为一个内置了“高德地图”和“自动驾驶”功能的机器人。它负责在由NavMesh导航网格即一张标记了可行走区域的地图构成的世界里自动计算从A点到B点的最优路径并控制角色沿着这条路径移动自动绕开障碍物。我们不需要手动写A*寻路算法也不需要处理复杂的物理碰撞避障这些脏活累活NavMeshAgent都包了。我们的工作就是告诉它“目标在哪”以及“在什么情况下应该切换目标”。这个项目的核心价值在于其极高的实用性和性价比。对于中小型团队或个人开发者而言从零开始打造一套鲁棒的寻路系统成本高昂。而NavMeshAgent提供了一套经过工业验证的成熟方案我们只需聚焦于游戏逻辑本身如何定义敌人的行为状态巡逻、追踪、丢失目标如何在这些状态间平滑、合理地切换。最终我们会得到一个代码结构清晰、行为可信、性能可控的敌人AI基础框架它可以直接用于FPS的僵尸、RPG的野外怪物、潜行游戏的守卫等众多场景。2. 核心思路与架构设计状态机驱动一切在动手写代码之前我们必须先理清敌人AI的行为逻辑。一个智能的敌人不应该只会做一件事它应该根据环境主要是玩家的位置在不同的行为模式间切换。最经典也最有效的设计模式就是有限状态机Finite-State Machine, FSM。我们可以将敌人的行为抽象为三个核心状态巡逻状态当玩家不在侦测范围内时敌人在预设的路径点之间循环移动表现出“例行公事”的行为。追踪状态当玩家进入侦测范围如视野或听觉范围后敌人立即放弃巡逻将移动目标设置为玩家当前位置并持续追击。返回/闲置状态当玩家脱离追踪比如跑出范围或躲藏起来一段时间后敌人应放弃无意义的追踪可以选择返回最后的已知位置查看然后恢复巡逻或者直接回到巡逻路径上。整个系统的驱动逻辑就是状态之间的转换条件。我们的代码架构将围绕一个EnemyAI脚本展开它持有NavMeshAgent组件引用并维护一个当前状态枚举。在Update函数中根据当前状态执行相应的行为逻辑并持续检查状态转换的条件。为什么选择状态机因为它直观、易于调试和扩展。每个状态的行为是独立的添加新的状态如“攻击状态”、“逃跑状态”或修改转换条件都非常方便不会让代码变成一团乱麻。这对于快速迭代的游戏开发至关重要。方案选型考量为什么不直接用NavMeshAgent.SetDestination然后不管了因为那样只会让敌人直扑目标缺少了“行为”的层次感。状态机让我们能轻松加入“警戒”、“犹豫”、“失去兴趣”等中间行为使AI显得更有“智慧”和“个性”而不是一个简单的追踪导弹。3. 环境准备与NavMesh烘焙任何基于NavMeshAgent的移动前提都是世界中有可行走的NavMesh。这需要我们在Unity编辑器中进行一次“烘焙”。3.1 场景准备与导航静态标记首先你需要构建一个简单的测试场景。放置一个平面Plane作为地面添加一些立方体Cube或导入的模型作为障碍物比如墙壁、箱子等。创建场景新建一个Unity项目3D核心模板即可在场景中创建一个平面缩放至合适大小作为地面。设置导航静态物体在Unity编辑器中选中所有不可移动的、作为行走表面或障碍物的物体包括地面和那些障碍物。在右侧检查器Inspector窗口的右上角找到“Static”下拉复选框。点击它勾选“Navigation Static”。这意味着这些物体将被纳入导航系统的计算中。注意对于复杂的模型确保其碰撞体Collider能够准确代表其物理轮廓。对于斜坡、楼梯等可能需要调整NavMesh的生成参数才能正确行走。3.2 导航网格NavMesh烘焙这是生成可行走区域地图的关键步骤。打开导航窗口点击顶部菜单栏Window-AI-Navigation。切换到“Bake”标签页。这里有很多参数但对于起步我们主要关注以下几个Agent Radius代理半径。可以理解为NavMeshAgent的“身体”宽度。这个值会影响生成的NavMesh边缘与障碍物的距离以及路径的宽度。默认值0.5通常偏大对于小型敌人可以设为0.2-0.3避免在狭窄通道卡住。Agent Height代理高度。NavMeshAgent能通过的最低空间高度。低于此高度的通道如门楣会被认为不可通过。Max Slope最大坡度。角色可以行走的最大斜坡角度超过此角度的面不会被烘焙为可行走区域。Step Height台阶高度。角色可以迈上的最大台阶高度。这是一个非常实用的参数可以让角色自动走上楼梯或低矮的台阶。设置好参数后初期可保持大部分默认点击右下角的“Bake”按钮。Unity会开始计算并在场景视图中以蓝色区域显示生成的NavMesh。蓝色区域就是你的敌人可以行走的地方。实操心得分层烘焙如果你的游戏有飞行单位、水下单位等它们对“可行走”的定义不同。你可以在“Object”标签页为不同的游戏对象设置不同的“Navigation Area”如Walkable, Not Walkable, Jump, Water等然后在“Bake”标签页为每种Area指定成本。在代码中可以为NavMeshAgent指定其可以行走的AreaMask。这属于进阶用法但规划好能极大提升AI的多样性。动态障碍物NavMesh烘焙是静态的。对于可以开关的门、可被摧毁的箱子等动态障碍物需要使用NavMeshObstacle组件。将其添加到动态障碍物上并设置好形状和大小NavMeshAgent在运行时就会自动避开它。性能复杂的场景烘焙可能较慢。在开发中期可以只烘焙当前测试的区域。对于大型开放世界通常需要将世界分割成多个NavMesh块并动态加载。4. 敌人AI脚本核心实现接下来是代码部分。我们将创建一个名为EnemyAIController的C#脚本并将其挂载到敌人角色一个带有NavMeshAgent组件的GameObject上。4.1 基础组件与状态定义首先我们需要引用必要的组件并定义状态枚举。using UnityEngine; using UnityEngine.AI; // 引入AI命名空间 public class EnemyAIController : MonoBehaviour { // 核心组件 private NavMeshAgent agent; private Transform playerTransform; // 假设玩家标签为Player // AI状态 private enum AIState { Patrol, Chase, Investigate } private AIState currentState AIState.Patrol; // 巡逻相关 public Transform[] patrolPoints; // 巡逻路径点数组在Inspector中拖拽赋值 private int currentPatrolIndex 0; public float patrolWaitTime 2f; // 到达路径点后等待时间 private float waitTimer 0f; private bool isWaiting false; // 侦测相关 public float sightRange 10f; // 视野范围 public float sightAngle 90f; // 视野角度前方扇形 public LayerMask obstacleMask; // 遮挡检测的层应包含墙壁等 private Vector3 lastKnownPlayerPosition; // 追踪丢失相关 public float chaseStopDistance 2f; // 追到多近才算“抓住” public float loseSightTime 5f; // 丢失视野后持续追踪的时间 private float loseSightTimer 0f; void Start() { agent GetComponentNavMeshAgent(); if (agent null) { agent gameObject.AddComponentNavMeshAgent(); Debug.LogWarning(NavMeshAgent component not found, added one automatically.); } // 简单查找玩家实际项目建议使用更稳健的方式如GameManager单例 GameObject player GameObject.FindGameObjectWithTag(Player); if (player ! null) playerTransform player.transform; else Debug.LogError(Player not found! Make sure your player GameObject is tagged as Player.); // 初始状态巡逻到第一个点 if (patrolPoints ! null patrolPoints.Length 0) { MoveToPoint(patrolPoints[currentPatrolIndex].position); } } }代码解析我们通过GetComponentNavMeshAgent获取组件并提供了自动添加的容错逻辑。AIState枚举清晰定义了三种状态currentState变量驱动整个AI逻辑。公开的变量如sightRange,patrolPoints允许我们在Unity编辑器中直观地调整AI行为参数这是快速迭代的关键。在Start中我们初始化并让敌人开始向第一个巡逻点移动。MoveToPoint是一个我们即将封装的方法用于安全地设置目标点。4.2 状态更新与巡逻逻辑AI的大脑在Update函数中运转。我们使用一个switch语句根据当前状态执行不同逻辑。void Update() { // 无论处于何种状态都持续检测玩家是否进入视野 bool canSeePlayer CheckForPlayer(); switch (currentState) { case AIState.Patrol: PatrolUpdate(canSeePlayer); break; case AIState.Chase: ChaseUpdate(canSeePlayer); break; case AIState.Investigate: InvestigateUpdate(canSeePlayer); break; } } // 移动封装方法避免在目标点无效时报错 void MoveToPoint(Vector3 point) { if (agent.isActiveAndEnabled agent.isOnNavMesh) { agent.SetDestination(point); } }现在实现巡逻状态的具体逻辑void PatrolUpdate(bool canSeePlayer) { // 状态转换巡逻时看到玩家立即转为追踪 if (canSeePlayer) { currentState AIState.Chase; loseSightTimer loseSightTime; // 重置丢失计时器 Debug.Log(${gameObject.name}: Player spotted! Switching to Chase.); return; // 转换状态后立即退出当前状态的更新 } // 巡逻行为逻辑 if (!isWaiting) { // 检查是否到达当前巡逻点 if (!agent.pathPending agent.remainingDistance agent.stoppingDistance) { if (!agent.hasPath || agent.velocity.sqrMagnitude 0f) { // 到达点开始等待 isWaiting true; waitTimer patrolWaitTime; } } } else { // 等待倒计时 waitTimer - Time.deltaTime; if (waitTimer 0) { isWaiting false; // 前往下一个巡逻点 currentPatrolIndex (currentPatrolIndex 1) % patrolPoints.Length; // 循环索引 MoveToPoint(patrolPoints[currentPatrolIndex].position); } } }巡逻逻辑详解转换检查每帧先判断是否看到玩家这是最高优先级的转换。移动判断agent.pathPending表示路径是否在计算中agent.remainingDistance是到当前目标点的剩余距离。我们通常用remainingDistance stoppingDistance来判断是否“到达”。但为了更精确加上对hasPath是否有路径和velocity速度的判断可以避免在复杂地形或目标点不可达时的误判。等待机制到达后设置一个等待计时器。这个简单的延迟能让巡逻行为看起来更自然而不是机器人在点与点之间瞬间“跳转”。循环路径使用取模运算%来循环访问巡逻点数组实现无限循环巡逻。4.3 玩家侦测与追踪逻辑侦测玩家是AI的“眼睛”。一个简单的距离检测太假我们实现一个更真实的扇形视野检测并考虑障碍物遮挡。bool CheckForPlayer() { if (playerTransform null) return false; Vector3 directionToPlayer playerTransform.position - transform.position; float distanceToPlayer directionToPlayer.magnitude; // 1. 距离检查 if (distanceToPlayer sightRange) return false; // 2. 角度检查前方扇形 float angleToPlayer Vector3.Angle(transform.forward, directionToPlayer); if (angleToPlayer sightAngle / 2) return false; // 3. 射线遮挡检查避免隔墙看到玩家 RaycastHit hit; if (Physics.Raycast(transform.position Vector3.up * 0.5f, // 从胸部高度发射 directionToPlayer.normalized, out hit, distanceToPlayer, obstacleMask)) { // 如果射线打到了障碍物层的东西说明视线被阻挡 if (hit.transform ! playerTransform) { return false; } } // 所有检查通过看到玩家了 lastKnownPlayerPosition playerTransform.position; // 记录最后已知位置 return true; }有了侦测追踪逻辑就清晰了void ChaseUpdate(bool canSeePlayer) { // 如果还能看到玩家就更新目标并重置计时器 if (canSeePlayer) { MoveToPoint(playerTransform.position); loseSightTimer loseSightTime; // 刷新“记忆”时间 // 检查是否追到足够近可以触发攻击等行为此处仅作示例 if (agent.remainingDistance chaseStopDistance) { // Debug.Log(Reached player! Could trigger attack here.); // 例如currentState AIState.Attack; } } else { // 看不到玩家了开始倒计时 loseSightTimer - Time.deltaTime; // 计时器归零转为调查状态 if (loseSightTimer 0) { currentState AIState.Investigate; Debug.Log(${gameObject.name}: Lost player. Switching to Investigate.); // 前往玩家最后消失的位置 MoveToPoint(lastKnownPlayerPosition); } else { // 在丢失视野的这段时间内继续朝最后已知位置移动 MoveToPoint(lastKnownPlayerPosition); } } }追踪逻辑精要持续追踪只要能看到玩家就不断将玩家的当前位置设为NavMeshAgent的目标。NavMeshAgent会自动处理路径重算。丢失处理这是让AI显得“智能”的关键。不是一丢失目标就立刻回到巡逻而是有一个“犹豫期”loseSightTime。在这段时间里AI会继续向玩家最后出现的位置移动模拟“他刚才还在这去哪了”的搜索行为。停止距离chaseStopDistance可以用来定义“抓住”玩家的距离。当距离小于此值时可以触发攻击动画、伤害判定或其它游戏逻辑。4.4 调查与状态重置逻辑调查状态是追踪和巡逻之间的缓冲地带让行为转换不那么突兀。void InvestigateUpdate(bool canSeePlayer) { // 调查过程中如果又看到玩家立刻回去追踪 if (canSeePlayer) { currentState AIState.Chase; loseSightTimer loseSightTime; Debug.Log(${gameObject.name}: Saw player again from investigation! Back to Chase.); return; } // 调查行为走到最后已知位置 if (!agent.pathPending agent.remainingDistance agent.stoppingDistance) { if (!agent.hasPath || agent.velocity.sqrMagnitude 0f) { // 到达最后已知位置停留片刻模拟查看然后回归巡逻 // 这里可以播放一个“查看四周”的动画 Debug.Log(${gameObject.name}: Investigation complete. Returning to Patrol.); currentState AIState.Patrol; // 可以选择直接前往下一个巡逻点或者回到中断的巡逻点 // 这里简单处理继续之前的巡逻循环 MoveToPoint(patrolPoints[currentPatrolIndex].position); } } }5. 参数调优与行为打磨代码框架搭建好后大部分工作在于在Unity编辑器中调整参数让AI行为符合游戏设计预期。这本身就是一个“游戏性调试”的过程。5.1 NavMeshAgent参数详解选中敌人GameObject查看NavMeshAgent组件这些参数直接影响移动表现Speed最大移动速度。巡逻时可以设低一点如2追踪时可以通过代码临时提高如5制造紧张感。Angular Speed旋转速度。决定转向的快慢。太慢会显得笨拙太快可能不自然。通常200-500之间比较合适。Acceleration加速度。值越大起步和变速越快。影响移动的“重量感”。Stopping Distance停止距离。对于巡逻点可以设一个较小值0.1对于追踪玩家可以设为攻击距离如1.5。Auto Braking自动刹车。勾选后接近目标时会减速移动更平滑。通常建议勾选。Obstacle Avoidance避障质量。设置NavMeshAgent如何避开其他同样带有NavMeshAgent的移动单位。对于大量AI同屏可以调低此值或使用分层避障来优化性能。5.2 AI行为参数调试在我们的EnemyAIController脚本中以下参数需要仔细调试sightRange(视野范围) 与sightAngle(视野角度)平衡性范围太大玩家毫无接近机会太小AI像个瞎子。角度通常设为90-120度模拟人类前方视野。调试技巧可以在OnDrawGizmosSelected方法中绘制可视化的侦测范围便于调试。void OnDrawGizmosSelected() { // 绘制视野范围球 Gizmos.color Color.yellow; Gizmos.DrawWireSphere(transform.position, sightRange); // 绘制前方扇形简化版绘制两条边 Vector3 leftBoundary Quaternion.Euler(0, -sightAngle / 2, 0) * transform.forward; Vector3 rightBoundary Quaternion.Euler(0, sightAngle / 2, 0) * transform.forward; Gizmos.color Color.green; Gizmos.DrawRay(transform.position, leftBoundary * sightRange); Gizmos.DrawRay(transform.position, rightBoundary * sightRange); }loseSightTime(丢失视野时间)这个参数直接决定了AI的“执着”程度。在恐怖游戏中可以设长一些7-10秒让压迫感持续在轻松的游戏里可以设短一些3秒给玩家更多喘息和迂回的空间。patrolWaitTime(巡逻等待时间)让AI在每个点停留的时间。不同的敌人可以有不同的“性格”严谨的守卫等待时间短1秒懒散的哨兵等待时间长4-5秒。实操心得创造“个性”通过组合调整这些参数你可以轻松创造出不同行为的敌人而不需要重写代码敏捷斥候高Speed高Angular Speed中等sightRange大sightAngle。笨重守卫低Speed低Angular Speed但sightRange很长loseSightTime也长一旦发现就很难摆脱。漫游者很短的sightRange很长的patrolWaitTime移动缓慢容易被玩家从背后偷袭。6. 常见问题、优化与扩展即使基础功能完成在实际项目中还是会遇到各种问题。这里记录一些典型的“坑”和解决方案。6.1 常见问题排查表问题现象可能原因解决方案敌人原地不动或抽搐1.NavMesh未正确烘焙。2. 目标点不在NavMesh上。3.NavMeshAgent的Base OffsetY轴偏移不合适导致采样点错误。1. 检查场景NavMesh蓝色区域是否覆盖目标点。2. 使用NavMesh.SamplePosition来确保目标点有效。3. 调整NavMeshAgent组件的Base Offset或确保角色碰撞体中心正确。敌人不追踪玩家1.playerTransform未正确赋值。2. 侦测条件过于苛刻如sightAngle太小有遮挡。3. 玩家所在层被obstacleMask遮挡。1. 检查玩家Tag或使用更可靠的查找方式如单例管理器。2. 在OnDrawGizmosSelected中可视化侦测范围调试。3. 确保玩家和障碍物设置了正确的Layer并在obstacleMask中排除玩家层。多个敌人追踪时堆叠在一起NavMeshAgent的避障功能未启用或质量太低。1. 提高NavMeshAgent的Obstacle Avoidance优先级和质量。2. 代码层面在接近玩家时可以轻微随机化每个敌人的最终目标点避免完全重合。性能开销大大量AI时每帧每个AI都在进行射线检测、路径查找等高开销操作。1.降低检测频率不用每帧Update检测用InvokeRepeating或协程每隔0.2-0.5秒检测一次。2.距离分级更新距离玩家很远的AI可以大幅降低状态更新频率甚至暂停AI逻辑。3.使用NavMeshAgent的autoRepath将其设为false在需要时手动调用SetDestination避免频繁自动重算路径。6.2 性能优化技巧对于大量使用NavMeshAgent的场景性能是需要关注的重点。代理层级管理在Navigation窗口的“Agents”标签页可以创建不同大小的代理类型。为小型、中型、大型敌人分别创建配置并在烘焙时选择对应的代理类型进行烘焙。这样可以让不同体型的AI走不同的路避免大个子卡在小巷里反复寻路消耗性能。LOD for AI仿照图形LOD多层次细节为AI也实现逻辑LOD。例如全逻辑距离玩家30米每帧更新。简逻辑距离玩家30-100米每3帧更新一次状态路径重算频率降低。休眠距离玩家100米停止所有寻路和检测只保留坐标。对象池与异步加载对于频繁生成和销毁的敌人如刷怪点使用对象池复用GameObject和NavMeshAgent组件避免Instantiate和Destroy的开销。6.3 功能扩展方向基础巡逻追踪框架之上可以添加更多层次让敌人AI更加丰富听觉系统除了视觉增加听觉侦测。当玩家开枪、跑步发出声音时即使不在视野内敌人也可以被吸引到声源位置lastKnownPlayerPosition设为声源点并进入Investigate状态。// 伪代码示例在玩家发出声音时调用 public void OnPlayerMadeNoise(Vector3 noisePosition, float noiseLoudness) { if (Vector3.Distance(transform.position, noisePosition) noiseLoudness * hearingSensitivity) { lastKnownPlayerPosition noisePosition; if (currentState AIState.Patrol) { currentState AIState.Investigate; MoveToPoint(noisePosition); } } }视野锥边缘模糊当前的视野检测是“全有或全无”。可以引入一个“警觉度”概念。在视野边缘发现玩家时警觉度缓慢上升播放一个“”动画达到阈值后再转为追踪。这给了玩家一个反应的机会。团队AI与通信当一个敌人发现玩家后可以通知一定范围内的其他同伴通过发送消息或事件让它们也进入追踪状态实现“一人发现全员警戒”的效果。复杂巡逻路径当前的巡逻点是简单的顺序循环。可以扩展为随机选择下一个点或者根据预设的“巡逻路线”ScriptableObject来定义更复杂的行为如在某些点停留并播放观望动画。这套基于NavMeshAgent和简单状态机的敌人AI系统其优势在于快速原型、易于理解和足够健壮。它可能不是性能极致或行为最复杂的解决方案但对于绝大多数中小型游戏项目而言它提供了所需功能的80%而剩下的20%可以通过上述的扩展和调优来不断丰富。从“木桩”到“猎手”关键的飞跃往往就来自于这些对细节的雕琢和对行为逻辑的合理设计。