Unity游戏AI开发:基于状态机的敌人行为系统设计与实现
1. 项目概述:为什么我们需要一个“聪明”的敌人?
在游戏开发中,尤其是动作、RPG或类银河恶魔城这类游戏里,敌人(NPC)的AI行为是游戏体验的核心支柱之一。一个只会傻站着或者无脑冲锋的敌人,很快就会让玩家感到乏味。而一个行为丰富、反应灵敏、状态切换流畅的敌人,则能极大地提升游戏的挑战性和沉浸感。今天要聊的“敌人状态机”,就是实现这种“聪明”敌人的核心技术骨架。
简单来说,状态机(State Machine)是一种编程模型,它定义了一个对象(在这里就是敌人)在其生命周期内可能处于的几种“状态”,以及这些状态之间相互转换的“条件”。想象一下一个站岗的士兵:他大部分时间处于“巡逻”状态,眼睛四处张望;一旦发现可疑目标,立即切换到“警戒”状态,举起武器;如果目标进入攻击范围,则切换到“攻击”状态;如果受伤过重,可能会切换到“逃跑”或“死亡”状态。这个从“巡逻”到“警戒”再到“攻击”的过程,就是状态机在驱动。
在Unity中实现敌人状态机,远不止是写一堆if-else判断那么简单。一个健壮、可维护的状态机架构,需要清晰的层次划分、松耦合的设计以及高效的状态切换逻辑。它直接关系到后续添加新敌人类型、调试AI逻辑以及进行性能优化的效率。很多新手在实现敌人AI时,容易把各种行为逻辑(移动、攻击、寻路、动画)全部塞进一个巨大的EnemyController脚本里,导致代码臃肿不堪,修改一个行为可能引发一连串难以预料的Bug。状态机正是为了解决这种混乱而生的设计模式。
本教程将带你从零开始,构建一个适用于大多数2D/3D动作游戏的、模块化的敌人状态机系统。我们将不仅实现“巡逻”、“追击”、“攻击”、“死亡”这些基础状态,更会深入探讨状态机的设计哲学、如何与Unity的动画系统(Animator)、导航系统(NavMeshAgent或自定义移动)以及事件系统优雅地集成。无论你是刚刚入门Unity,想为你的小游戏添加一些像样的敌人,还是已经有一定经验,希望重构自己混乱的AI代码,这篇内容都将提供一套可直接“抄作业”的完整方案和背后的思考逻辑。
2. 状态机核心架构设计:告别“面条代码”
在动手写代码之前,我们必须先想清楚架构。一个好的架构能让后续开发事半功倍,而一个糟糕的架构则会让你在添加第三个状态时就想重写。我们将采用基于“状态模式”的设计,这是游戏开发中实现状态机最经典、最灵活的方式之一。
2.1 状态模式与三层结构
状态模式的核心思想是:将每一个状态封装成一个独立的类。这个类负责管理该状态下的所有行为(进入、更新、退出),以及判断何时应该切换到其他状态。这样做的好处是极高的内聚性和极低的耦合性:巡逻的逻辑全在PatrolState里,攻击的逻辑全在AttackState里,它们彼此不知道对方的存在,只通过一个共同的“上下文”(Context)——也就是我们的敌人主体——来进行通信和切换。
基于此,我们设计一个三层结构:
- 上下文层(Context):这是状态机的“宿主”,通常就是我们的敌人游戏对象(GameObject)上挂载的主控制器脚本,比如
EnemyController。它持有当前状态对象的引用,并负责在每帧调用当前状态的更新逻辑。它还应该提供一些公共方法和属性(如玩家的位置、自身的血量、导航组件引用等),供各个状态类查询和调用。 - 状态基类层(Base Class):定义一个抽象的
EnemyState基类或接口。它规定了所有具体状态类必须实现的方法,通常至少包括:OnEnter(): 当进入该状态时调用。在这里进行初始化工作,比如播放特定的动画、重置计时器、寻找路径点等。OnUpdate(): 在该状态的每一帧被调用。这里是状态核心逻辑发生的地方,比如计算与玩家的距离、执行移动、检查攻击冷却等。OnExit(): 当退出该状态时调用。在这里进行清理工作,比如停止动画、取消未完成的寻路请求等。
- 具体状态层(Concrete States):继承自
EnemyState基类的各个具体状态类,如PatrolState、ChaseState、AttackState、HurtState、DeathState等。每个类只关心自己状态内的逻辑。
2.2 关键组件与数据流
除了代码结构,我们还需要规划好状态机与Unity其他组件的交互。一个典型的敌人AI会涉及以下组件:
- 导航系统:对于3D游戏或复杂的2D地形,Unity的NavMeshAgent是首选。对于简单的2D平台游戏,可能需要自己实现基于物理或Transform的移动。我们的状态机需要持有对这些移动组件的引用。
- 动画系统:状态切换往往伴随着动画切换。我们可以通过Animator Controller中的状态机来驱动,也可以更直接地在代码状态机的
OnEnter中触发Animator的SetTrigger或SetBool。建议将动画逻辑封装在状态类内部,实现“状态-动画”的强对应。 - 感知系统:敌人如何发现玩家?通常通过触发器(Trigger Collider)实现一个“视觉范围”和“攻击范围”。当玩家进入这些范围时,触发对应的事件,这些事件会通知状态机上下文,进而可能触发状态切换。
- 数据配置:敌人的移动速度、巡逻点、攻击力、视野距离、攻击间隔等参数,不应该硬编码在脚本里。我们可以创建一个
EnemyData的ScriptableObject资产,方便在编辑器中进行配置和批量调整。
数据流的典型过程是:EnemyController(上下文)在Update中调用CurrentState.OnUpdate()。CurrentState在执行逻辑过程中,会根据感知到的信息(如与玩家距离)判断是否满足切换条件。如果满足,它会调用上下文提供的SwitchState()方法,传入下一个状态的类型。上下文负责销毁旧状态实例、创建新状态实例,并调用新旧状态的OnExit和OnEnter。
注意:状态切换的时机。状态切换通常不应该在状态的
OnUpdate中间直接发生,因为这会打断当前帧的逻辑。更安全的做法是,状态类只负责“建议”切换(比如设置一个nextStateType标志),由上下文在Update的最后统一处理切换。或者,使用一个简单的状态切换请求队列。
3. 核心状态类详解与实现
理论讲完了,我们开始动手实现几个最核心的状态类。我会以C#代码为例,并附上关键逻辑的讲解。
3.1 状态基类与上下文定义
首先,定义我们的状态基类和上下文接口。
// EnemyState.cs public abstract class EnemyState { protected EnemyController context; // 持有上下文的引用 public EnemyState(EnemyController context) { this.context = context; } // 状态生命周期方法 public virtual void OnEnter() { } public virtual void OnUpdate(float deltaTime) { } public virtual void OnExit() { } } // IEnemyContext.cs (接口,定义上下文需要提供的能力) public interface IEnemyContext { Transform Transform { get; } Transform PlayerTransform { get; } NavMeshAgent Agent { get; } Animator Animator { get; } EnemyData Data { get; } float CurrentHealth { get; } void SwitchState<T>() where T : EnemyState; bool IsPlayerInSight(); bool IsPlayerInAttackRange(); }EnemyController作为上下文,需要实现这个接口,并管理当前状态。
// EnemyController.cs public class EnemyController : MonoBehaviour, IEnemyContext { [SerializeField] private EnemyData enemyData; [SerializeField] private Transform[] patrolPoints; private EnemyState currentState; private NavMeshAgent agent; private Animator animator; private float health; public Transform Transform => transform; public Transform PlayerTransform { get; private set; } // 需要通过其他方式赋值,如GameManager public NavMeshAgent Agent => agent; public Animator Animator => animator; public EnemyData Data => enemyData; public float CurrentHealth => health; void Start() { agent = GetComponent<NavMeshAgent>(); animator = GetComponent<Animator>(); health = enemyData.maxHealth; PlayerTransform = GameObject.FindGameObjectWithTag("Player").transform; // 简单查找,生产环境建议用更稳健的方式 // 初始状态:巡逻 SwitchState<PatrolState>(); } void Update() { if (currentState != null) currentState.OnUpdate(Time.deltaTime); } public void SwitchState<T>() where T : EnemyState { // 退出当前状态 currentState?.OnExit(); // 创建并进入新状态 currentState = Activator.CreateInstance(typeof(T), this) as EnemyState; currentState?.OnEnter(); Debug.Log($"[{gameObject.name}] 切换到状态: {typeof(T).Name}"); } // 感知方法实现 public bool IsPlayerInSight() { if (PlayerTransform == null) return false; float distance = Vector3.Distance(transform.position, PlayerTransform.position); return distance < enemyData.sightRange; } public bool IsPlayerInAttackRange() { if (PlayerTransform == null) return false; float distance = Vector3.Distance(transform.position, PlayerTransform.position); return distance < enemyData.attackRange; } // 其他方法,如受伤、死亡等 public void TakeDamage(float damage) { health -= damage; if (health <= 0) { SwitchState<DeathState>(); } else { // 可以切换到受伤状态,或者只是播放受击动画 // SwitchState<HurtState>(); } } }3.2 巡逻状态实现
巡逻状态是敌人的默认状态。逻辑通常包括:在预设的几个点之间循环移动,到达一个点后等待片刻,然后前往下一个点。同时,需要持续检测玩家是否进入视野。
// PatrolState.cs public class PatrolState : EnemyState { private int currentPatrolIndex = 0; private float waitTimer = 0f; private bool isWaiting = false; public PatrolState(EnemyController context) : base(context) { } public override void OnEnter() { // 进入巡逻状态,停止可能的追击或攻击动画,播放行走动画 context.Animator.SetBool("IsWalking", true); context.Animator.SetBool("IsAttacking", false); // 设置导航代理参数 context.Agent.speed = context.Data.patrolSpeed; context.Agent.stoppingDistance = 0.1f; // 巡逻点可以停得很近 MoveToNextPatrolPoint(); } public override void OnUpdate(float deltaTime) { // 1. 首要检查:玩家是否进入视野?如果是,切换到追击状态 if (context.IsPlayerInSight()) { context.SwitchState<ChaseState>(); return; } // 2. 处理巡逻逻辑 if (isWaiting) { waitTimer -= deltaTime; if (waitTimer <= 0) { isWaiting = false; MoveToNextPatrolPoint(); } } else { // 检查是否到达当前巡逻点 if (!context.Agent.pathPending && context.Agent.remainingDistance <= context.Agent.stoppingDistance) { StartWaitingAtPoint(); } } } public override void OnExit() { // 退出时,可以停止行走动画 context.Animator.SetBool("IsWalking", false); } private void MoveToNextPatrolPoint() { if (context.PatrolPoints == null || context.PatrolPoints.Length == 0) { // 如果没有巡逻点,就在原地发呆 isWaiting = true; waitTimer = context.Data.patrolWaitTime; return; } // 设置目的地为下一个巡逻点 context.Agent.SetDestination(context.PatrolPoints[currentPatrolIndex].position); // 更新索引,循环 currentPatrolIndex = (currentPatrolIndex + 1) % context.PatrolPoints.Length; } private void StartWaitingAtPoint() { isWaiting = true; waitTimer = context.Data.patrolWaitTime; context.Animator.SetBool("IsWalking", false); // 等待时停止行走动画 } }实操心得:巡逻点的设置。巡逻点可以直接在
EnemyController的Inspector面板中拖拽赋值。对于更动态的场景,可以考虑在运行时通过代码生成巡逻点,比如在敌人周围随机生成几个点。确保巡逻点在地面(NavMesh)上,否则导航会失败。对于2D游戏,你可能需要自己实现一个简单的“移动到位置”的逻辑,而不是用NavMeshAgent。
3.3 追击状态实现
当敌人发现玩家后,进入追击状态。核心逻辑是持续将玩家的当前位置设为自己的导航目标。
// ChaseState.cs public class ChaseState : EnemyState { public ChaseState(EnemyController context) : base(context) { } public override void OnEnter() { // 播放奔跑或快速行走动画 context.Animator.SetBool("IsRunning", true); context.Animator.SetBool("IsWalking", false); context.Agent.speed = context.Data.chaseSpeed; context.Agent.stoppingDistance = context.Data.attackRange * 0.9f; // 在接近攻击范围时就开始准备攻击 } public override void OnUpdate(float deltaTime) { // 1. 检查玩家是否丢失?如果玩家跑出视野范围,可能返回巡逻状态。 // 这里实现一个简单的“丢失计时器”,玩家离开视野一段时间后才丢失。 if (!context.IsPlayerInSight()) { // 简单处理:直接回到巡逻 // context.SwitchState<PatrolState>(); // 更高级:可以加入一个“寻找”状态,或者原地警戒几秒。 return; } // 2. 检查是否进入攻击范围?如果是,切换到攻击状态。 if (context.IsPlayerInAttackRange()) { context.SwitchState<AttackState>(); return; } // 3. 持续追击玩家 if (context.PlayerTransform != null) { context.Agent.SetDestination(context.PlayerTransform.position); } } public override void OnExit() { context.Animator.SetBool("IsRunning", false); // 可能还需要停止导航,但通常由下一个状态来处理 // context.Agent.ResetPath(); } }注意事项:追击的“粘性”。直接让敌人永远精确追踪玩家可能会让游戏体验很糟糕,尤其是玩家想逃跑时。可以引入一些“不完美”的因素:比如给导航代理一个较小的
angularSpeed(旋转速度)和acceleration(加速度),让敌人转向和加速不那么灵敏;或者在玩家离开视野后,敌人只追到最后一个已知位置,然后进入“搜寻”状态。这些细节能大大增加AI的真实感。
3.4 攻击状态实现
攻击状态通常是一个短暂的状态,包含攻击动画的播放、伤害判定的时机以及攻击后的冷却或硬直。
// AttackState.cs public class AttackState : EnemyState { private float attackTimer = 0f; private bool hasDealtDamage = false; public AttackState(EnemyController context) : base(context) { } public override void OnEnter() { // 停止移动,播放攻击动画 context.Agent.isStopped = true; // 暂停导航 context.Animator.SetTrigger("Attack"); // 触发攻击动画 context.Animator.SetBool("IsRunning", false); attackTimer = context.Data.attackDuration; // 攻击动作总时长 hasDealtDamage = false; // 面向玩家 if (context.PlayerTransform != null) { Vector3 lookDir = context.PlayerTransform.position - context.Transform.position; lookDir.y = 0; // 保持水平旋转 if (lookDir != Vector3.zero) { context.Transform.rotation = Quaternion.LookRotation(lookDir); } } } public override void OnUpdate(float deltaTime) { attackTimer -= deltaTime; // 在动画的特定帧(如武器挥到最高点时)造成伤害。 // 这里用计时器简单模拟,更精确的做法是使用动画事件(Animation Event)。 if (!hasDealtDamage && attackTimer < context.Data.attackDuration * 0.5f) // 假设在攻击动作进行到一半时造成伤害 { TryDealDamage(); hasDealtDamage = true; } // 攻击动作结束后,判断下一步 if (attackTimer <= 0) { // 检查玩家是否还在攻击范围内 if (context.IsPlayerInAttackRange()) { // 还在范围内,可以继续攻击(重置计时器,或者进入一个短暂的“攻击后摇”状态) // 这里我们简单切换回攻击状态,实现连击。实际可能需要一个攻击冷却。 context.SwitchState<AttackState>(); } else if (context.IsPlayerInSight()) { // 玩家在视野内但不在攻击范围,切回追击 context.SwitchState<ChaseState>(); } else { // 玩家丢失,回到巡逻 context.SwitchState<PatrolState>(); } } } public override void OnExit() { context.Agent.isStopped = false; // 恢复导航 } private void TryDealDamage() { // 简单的球形检测,判断玩家是否在攻击判定范围内 Collider[] hitColliders = Physics.OverlapSphere(context.Transform.position + context.Transform.forward * context.Data.attackRange * 0.5f, context.Data.attackRange * 0.5f); foreach (var hitCollider in hitColliders) { if (hitCollider.CompareTag("Player")) { // 假设玩家有一个`PlayerHealth`组件 var playerHealth = hitCollider.GetComponent<PlayerHealth>(); if (playerHealth != null) { playerHealth.TakeDamage(context.Data.attackDamage); } break; } } } }核心技巧:伤害判定与动画同步。上面用计时器模拟伤害判定时机是非常粗糙的。最佳实践是使用动画事件。在Attack动画的特定帧上添加一个事件,该事件会调用
EnemyController的一个公共方法(如OnAttackAnimationHit),然后由控制器或状态机来处理伤害判定。这样能确保伤害判定与视觉表现完全同步,无论帧率如何变化。在AttackState的OnEnter中,可以订阅这个事件,在OnExit中取消订阅。
4. 状态机的扩展、调试与性能优化
实现基础状态后,一个可用的敌人AI已经成型。但要让它更强大、更易维护,我们还需要考虑扩展性和工程化问题。
4.1 扩展更多状态
基于现有的架构,添加新状态变得非常容易。例如,添加一个HurtState(受伤状态):
- 在
EnemyController.TakeDamage方法中,除了扣血,可以调用SwitchState<HurtState>()。 - 创建
HurtState类,在OnEnter中播放受击动画,并设置一个短暂的无敌时间或硬直时间。 - 在
OnUpdate中计时,时间结束后根据情况切换回ChaseState或PatrolState。
再比如,添加一个FleeState(逃跑状态),当敌人血量很低时,有一定概率会逃跑。只需要在HurtState或AttackState中检查血量,并概率性地切换到FleeState即可。
4.2 可视化调试与日志
调试状态机时,最怕的就是不知道敌人当前处于什么状态,以及为什么切换。我们可以通过多种方式增强可调试性:
- OnGUI 或 UI 文本:在敌人头顶显示当前状态名。
void OnGUI() { if (currentState != null) { GUI.Label(new Rect(10, 10, 200, 20), $"State: {currentState.GetType().Name}"); } } - Unity Editor 自定义编辑器:为
EnemyController编写一个自定义的Editor脚本,在Inspector面板中实时显示当前状态、血量、到玩家的距离等信息。 - 使用Debug.DrawRay或Gizmos:在Scene视图中绘制敌人的视野范围(一个扇形)和攻击范围(一个圆),非常直观。
void OnDrawGizmosSelected() { if (enemyData != null) { Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(transform.position, enemyData.sightRange); Gizmos.color = Color.red; Gizmos.DrawWireSphere(transform.position, enemyData.attackRange); } } - 结构化日志:像上面代码中那样,在
SwitchState时输出带对象名的日志,方便在Console窗口过滤查看。
4.3 性能考量与优化建议
当场景中有大量敌人时,状态机的性能需要关注:
- 减少每帧的运算:
- 感知检测的频率:不需要每帧都进行
Physics.OverlapSphere或计算距离来检测玩家。可以每N帧(例如5帧)检测一次,或者使用协程(Coroutine)每隔一段时间检测一次。对于“玩家是否在视野内”这种复杂检测(可能包含射线遮挡判断),频率应该更低。 - 导航路径更新频率:
NavMeshAgent.SetDestination是有开销的。在ChaseState中,不需要每帧都设置目标。可以判断玩家位置移动超过一定阈值后再更新路径。
- 感知检测的频率:不需要每帧都进行
- 对象池与状态复用:每次
SwitchState都new一个新的状态对象,如果状态切换非常频繁,可能会产生GC(垃圾回收)压力。对于状态种类不多且无状态数据(或可重置)的情况,可以考虑使用对象池来复用状态实例。在EnemyController中预先创建所有可能的状态对象,切换时只是激活和禁用它们。 - 使用更轻量的移动方案:对于大量简单的2D敌人,使用
NavMeshAgent可能过重。可以考虑使用更简单的基于Rigidbody2D或直接修改Transform.position的移动方式,并在状态机中自己处理寻路逻辑(如A*算法或简单的朝向移动)。 - 动画状态机优化:确保Animator Controller的布局简洁,避免过多的过渡条件和层。可以考虑使用动画融合树(Blend Trees)来平滑处理移动速度变化。
4.4 与Unity动画状态机的协作模式
我们的代码状态机和Unity的Animator状态机是两套独立的状态机,如何让它们协同工作?有两种主流模式:
- 代码驱动动画:这是我们上面采用的方式。在代码状态机的
OnEnter和OnExit中,通过Animator.SetBool/SetTrigger来驱动Animator中的状态切换。代码状态机是主控,动画状态机是表现层。这种方式逻辑清晰,耦合度低。 - 动画驱动代码:在Animator中设置状态机,并在状态(State)或过渡(Transition)上添加动画事件,这些事件调用
EnemyController的方法来触发逻辑状态切换。这种方式更适合动画师主导的工作流,但逻辑可能会分散在动画文件和代码中,调试稍复杂。
我个人更推荐代码驱动动画的模式,因为游戏逻辑(何时攻击、何时追击)应该由代码决定,动画只是视觉反馈。这符合“数据/逻辑层”与“表现层”分离的原则。
5. 常见问题排查与进阶技巧
即使按照教程搭建,在实际项目中你还是会遇到各种问题。这里记录一些我踩过的坑和解决方案。
5.1 状态切换混乱或卡死
- 症状:敌人在两个状态间快速来回切换,或者卡在某个状态出不来了。
- 排查:
- 检查切换条件:确保你的条件判断是互斥且稳定的。例如,
IsPlayerInSight()和IsPlayerInAttackRange()的判断阈值要有明确的差值,避免玩家刚好在边界时反复横跳。可以加入一个“状态保持时间”的最小阈值(例如,进入攻击状态后至少0.5秒内不允许切换出去)。 - 检查Update顺序:确保状态切换的逻辑在
OnUpdate中是“原子”的,且切换后立即return,避免同一帧执行了多个状态的逻辑。 - 打印调试信息:在每个状态的
OnEnter和可能触发切换的地方打印日志,观察切换流程。
- 检查切换条件:确保你的条件判断是互斥且稳定的。例如,
- 解决:在
EnemyController的SwitchState方法开头,可以加入一个检查,如果请求切换到的状态就是当前状态,则直接忽略。
5.2 导航代理行为异常
- 症状:敌人不移动、抖动、穿墙、卡在角落。
- 排查:
- NavMesh烘焙:首先检查场景的NavMesh是否已正确烘焙,并且敌人的位置在NavMesh之上。
- 代理参数:检查
NavMeshAgent的radius、height、step height、slope limit是否设置合理。一个过大的radius可能导致在狭窄处无法通过。 - 停止距离:
stoppingDistance很重要。在PatrolState中应该很小(如0.1),在ChaseState中应设为略小于攻击范围,这样敌人在接近到攻击范围时就会停下并切换状态。 - 路径状态:在设置目的地前,可以检查
agent.isOnNavMesh。如果为false,需要调用NavMeshAgent.Warp将其瞬移到最近的NavMesh上。
- 解决:对于卡住问题,可以增加一个“解困”机制:如果代理超过一定时间(如3秒)都没有移动或路径无效,则强制停止当前路径,并尝试一个随机方向的小幅度移动或直接切换到
Idle状态。
5.3 动画与逻辑不同步
- 症状:攻击动作播放了,但伤害没有产生;或者伤害产生了,但动画还没播。
- 排查与解决:
- 坚决使用动画事件:这是解决此类问题的银弹。在动画编辑器中,在准确的帧上添加事件,调用一个定义好的方法。确保事件方法名在Animator和脚本中完全一致。
- 状态机时序:确保在
AttackState的OnEnter中触发攻击动画,然后在动画事件触发的方法中进行伤害判定。在OnExit中清理。这样逻辑和视觉就牢牢绑定在一起了。
5.4 如何设计更有趣的AI行为?
基础状态机只是骨架,要让敌人真正“活”起来,需要注入一些行为逻辑:
- 引入随机性:巡逻等待时间、攻击后的下一次攻击间隔、发现玩家后的反应延迟,都可以加入随机值,避免所有敌人行为完全一致。
- 状态间共享数据:有时一个状态需要知道另一个状态的信息。例如,
FleeState可能需要知道是从哪个状态逃过来的,以便在安全后返回。可以通过上下文(EnemyController)来存储这些共享数据,比如LastKnownPlayerPosition、LastStateBeforeFlee等。 - 分层状态机:当敌人行为复杂时(比如一个Boss有多种攻击模式,每种模式又包含多个阶段),可以考虑使用分层状态机(Hierarchical State Machine)。Unity的Animator Controller本身就是一个分层状态机,可以参考其思想,或者使用像
Stateless这样的第三方库。 - 行为树:对于极其复杂、需要大量条件分支和优先级选择的AI(如RTS游戏中的单位),状态机可能变得难以维护。这时可以考虑升级到行为树(Behavior Tree)。但在大多数动作游戏中,精心设计的状态机已经完全够用,且性能更好。
构建一个稳健的敌人状态机是游戏开发中一项极具成就感的工作。它就像在赋予一堆多边形和贴图以生命和个性。从简单的巡逻追击,到复杂的多阶段Boss战,其底层逻辑都离不开状态机这一强大而优雅的设计模式。希望这套从架构到实现,从原理到避坑的完整思路,能帮助你打造出令玩家印象深刻、行为可信的敌人。记住,最好的AI是让玩家感觉不到“规则”的存在,而是觉得在与一个真实的对手交锋。
