Unity游戏AI实战:从零搭建潜行敌人感知与状态机系统
最近顽皮狗官方确认《最后生还者》系列还有更多游戏项目正在开发中而且这次明确不是电视剧内容。消息一出玩家社区里关于“艾莉之后的故事会怎么展开”“新作会不会用全新主角”的讨论就没停过。不过作为一个常年关注游戏开发技术的博主我看到的反而是另一层信息顽皮狗能够同时推进多个高质量项目背后那套成熟的 AI 行为设计、电影化叙事流程和技术管线才是真正值得开发者学习的东西。这篇文章不聊剧情猜测我们单纯从技术角度拆解《最后生还者》这类第三人称叙事驱动游戏的核心玩法实现。我会结合 Unity 引擎带大家从零搭建一个包含敌人 AI 感知、潜行、同伴跟随和简单状态机的可运行 Demo。无论你是刚接触游戏 AI 开发的初学者还是在公司里负责玩法系统设计的开发者都能在这篇文章里找到可以直接落地的思路和代码。1. 背景与核心概念1.1 《最后生还者》的技术标签叙事驱动的 AI 设计顽皮狗的游戏之所以能被玩家记住除了剧情演出之外还有一个容易被忽略的技术核心AI 系统。在《最后生还者》中敌人不会像传统射击游戏那样傻站在掩体后面对射他们会根据玩家的脚步声、枪声、视线范围和环境互动做出复杂反应。感染者会循着声音搜索玩家位置人类敌人会相互掩护、包抄侧翼。这些行为的底层其实就是一套由“感知系统 状态机 行为选择”组成的 AI 架构。对开发者来说直接复刻顽皮狗的完整 AI 系统不现实但我们可以提炼出一套适用于中小型项目的简化模型。这套模型通常包含感知层负责收集视觉、听觉信息。决策层根据感知结果切换行为状态。行为层执行巡逻、追踪、攻击、搜索等具体动作。动画层根据行为状态播放对应动画。理解了这个分层结构后续无论是用 Unity、Unreal 还是 Godot都能快速迁移思路。1.2 为什么顽皮狗的 AI 会让人觉得“聪明”很多开发者第一次玩《最后生还者》时会觉得敌人 AI 特别“聪明”但实际上他们并不是真的具备高级推理能力。顽皮狗的做法是用大量的细节规则去模拟真实行为。比如敌人在听到声音后不会立刻锁定玩家位置而是先朝声源方向警戒一段时间。同伴 AI 不会主动暴露玩家位置会尽量保持安静并跟随玩家路径。敌人视线被障碍物遮挡时会丢失目标并进入搜索状态。这些设计本质上是对“信息不确定性”的模拟。在实现层面就是要给 AI 的感知系统加入“置信度”和“遗忘时间”的概念。1.3 本文要动手做什么为了更直观地讲解我会带大家用 Unity 实现一个简化版“循声者”敌人 AI敌人拥有“巡逻 / 警觉 / 追踪 / 攻击”四种状态。敌人能听到玩家的脚步声距离越近声音越大。玩家可以蹲下潜行来降低脚步声从而避开敌人。一个简单的同伴 NPC 会跟随玩家并在玩家潜行时也保持安静。最终效果是一个可以运行的小场景你操控玩家角色穿过有敌人巡逻的走廊需要利用潜行机制绕过敌人同伴会自动跟随你的路径。整个过程全部采用 Unity 原生组件实现不依赖任何付费插件。2. 环境准备与版本说明2.1 引擎与工具版本本文示例以 Unity 开发环境为主。为避免版本差异导致的配置问题建议使用以下环境工具版本建议说明Unity Editor2022.3 LTS 或更新长期支持版本稳定性更好Visual Studio2022 或 VS Code用于 C# 脚本编写操作系统Windows / macOS / LinuxUnity 均支持建模工具Blender可选仅用于替换占位模型版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。如果你使用的是 Unity 2021 或 Unity 6核心 API 基本通用个别命名空间可能需要根据官方文档微调。2.2 项目结构规划为了保持代码清晰我推荐在 Unity 中建立以下目录结构Assets/ ├── Scripts/ │ ├── AI/ │ │ ├── EnemyController.cs │ │ ├── EnemyPerception.cs │ │ └── EnemyStateMachine.cs │ ├── Player/ │ │ ├── PlayerController.cs │ │ └── PlayerNoiseEmitter.cs │ └── Companion/ │ └── CompanionFollow.cs ├── Scenes/ │ └── DemoScene.unity └── Prefabs/ ├── Enemy.prefab ├── Player.prefab └── Companion.prefab这里的PlayerNoiseEmitter.cs是一个比较关键的设计玩家移动时不再直接通知敌人而是通过一个“噪声源”组件广播自己的声音强度敌人的感知系统再去监听。这是一个低耦合设计在项目变大之后会很好维护。2.3 场景搭建所需基础对象为了快速跑通 Demo我们可以用 Unity 内置的几何体代替正式美术资源玩家一个 Capsule胶囊体 一个小方块作为“面向”标识。敌人一个 Cylinder圆柱体 圆锥作为“视线方向”标识。同伴一个 Capsule使用不同的颜色材质区分。场景一个 Plane 作为地面再加上几个 Cube 作为掩体。不需要导入外部素材所有资源都在 Unity 菜单GameObject 3D Object里创建。3. 核心机制原理拆解3.1 NPC 感知系统如何“看到”和“听到”玩家NPC 感知系统是 AI 行为的基础。在《最后生还者》里敌人的感知分为视觉和听觉两条通道。视觉系统包含视野角度、可视距离和遮挡检测听觉系统包含声源距离和声源强度。先看视觉感知的核心代码思路。我们需要让敌人只看到“视野范围内 没有遮挡 距离足够近”的对象。// 文件路径Assets/Scripts/AI/EnemyPerception.cs using UnityEngine; public class EnemyPerception : MonoBehaviour { [Header(视觉参数)] public float viewRadius 10f; // 可视半径 public float viewAngle 110f; // 视野角度 public LayerMask targetMask; // 目标层级 public LayerMask obstacleMask; // 障碍物层级 [Header(听觉参数)] public float hearRadius 6f; // 可听半径 public float hearingSensitivity 1f; // 听觉灵敏度 public Vector3 lastKnownTargetPosition { get; private set; } // 检测玩家是否在视野中 public bool CanSeeTarget(Transform target) { if (target null) return false; Vector3 directionToTarget target.position - transform.position; float distance directionToTarget.magnitude; // 距离检测 if (distance viewRadius) return false; // 角度检测 Vector3 forward transform.forward; float angle Vector3.Angle(forward, directionToTarget); if (angle viewAngle * 0.5f) return false; // 遮挡检测 if (Physics.Raycast(transform.position, directionToTarget.normalized, out RaycastHit hit, distance, obstacleMask)) { return false; } // 额外判断目标身上是否有“可被发现”标记 if (target.TryGetComponentPlayerNoiseEmitter(out _)) { lastKnownTargetPosition target.position; return true; } return false; } // 检测是否听到声源 public bool CanHearTarget(Vector3 targetPosition, float noiseRange) { float distance Vector3.Distance(transform.position, targetPosition); // noiseRange 是声源的最大传播距离 if (distance noiseRange) return false; // 距离越近听得越清楚 float intensity 1f - (distance / noiseRange); return intensity * hearingSensitivity 0.3f; } // 供外部调用的获取目标方法用于 Gizmos 调试 public void OnDrawGizmosSelected() { UnityEditor.Handles.color Color.yellow; UnityEditor.Handles.DrawWireArc(transform.position, Vector3.up, Quaternion.Euler(0, -viewAngle * 0.5f, 0) * transform.forward, viewAngle, viewRadius); UnityEditor.Handles.color Color.red; UnityEditor.Handles.DrawWireArc(transform.position, Vector3.up, Vector3.forward, 360f, hearRadius); } }这段代码的关键点有三个角度检测使用Vector3.Angle(forward, directionToTarget)判断目标是否在视野角内。遮挡检测通过Physics.Raycast从 AI 位置向目标发射射线如果中间有障碍物就视为不可见。听觉判定用noiseRange表示声源传播范围AI 与声源距离越近听到的概率越高。注意CanSeeTarget中有一步target.TryGetComponentPlayerNoiseEmitter(out _)这其实是一个简化处理。在正式项目中更推荐通过 LayerMask 来区分目标这样性能更好。3.2 状态机从“巡逻”到“追踪”的决策逻辑状态机是 NPC 决策层的核心。一个标准的敌人 AI 状态包含进入状态时执行的操作。状态持续期间每帧更新的操作。切换到其他状态的条件。离开状态时执行的操作。我们用枚举定义状态然后在EnemyStateMachine中实现状态切换逻辑。为了让代码更易读这里使用简单的switch结构管理复杂项目可以改用状态模式或行为树但原理是一致的。// 文件路径Assets/Scripts/AI/EnemyStateMachine.cs using UnityEngine; public enum EnemyState { Patrol, // 巡逻 Alert, // 警觉 Chase, // 追踪 Attack // 攻击 } public class EnemyStateMachine : MonoBehaviour { public EnemyState currentState { get; private set; } [Header(行为参数)] public float patrolSpeed 2f; public float chaseSpeed 5f; public float alertDuration 3f; public float attackDistance 1.5f; [Header(引用)] public Transform player; public Transform[] patrolPoints; public EnemyPerception perception; public UnityEngine.AI.NavMeshAgent agent; public Animator animator; private int currentPatrolIndex 0; private float alertTimer 0f; private void Start() { agent GetComponentUnityEngine.AI.NavMeshAgent(); int state (int)EnemyState.Patrol; EnterState(EnemyState.Patrol); } private void Update() { switch (currentState) { case EnemyState.Patrol: UpdatePatrol(); break; case EnemyState.Alert: UpdateAlert(); break; case EnemyState.Chase: UpdateChase(); break; case EnemyState.Attack: UpdateAttack(); break; } } private void EnterState(EnemyState newState) { // 离开旧状态时的清理逻辑 switch (currentState) { case EnemyState.Chase: agent.isStopped false; break; } currentState newState; switch (newState) { case EnemyState.Patrol: agent.speed patrolSpeed; GoToNextPatrolPoint(); break; case EnemyState.Alert: agent.isStopped true; alertTimer 0f; break; case EnemyState.Chase: agent.isStopped false; agent.speed chaseSpeed; break; case EnemyState.Attack: agent.isStopped true; break; } } private void UpdatePatrol() { // 巡逻过程中持续感知 if (perception.CanSeeTarget(player)) { EnterState(EnemyState.Chase); return; } if (player ! null perception.CanHearTarget(player.position, 5f)) { EnterState(EnemyState.Alert); return; } // 到达巡逻点后切换到下一个点 if (agent.remainingDistance 0.5f) { GoToNextPatrolPoint(); } } private void UpdateAlert() { alertTimer Time.deltaTime; // 警觉状态下保留视觉检测 if (perception.CanSeeTarget(player)) { EnterState(EnemyState.Chase); return; } // 警觉结束后回到巡逻 if (alertTimer alertDuration) { EnterState(EnemyState.Patrol); } } private void UpdateChase() { // 追踪时不断更新目标位置 agent.SetDestination(player.position); if (Vector3.Distance(transform.position, player.position) attackDistance) { EnterState(EnemyState.Attack); return; } // 如果丢失目标太久回到警觉状态 if (!perception.CanSeeTarget(player) !perception.CanHearTarget(player.position, 5f)) { EnterState(EnemyState.Alert); } } private void UpdateAttack() { if (Vector3.Distance(transform.position, player.position) attackDistance) { EnterState(EnemyState.Chase); } } private void GoToNextPatrolPoint() { if (patrolPoints.Length 0) return; agent.SetDestination(patrolPoints[currentPatrolIndex].position); currentPatrolIndex (currentPatrolIndex 1) % patrolPoints.Length; } }这段状态机代码虽然简单但已经覆盖了 AI 开发中最常见的模式巡逻期间持续感知优先级是视觉大于听觉警觉状态是一个缓冲区用来模拟“好像听到了什么”的效果追踪状态会持续更新目标位置直到靠近后进入攻击状态。3.3 玩家的“声音发射器”设计在传统游戏开发中很多初学者会让 AI 直接监听玩家身上的碰撞体或者直接读取玩家坐标。这种方式有一个问题如果后期加入更多 NPC、更多 AI 逻辑代码会变得极其混乱。更合理的做法是“声音事件广播”。我们来写一个PlayerNoiseEmitter组件。它的职责只有一个根据玩家的移动速度计算出当前产生的噪音大小并发送给周围的 AI 感知器。// 文件路径Assets/Scripts/Player/PlayerNoiseEmitter.cs using UnityEngine; public class PlayerNoiseEmitter : MonoBehaviour { [Header(噪声参数)] public float walkNoise 3f; // 行走时的噪声范围 public float runNoise 8f; // 奔跑时的噪声范围 public float crouchNoise 1f; // 潜行时的噪声范围 private CharacterController characterController; private PlayerController playerController; // 来自我们后续写的玩家控制脚本 private void Awake() { characterController GetComponentCharacterController(); playerController GetComponentPlayerController(); } // 返回当前噪声强度供 AI 感知系统查询 public float GetCurrentNoiseRange() { if (playerController null) return walkNoise; if (playerController.IsCrouching) return crouchNoise; float speed characterController.velocity.magnitude; if (speed 0.1f) return 0f; // 站立不动 if (speed 4f) return walkNoise; return runNoise; } // 可视化调试 private void OnDrawGizmosSelected() { Gizmos.color Color.cyan; Gizmos.DrawWireSphere(transform.position, GetCurrentNoiseRange()); } }这个脚本本身不需要主动调用物理系统敌人 AI 会在每帧 Update 中主动查询玩家身上的GetCurrentNoiseRange()然后结合CanHearTarget判断是否听到声音。这种“查询式”的事件驱动方式是 Unity 中比较轻量的一种选择。如果项目规模变大也可以改用UnityEngine.Events或者消息系统来做事件广播效果更好但代码复杂度会上升。3.4 同伴跟随 AI保持冷静不暴露玩家在《最后生还者》里同伴的行为设计非常讲究他们会跟着主角走但不会在潜行时乱跑也不会挡在敌人枪口前面。实现一个像样的同伴 AI 其实不复杂核心是把“跟随”和“避险”结合起来。// 文件路径Assets/Scripts/Companion/CompanionFollow.cs using UnityEngine; using UnityEngine.AI; public class CompanionFollow : MonoBehaviour { [Header(跟随参数)] public Transform player; public float followDistance 2f; public float updateInterval 0.5f; public float avoidDistance 1.2f; private NavMeshAgent agent; private PlayerNoiseEmitter playerNoise; private void Start() { agent GetComponentNavMeshAgent(); playerNoise player.GetComponentPlayerNoiseEmitter(); } private void Update() { if (player null) return; // 如果玩家正在潜行同伴也保持安静 if (playerNoise ! null playerNoise.GetCurrentNoiseRange() 1f) { agent.speed 1.5f; } else { agent.speed 3f; } // 计算跟随目标位置玩家背后偏移一点 Vector3 offset -player.forward * followDistance; Vector3 targetPosition player.position offset; // 避让障碍物如果目标位置被遮挡直接站到玩家旁边 if (Physics.Raycast(player.position, offset.normalized, out RaycastHit hit, followDistance)) { targetPosition player.position (hit.point - player.position).normalized * avoidDistance; } agent.SetDestination(targetPosition); } }这里有一个很实用的技巧offset -player.forward * followDistance让同伴始终站在玩家背后方向而不是直接站在玩家当前位置。这样在过转角时同伴不会被卡在墙角。3.5 潜行与掩体系统潜行系统本身不复杂但它连接了玩家控制、动画状态、敌人 AI 等多个模块。我们的简化设计只需要做到两点玩家按下Ctrl键切换站立 / 蹲下状态。蹲下状态下PlayerNoiseEmitter返回较小的噪声范围同时玩家移动速度降低。掩体系统在正式项目中会涉及物理射线检测和相机联动这里就不展开实现只说明思路掩体检测一般是从玩家位置向外发射一圈射线找到最近的掩体边缘然后把玩家吸附到掩体边缘位置。Unity 的 Character Controller 组件和Physics.SphereCast是常用实现方案。4. 完整实战案例搭建一个可运行 Demo4.1 Demo 目标我们最终要实现这样的效果玩家使用 WASD 移动鼠标控制朝向。按Ctrl或C键进入潜行状态移动速度降低。场景中有一个敌人按设定路线巡逻。当玩家奔跑经过敌人附近时敌人听到声音并向玩家位置警觉。当敌人看到玩家时进入追踪状态靠近后攻击。同伴 NPC 自动跟随玩家并随玩家一起潜行。4.2 创建场景和基础物体打开 Unity新建一个 3D 项目然后按以下步骤操作在 Hierarchy 窗口右键创建 Plane 作为地面。创建几个 Cube随意摆放作为掩体墙。创建 Capsule 并命名 Player添加 Character Controller 组件。创建 Cylinder 并命名 Enemy添加 NavMeshAgent 组件。创建 Capsule 并命名 Companion添加 NavMeshAgent 组件。给 Player 和 Companion 分配不同颜色的材质方便区分。摆放完成后地面还是无法寻路因为需要先构建 NavMesh。点击菜单Window AI Navigation在 Navigation 面板里选择 Bake 按钮就会在地面上生成蓝色寻路区域。注意掩体 Cube 需要设置为静态物体Static否则烘焙结果会不对。4.3 编写玩家控制脚本玩家控制脚本会处理移动、转向和潜行切换。为了让代码更完整这里给出一个可直接使用的版本// 文件路径Assets/Scripts/Player/PlayerController.cs using UnityEngine; [RequireComponent(typeof(CharacterController))] [RequireComponent(typeof(PlayerNoiseEmitter))] public class PlayerController : MonoBehaviour { [Header(移动参数)] public float walkSpeed 3f; public float runSpeed 6f; public float crouchSpeed 1.5f; public float turnSpeed 15f; [Header(潜行)] public bool IsCrouching { get; private set; } public KeyCode crouchKey KeyCode.C; private CharacterController controller; private PlayerNoiseEmitter noiseEmitter; private void Awake() { controller GetComponentCharacterController(); noiseEmitter GetComponentPlayerNoiseEmitter(); } private void Update() { // 切换潜行 if (Input.GetKeyDown(crouchKey)) { IsCrouching !IsCrouching; } // 计算移动方向 float horizontal Input.GetAxis(Horizontal); float vertical Input.GetAxis(Vertical); Vector3 direction new Vector3(horizontal, 0f, vertical).normalized; // 根据状态选择速度 float speed runSpeed; if (IsCrouching) speed crouchSpeed; else if (Input.GetKey(KeyCode.LeftShift)) speed runSpeed; else speed walkSpeed; // 应用移动 Vector3 move direction * speed; controller.Move(move * Time.deltaTime); // 角色朝向移动方向仅在移动时旋转 if (direction.magnitude 0.1f) { Quaternion targetRotation Quaternion.LookRotation(direction, Vector3.up); transform.rotation Quaternion.Slerp(transform.rotation, targetRotation, turnSpeed * Time.deltaTime); } } private void OnControllerColliderHit(ControllerColliderHit hit) { // 简单实现碰撞时不再移动防止穿过掩体 // 正式项目建议使用 Physics.SphereCast 做移动检测 } }这个脚本用了CharacterController.Move而不是transform.Translate优点是自带碰撞检测可以避免玩家穿过掩体。4.4 编写敌人 AI 状态机与感知逻辑敌人的代码分为两个脚本EnemyPerception.cs负责感知EnemyStateMachine.cs负责状态决策。这两个脚本在第三节已经给过核心代码这里做两处补充EnemyStateMachine的Update中需要补充对PlayerNoiseEmitter的查询。为了能实际看到状态切换我们在 Enemy 上添加一个简单的颜色变化显示状态。// 在 EnemyStateMachine.Update 的 UpdatePatrol 中替换声音检测部分 private void UpdatePatrol() { if (perception.CanSeeTarget(player)) { EnterState(EnemyState.Chase); return; } // 从玩家身上获取噪声范围 PlayerNoiseEmitter noise player.GetComponentPlayerNoiseEmitter(); if (noise ! null perception.CanHearTarget(player.position, noise.GetCurrentNoiseRange())) { EnterState(EnemyState.Alert); return; } if (agent.remainingDistance 0.5f) { GoToNextPatrolPoint(); } }4.5 部署到 Unity 并连接引用脚本写好之后还需要在 Inspector 面板中拖拽引用选中 Enemy 物体在 EnemyStateMachine 组件中把player拖到 Player 物体上。在patrolPoints数组中添加 2~3 个空物体作为巡逻点。perception字段拖入 Enemy 身上的 EnemyPerception 组件。agent字段会自动获取因为我们在代码里使用了GetComponent。跟随玩家将 CompanionFollow 的player指向 Player。4.6 运行与预期结果点击 Play 运行场景预期效果如下玩家站在起点敌人沿巡逻点来回巡逻。玩家快速按住 Shift 跑向敌人敌人会在跑动声音进入听阈后进入 Alert 状态颜色变橙。玩家继续跑进敌人视野敌人进入 Chase 状态颜色变红开始追击。玩家按下 C 蹲下并远离敌方视野敌人会进入 Alert 状态并在几秒后回到 Patrol。如果一切正常你会在 Console 窗口看到我们写的 Debug.Log 输出如果需要可视化信息可以在状态切换处加一行日志。5. 常见问题与排查思路在实际开发中AI 系统是 bug 高发区域。这里整理几个高频问题问题现象常见原因解决思路敌人不移动原地发呆NavMeshAgent 没有正确烘焙 NavMesh检查 Window AI Navigation 是否烘焙完成敌人能“穿墙”看到玩家遮挡检测射线没有检测到障碍物确认掩体物体是否勾选 Static并配置 obstacleMask玩家蹲下后敌人仍然追杀潜行时噪音范围没有被正确传入敌人检查 PlayerNoiseEmitter 的返回值和 CanHearTarget 逻辑同伴与玩家重叠碰撞跟随目标点被玩家自身阻挡使用-player.forward偏移量并配合物理测试调整 followDistanceAI 状态切换过于频繁听觉阈值设置太低把 hearingSensitivity 调低或把 noiseRange 数值改小动画不切换Animator 参数与代码参数不一致在 Unity 中检查 Animator 参数名称并同步枚举攻击状态下不播放攻击动画缺少攻击动画层简化处理时可以直接播放默认动画或使用 Debug.Log 代替排查 AI 问题时建议按“感知 - 决策 - 行为 - 表现”的顺序检查。先确认敌人是否能“感知”到玩家再确认状态是否切换最后才看表现层动画。很多问题最终都出在感知层数据没传对。6. 最佳实践与工程建议6.1 AI 感知参数不要魔法数字我在示例代码里直接写了很多数值比如viewRadius 10f、hearRadius 6f。在 Demo 阶段没问题项目变大后就会出现“调了这个数值忘了那个数值”的情况。建议把所有感知参数收集到一个可配置的数据类中或者使用 Unity 的 ScriptableObject 来管理。// 文件路径Assets/Scripts/AI/EnemyPerceptionConfig.cs using UnityEngine; [CreateAssetMenu(fileName EnemyPerceptionConfig, menuName AI/Perception Config)] public class EnemyPerceptionConfig : ScriptableObject { public float viewRadius 10f; public float viewAngle 110f; public float hearRadius 6f; public float hearingSensitivity 1f; public float alertDuration 3f; public float attackDistance 1.5f; }这样做的好处是策划同学可以单独调整参数不需要改代码。6.2 不要每帧发射大量射线在玩家机器上黄点的敌人 AI 可能只有几个每帧射线不会成为瓶颈。但如果你把同样逻辑用到 50 个 NPC 上性能就会急剧下降。优化思路降低感知检测频率比如每 0.2 秒检测一次而不是每帧检测。使用Physics.OverlapSphereNonAlloc批量获取周围对象再逐个判断距离和角度。对远处的 AI 使用 LOD 简化距离远的 AI 不检测视野只检测听觉。6.3 状态机扩展性考虑用行为树替代示例中的switch状态机适合状态少、逻辑简单的场景。如果敌人需要结合“逃跑、呼叫同伴、掩体切换、投掷物”等复杂行为建议学习行为树Behavior Tree。Unity 自带的 NavMesh 组件 行为树插件如 Behavior Designer是目前商业项目常用的组合。6.4 安全与合规不要设计“必死”陷阱在开发游戏 AI 时有一个设计原则需要重视玩家永远有可用的应对策略。如果敌人视距过长、听觉过灵敏玩家就会陷入“必死”循环。顽皮狗的关卡设计就很注重“试探性失败”让玩家逐渐摸清 AI 规则后完成反制。在做 Demo 时也要注意把潜行作为一种正反馈机制来设计而不是把潜行做成惩罚机制。7. 总结与学习路线这篇文章围绕着顽皮狗《最后生还者》项目开发背后的技术核心拆解了叙事驱动游戏中常见的 AI 感知、状态机、潜行和同伴跟随系统。我们用 Unity 实现了一个简化版 Demo核心代码包括EnemyPerception视觉角度检测 听觉噪声检测 遮挡判断。EnemyStateMachine巡逻 / 警觉 / 追踪 / 攻击四种状态。PlayerNoiseEmitter根据移动状态生成不同噪声范围。CompanionFollow简单有效的同伴跟随逻辑。如果你能把这套逻辑完整跑通下一步可以这样进阶学习行为树把状态机替换成可视化行为树。增加掩体切换让 AI 在受到攻击时主动寻找掩体。加入群体 AI 通信让附近敌人响应同伴的警报。结合动画状态机让 AI 行为与最终表现完全匹配。顽皮狗能够同时推进多个《最后生还者》项目背后是他们积累多年的 AI 行为设计能力与技术管线。我们虽然没法一夜之间复制他们的完整方案但从感知、决策、执行这个分层模型开始一步一步搭建属于你自己的 AI 体系这条路是每个游戏开发者都能走的。如果有不清楚的地方欢迎在评论区留言交流也可以把你跑通 Demo 的截图发出来大家一起讨论优化。