的实战重构)
1. 项目概述为什么敌人AI需要重构在Unity游戏开发里敌人AI人工智能的构建往往是决定游戏体验深度和可玩性的关键。很多开发者尤其是刚入门的同学可能都是从最基础的“if-else”状态判断开始的。比如敌人看到玩家就追击进入攻击范围就攻击血量低了就逃跑。这种写法在小体量原型阶段确实快但随着功能增加代码很快就会变成一团乱麻各种状态互相嵌套改一处而动全身调试起来更是噩梦。我自己就踩过这个坑。早期一个项目里一个“精英怪”的AI脚本膨胀到了上千行里面塞满了布尔变量和条件判断。想给它加一个“被击晕后短暂发呆”的新状态我得在追击、攻击、逃跑等五六个地方都加上对“是否被击晕”的判断漏掉一处就可能产生诡异的BUG比如一边被击晕一边追着你砍。这种代码的维护成本会随着开发进程呈指数级上升。这时状态机Finite State Machine, FSM就成为了一个更优雅的解决方案。它把每个行为如巡逻、追击、攻击抽象成一个独立的“状态”并明确定义状态之间转换的条件。代码结构瞬间清晰了很多。但传统的FSM也有其局限性当状态本身又包含复杂的子行为时比如“攻击”状态里又包含“举起武器”、“挥砍”、“收招”三个子阶段用平铺的FSM来实现又会变得臃肿。于是分层状态机Hierarchical Finite State Machine, HFSM应运而生。它允许你在一个状态内部再嵌套一个完整的状态机。这就好比公司的组织架构CEO顶层状态下面有技术部、市场部子状态机技术部里又有开发组、测试组更深层的子状态。用HFSM来重构敌人AI正是为了解决复杂行为逻辑的模块化问题让代码像乐高积木一样可以灵活组合和调试这也是本次实战的核心目标。2. HFSM核心概念与设计思路拆解在动手写代码之前我们必须把HFSM的几个核心概念和设计思路吃透。这决定了我们最终实现的框架是否健壮和易用。2.1 状态机的“分层”到底意味着什么分层本质上是为了管理复杂度。我们以一个经典的“城堡守卫”敌人为例它的顶层行为可以划分为三个状态巡逻Patrol、战斗Combat、死亡Death。巡逻状态相对简单可能就是一个子状态机包含“沿路径点移动”和“原地停留观察”两个子状态。战斗状态这是最复杂的部分。它本身应该是一个状态机内部包含诸如追击Chase、攻击Attack、防御Defend、撤退Retreat等子状态。死亡状态播放死亡动画销毁或禁用GameObject。这里的“战斗”就是一个父状态Parent State它内部管理着自己的子状态机。父状态可以拥有自己的OnEnter进入战斗模式播放战吼、OnUpdate检查是否脱离战斗和OnExit退出战斗重置仇恨逻辑。同时它负责管理和驱动其内部的子状态如追击、攻击的运行和切换。这种设计的巨大优势在于关注点分离和状态复用。追击和攻击的逻辑只关心自己在战斗上下文中的行为而“是否进入战斗”、“是否死亡”这些更高层的决策则由顶层状态机来管理。你可以轻松地把一套设计好的“战斗子状态机”复用到不同的敌人类型上只需调整顶层状态比如有的敌人没有“巡逻”直接就是“战斗”。2.2 我们的HFSM框架设计蓝图基于以上概念我们将设计一个轻量级、非可视化代码驱动的HFSM框架。它包含以下几个核心类IState接口所有状态的基类定义OnEnter,OnUpdate,OnExit三个基本生命周期方法。StateMachine类状态机的核心管理器。它维护当前状态并驱动状态的切换和更新。关键点在于它本身也可以实现IState接口。这样一个StateMachine实例就可以作为另一个StateMachine的一个状态从而实现分层。Transition类状态转换的封装。它包含一个条件Condition返回bool的委托或函数和一个目标状态ID。当条件满足时状态机就会执行转换。State基类一个可继承的抽象类实现了IState并内置了对子状态机StateMachine的引用和管理逻辑。这是实现分层的关键。这个设计模式通常被称为“状态模式State Pattern”与“组合模式Composite Pattern”的结合。状态模式让每个状态成为独立的对象组合模式则让状态机可以嵌套。设计心得为什么不直接用Animator ControllerUnity自带的Animator本身就是一个强大的状态机并且支持子状态机Sub-State Machine。对于动画驱动非常紧密的简单AIAnimator是绝佳选择。但对于逻辑复杂、需要大量自定义数据交互和复杂条件判断的AI如RTS游戏中的单位、拥有复杂技能树的Boss纯代码实现的HFSM往往更灵活、更易于调试和版本管理。你可以用Debug.Log精准输出状态流转也可以轻松地将AI逻辑与网络同步数据结合。3. HFSM框架完整代码实现与解析理论讲完我们进入实战环节。下面将一步步实现这个HFSM框架并附上详尽的代码注释。3.1 定义核心接口与抽象类首先创建IState.cs和State.cs。// IState.cs public interface IState { // 状态进入时调用 void OnEnter(); // 状态每帧更新时调用 void OnUpdate(float deltaTime); // 状态退出时调用 void OnExit(); }// State.cs using System.Collections.Generic; public abstract class State : IState { // 状态标识符用于查找和切换 public string Name { get; private set; } // 该状态所属的状态机父状态机 public StateMachine ParentStateMachine { get; set; } // 该状态可能拥有的子状态机实现分层的关键 protected StateMachine subStateMachine; // 从该状态出发的所有状态转换条件 protected ListTransition transitions new ListTransition(); protected State(string name) { Name name; } // 添加一个从本状态到其他状态的转换条件 public void AddTransition(IState to, System.Funcbool condition) { transitions.Add(new Transition(to, condition)); } // 子类必须实现的三个生命周期方法 public virtual void OnEnter() { // 进入状态时如果有子状态机则启动子状态机的默认状态 if (subStateMachine ! null) { subStateMachine.Start(); } } public virtual void OnUpdate(float deltaTime) { // 更新时首先检查所有转换条件 CheckTransitions(); // 然后更新子状态机如果存在 if (subStateMachine ! null) { subStateMachine.OnUpdate(deltaTime); } } public virtual void OnExit() { // 退出状态时如果有子状态机则停止它 if (subStateMachine ! null) { subStateMachine.Stop(); } } // 检查所有转换条件如果满足则触发状态切换 protected void CheckTransitions() { foreach (var transition in transitions) { if (transition.condition()) { ParentStateMachine?.ChangeState(transition.toState); break; // 一次只处理一个转换 } } } // 内部类封装单个转换条件 protected class Transition { public IState toState; public System.Funcbool condition; public Transition(IState to, System.Funcbool cond) { toState to; condition cond; } } }关键解析ParentStateMachine让每个状态都知道自己属于哪个“上级”这样在CheckTransitions中才能通知上级状态机进行切换。subStateMachine这是实现“分层”的核心。一个State可以持有一个StateMachine作为其子状态机。OnUpdate的调用顺序先检查自身状态的转换条件再更新子状态机。这个顺序很重要它保证了状态转换的优先级高于子状态的运行。3.2 实现状态机管理器接下来是大脑中枢StateMachine.cs。// StateMachine.cs using System.Collections.Generic; public class StateMachine : IState { // 状态字典通过名称快速查找状态 private Dictionarystring, IState states new Dictionarystring, IState(); // 当前活跃状态 private IState currentState; // 默认状态用于启动状态机 private IState defaultState; // 状态机是否正在运行 private bool isRunning false; // 添加一个状态到状态机 public void AddState(IState state) { states[state.GetType().Name] state; // 如果该状态是我们自定义的State类型设置其父状态机为当前实例 if (state is State baseState) { baseState.ParentStateMachine this; } } // 设置默认状态 public void SetDefaultState(IState state) { defaultState state; AddState(state); // 确保默认状态已被添加 } // 启动状态机进入默认状态 public void Start() { if (defaultState null) { UnityEngine.Debug.LogError(StateMachine has no default state set!); return; } isRunning true; ChangeState(defaultState); } // 停止状态机退出当前状态 public void Stop() { isRunning false; currentState?.OnExit(); currentState null; } // 状态机每帧更新 public void OnUpdate(float deltaTime) { if (!isRunning || currentState null) return; currentState.OnUpdate(deltaTime); } // 核心方法切换状态 public void ChangeState(IState newState) { if (newState null || newState currentState) return; // 退出旧状态 currentState?.OnExit(); // 进入新状态 currentState newState; currentState.OnEnter(); // 打印日志调试神器 UnityEngine.Debug.Log($State changed to: {currentState.GetType().Name}); } // 通过状态名称切换状态方便外部调用 public void ChangeState(string stateName) { if (states.TryGetValue(stateName, out IState state)) { ChangeState(state); } else { UnityEngine.Debug.LogWarning($State {stateName} not found!); } } // IState接口实现让StateMachine本身也能作为另一个StateMachine的一个状态 void IState.OnEnter() Start(); void IState.OnUpdate(float deltaTime) OnUpdate(deltaTime); void IState.OnExit() Stop(); }关键解析ChangeState方法这是状态流转的枢纽。它严格遵循“先退出旧状态再进入新状态”的顺序这是状态机正确运行的基础。StateMachine实现IState这是实现嵌套的魔法。一个StateMachine对象可以作为一个State被添加到另一个StateMachine中。当父状态机进入这个“状态”时实际上就是启动了子状态机。调试日志在ChangeState中输出日志至关重要。在开发复杂AI时通过观察控制台的状态切换记录可以快速定位逻辑错误。3.3 敌人AI实战构建一个分层敌人框架搭好了现在我们来创建一个具体的敌人AI。假设我们有一个“哨兵机器人”它的行为是顶层状态闲置Idle-警戒Alert-战斗Combat-死亡Dead。警戒状态是一个父状态内部子状态机包含转向玩家TurnToPlayer和发出警告Warn。战斗状态是一个父状态内部子状态机包含追击Chase和远程攻击RangedAttack。首先创建敌人的主控制器SentinelAI.cs它持有顶层的StateMachine。// SentinelAI.cs using UnityEngine; public class SentinelAI : MonoBehaviour { private StateMachine topLevelStateMachine; // 状态实例 private IdleState idleState; private AlertState alertState; private CombatState combatState; private DeadState deadState; // 一些敌人属性用于状态条件判断 public float sightRange 10f; public float attackRange 5f; public float health 100f; private Transform player; void Start() { player GameObject.FindGameObjectWithTag(Player).transform; // 1. 初始化所有状态 idleState new IdleState(this); alertState new AlertState(this); combatState new CombatState(this); deadState new DeadState(this); // 2. 创建顶层状态机 topLevelStateMachine new StateMachine(); topLevelStateMachine.AddState(idleState); topLevelStateMachine.AddState(alertState); topLevelStateMachine.AddState(combatState); topLevelStateMachine.AddState(deadState); topLevelStateMachine.SetDefaultState(idleState); // 3. 设置状态转换条件 // 闲置 - 警戒玩家进入视野 idleState.AddTransition(alertState, () IsPlayerInSight() health 0); // 警戒 - 战斗玩家进入攻击范围 alertState.AddTransition(combatState, () DistanceToPlayer() attackRange health 0); // 警戒 - 闲置玩家离开视野一段时间这里简化AlertState内部会处理 // 战斗 - 警戒玩家跑出攻击范围但还在视野内 combatState.AddTransition(alertState, () DistanceToPlayer() attackRange IsPlayerInSight() health 0); // 任何状态 - 死亡血量归零 idleState.AddTransition(deadState, () health 0); alertState.AddTransition(deadState, () health 0); combatState.AddTransition(deadState, () health 0); // 4. 启动顶层状态机 topLevelStateMachine.Start(); } void Update() { if (topLevelStateMachine ! null) { // 驱动顶层状态机更新 topLevelStateMachine.OnUpdate(Time.deltaTime); } } // 工具方法 public bool IsPlayerInSight() { if (player null) return false; float dist Vector3.Distance(transform.position, player.position); return dist sightRange; } public float DistanceToPlayer() { if (player null) return Mathf.Infinity; return Vector3.Distance(transform.position, player.position); } // 供其他脚本调用例如受到伤害 public void TakeDamage(float damage) { health - damage; } }接下来我们实现几个关键的状态类以AlertState和其子状态为例展示分层是如何工作的。// AlertState.cs public class AlertState : State { private SentinelAI ai; // 子状态 private TurnToPlayerState turnState; private WarnState warnState; public AlertState(SentinelAI aiController) : base(Alert) { this.ai aiController; // 1. 创建子状态 turnState new TurnToPlayerState(aiController); warnState new WarnState(aiController); // 2. 创建子状态机 subStateMachine new StateMachine(); subStateMachine.AddState(turnState); subStateMachine.AddState(warnState); subStateMachine.SetDefaultState(turnState); // 3. 设置子状态机内部的转换 // 转向玩家 - 发出警告当面向玩家后 turnState.AddTransition(warnState, () turnState.IsFacingPlayer()); // 发出警告 - 转向玩家警告结束后循环这里简化 warnState.AddTransition(turnState, () warnState.IsWarningFinished()); } public override void OnEnter() { base.OnEnter(); // 重要调用基类OnEnter以启动子状态机 Debug.Log(${ai.gameObject.name} 进入警戒状态); // 可以在这里播放警戒音效或全局特效 } public override void OnUpdate(float deltaTime) { base.OnUpdate(deltaTime); // 重要调用基类OnUpdate以更新子状态机 // 父状态自身的逻辑例如如果玩家完全离开视野应该切换回Idle // 这个判断写在父状态的Update里而不是子状态里逻辑更清晰 if (!ai.IsPlayerInSight()) { // 注意这里触发的是顶层状态机的转换从AlertState切回IdleState // 由于AlertState是State的子类其ParentStateMachine已被设置为顶层状态机 // CheckTransitions()会检查这个条件但我们需要在AlertState里添加这个Transition。 // 更合理的做法是在顶层状态机设置 Alert-Idle 的转换这里仅作演示。 } } } // TurnToPlayerState.cs public class TurnToPlayerState : State { private SentinelAI ai; private float turnSpeed 180f; // 度/秒 private float angleThreshold 5f; // 角度容差 public TurnToPlayerState(SentinelAI aiController) : base(TurnToPlayer) { this.ai aiController; } public override void OnUpdate(float deltaTime) { // 实现转向玩家的逻辑 Vector3 directionToPlayer (ai.PlayerPosition - ai.transform.position).normalized; directionToPlayer.y 0; // 保持水平旋转 Quaternion targetRotation Quaternion.LookRotation(directionToPlayer); ai.transform.rotation Quaternion.RotateTowards(ai.transform.rotation, targetRotation, turnSpeed * deltaTime); } public bool IsFacingPlayer() { Vector3 forward ai.transform.forward; Vector3 toPlayer (ai.PlayerPosition - ai.transform.position).normalized; float angle Vector3.Angle(forward, toPlayer); return angle angleThreshold; } } // WarnState.cs public class WarnState : State { private SentinelAI ai; private float warnDuration 2.0f; private float timer; public WarnState(SentinelAI aiController) : base(Warn) { this.ai aiController; } public override void OnEnter() { base.OnEnter(); timer warnDuration; Debug.Log(${ai.gameObject.name} 发出警告); // 播放警告动画、音效、亮起警报灯等 } public override void OnUpdate(float deltaTime) { base.OnUpdate(deltaTime); timer - deltaTime; // 警告期间可能有的逻辑比如扫描周围 } public bool IsWarningFinished() { return timer 0; } }实操心得在父状态如AlertState的OnEnter和OnUpdate中务必调用base.OnEnter()和base.OnUpdate()。这是驱动子状态机的关键。忘记调用base方法是分层状态机调试中最常见的错误之一会导致子状态机完全不起作用。4. 调试技巧与常见问题排查用上了HFSM代码结构清晰了但调试复杂度也上来了。状态在多层之间跳转光靠看日志可能不够直观。下面分享几个我实践中总结的调试技巧和常见坑点。4.1 可视化调试工具简易版我们可以给StateMachine和State添加简单的调试信息绘制功能在Scene视图或Game视图中实时查看当前状态。// 在StateMachine.cs中添加 public class StateMachine : IState { // ... 原有代码 ... public IState CurrentState currentState; public string CurrentStateName currentState?.GetType().Name ?? None; // 绘制调试信息在OnGUI或专门的调试管理器中调用 public void DrawDebugInfo() { GUILayout.Label($当前状态: {CurrentStateName}); if (currentState is State baseState baseState.subStateMachine ! null) { // 递归绘制子状态机信息 baseState.subStateMachine.DrawDebugInfo(); } } } // 在SentinelAI.cs的OnGUI中调用 void OnGUI() { if (topLevelStateMachine ! null) { GUILayout.BeginArea(new Rect(10, 10, 300, 200)); topLevelStateMachine.DrawDebugInfo(); GUILayout.EndArea(); } }更高级的做法是自定义一个EditorWindow以树状图形式展示所有敌人的状态层级这对于调试大量AI单位尤其有用。4.2 常见问题速查与解决方案下表列出了从传统代码转向HFSM时最容易遇到的几个问题问题现象可能原因排查步骤与解决方案状态不切换1. 转换条件Funcbool始终返回false。2.AddTransition添加错了状态对象。3. 父状态的OnUpdate中未调用base.OnUpdate()导致子状态机的转换检查未执行。1. 在转换条件的Lambda表达式中加入Debug.Log打印条件值。2. 检查AddTransition的两个参数确保to状态是同一个实例且已添加到状态机中。3.重点检查确保所有自定义State的OnUpdate方法都调用了base.OnUpdate()。进入状态时子状态机没启动父状态的OnEnter中未调用base.OnEnter()。重点检查确保所有自定义State的OnEnter方法都调用了base.OnEnter()。状态循环切换抖动两个状态之间的转换条件在同一帧内互相满足。例如A切到B的条件是distance 5B切回A的条件是distance 4.5当distance在4.5-5之间时就会抖动。1. 为转换条件增加“滞后区间”或“冷却时间”。例如追击切攻击的距离是3攻击切追击的距离是5。2. 在状态内部设置一个bool hasExited标志确保退出后至少一帧内不能再进入。性能疑虑每帧检查所有状态的转换条件尤其是Lambda表达式创建的开销。1. 对于简单的条件如距离判断性能影响微乎其微。现代CPU处理这些非常快。2. 如果状态非常多成百上千可以考虑按需检查或使用“事件驱动”的转换如收到“被攻击”消息才检查是否进入“受击”状态而不是每帧轮询。代码感觉变多了每个状态一个类确实增加了文件数量。这是为了换取清晰度和可维护性。对于复杂AI将500行杂乱的代码拆分成5个100行的清晰类绝对是值得的。可以使用“部分类partial class”将相关状态放在一个文件里管理。4.3 进阶优化使用ScriptableObject创建状态资产对于需要策划或设计师频繁调整的状态逻辑如条件参数、持续时间我们可以利用Unity的ScriptableObject将状态数据资产化。// AttackStateConfig.asset (ScriptableObject) [CreateAssetMenu(fileName NewAttackConfig, menuName AI/Attack Config)] public class AttackStateConfig : ScriptableObject { public float damage 10f; public float cooldown 1.5f; public float range 2f; public GameObject attackVFXPrefab; } // 然后在AttackState中引用这个配置 public class AttackState : State { private AttackStateConfig config; public AttackState(AttackStateConfig config) : base(Attack) { this.config config; } // ... 使用config.damage, config.cooldown等 ... }这样策划就可以在Unity编辑器中创建和修改AttackStateConfig资产并分配给不同的敌人预制体无需修改代码即可平衡游戏性。5. 总结与扩展思考通过以上步骤我们完成了一个从零搭建、可用于实战的HFSM框架并实现了一个具有分层行为的敌人AI。回顾整个过程HFSM的核心价值在于它将复杂的、面条式的行为逻辑分解成了一个个高内聚、低耦合的状态模块。这使得代码可读性极大提升每个状态做什么一目了然。调试变得简单通过状态名日志可以快速定位问题所在阶段。扩展性极强要加一个新行为比如“寻找掩体”只需新建一个TakeCoverState并在战斗状态机里添加相应的转换条件即可几乎不会影响旧代码。复用性高设计良好的子状态机如一套通用的“近战攻击”状态机可以像插件一样应用到不同的敌人类型上。这个自制的HFSM框架是一个起点你可以根据项目需求继续扩展例如状态共享数据创建一个Blackboard黑板类作为所有状态都能读写的共享数据空间用于传递玩家引用、仇恨列表、环境信息等避免状态类之间复杂的相互引用。并行状态机有些行为需要同时进行比如“移动”和“播放动画”。可以扩展框架支持多个状态机并行运行。与Unity动画系统深度集成在状态的OnEnter和OnExit中触发Animator的Trigger或Bool参数实现状态与动画的完美同步。最后选择工具永远要看场景。对于极度复杂、行为树Behavior Tree更合适的AI如拥有大量条件分支、优先级选择、序列执行的RTS单位你可以在这个状态机思想的基础上去学习并集成行为树库。但无论如何透彻理解状态机都是你迈向高级游戏AI程序员坚实的第一步。亲手实现一遍远比单纯使用插件理解得更深。希望这篇长文和附带的代码能成为你重构或构建下一个出色敌人AI的基石。