ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

游戏AI开发实战:用状态模式重构敌人行为,告别if-else地狱

游戏AI开发实战:用状态模式重构敌人行为,告别if-else地狱 1. 项目概述从“if-else地狱”到优雅的AI决策如果你写过游戏里的敌人AI尤其是那种需要根据环境、玩家行为做出不同反应的AI大概率经历过这样的场景一个Enemy类里塞满了isPatrolling、isChasing、isAttacking、isFleeing这样的布尔变量然后在Update函数里一个巨大的if-else if链或者switch语句像藤蔓一样疯狂生长。每加一个新状态比如“受伤后短暂眩晕”你都得小心翼翼地修改这个庞然大物生怕碰坏了哪个角落的逻辑。这种代码不仅难以阅读和维护更可怕的是状态之间的转换逻辑散落在各处一个小小的改动就可能引发连锁的Bug比如敌人可能在追逐时突然“闪现”回巡逻点或者在攻击动画播放到一半时莫名其妙地开始逃跑。这正是我们这次要解决的问题。项目标题“【《游戏编程模式》实战04】状态模式实现敌人AI”直指游戏开发中一个经典且高频的痛点如何管理游戏实体尤其是敌人复杂多变的行为状态。《游戏编程模式》这本书将状态模式列为游戏AI的基石之一不是没有道理的。它提供的不是某个具体的算法而是一种组织代码的思维方式一种将混乱的状态转换逻辑结构化、模块化的设计范式。简单来说状态模式的核心思想是“一个状态一个类”。敌人的每一种行为巡逻、追逐、攻击、逃跑等都被封装到独立的类中。敌人对象本身我们称之为Context或上下文并不关心当前具体在做什么它只是持有一个对“当前状态对象”的引用并将所有行为请求如Update、OnPlayerSpotted委托给这个状态对象去处理。当需要切换状态时只需更换这个引用指向的另一个状态类的实例即可。这么做的好处是显而易见的高内聚低耦合。所有与“巡逻”相关的代码和数据都住在PatrolState里所有“攻击”的逻辑都封装在AttackState中。添加新状态等于添加新类几乎不会影响旧代码。状态转换的逻辑也变得清晰通常由当前状态根据条件决定下一个状态是什么。对于需要快速迭代、经常添加新怪物类型的游戏项目来说这种可维护性和扩展性是无价的。接下来我们就从最原始的“面条代码”开始一步步重构看看状态模式如何将敌人AI从“if-else地狱”中拯救出来并探讨其在实战中的高级技巧与避坑指南。2. 状态模式核心思想与有限状态机FSM基础在深入代码之前我们必须先统一思想状态模式在游戏AI中的应用几乎总是与有限状态机Finite-State Machine, FSM的概念紧密结合。你可以把FSM看作状态模式的理论蓝图而状态模式是实现这个蓝图最优雅的面向对象方法之一。2.1 什么是有限状态机FSM想象一下一个老式的旋转拨号盘电话。它有几种明确的“状态”挂机听筒在座机上、摘机听筒被拿起可以听到拨号音、拨号中正在转动拨号盘、通话中、响铃中。这些状态是“有限”的电话不可能处于一个既不是挂机也不是摘机的模糊状态。状态之间的转换由明确的事件触发你“拿起”听筒状态从“挂机”变为“摘机”你“拨完号码”状态从“拨号中”变为“通话中”或“响铃中”。这就是一个典型的FSM。将其映射到我们的敌人AI状态集合巡逻(Patrol)、追逐(Chase)、攻击(Attack)、逃跑(Flee)、死亡(Dead)。当前状态敌人同一时刻只能处于其中一个状态。这解决了多个布尔标志可能冲突的问题比如isChasing和isPatrolling同时为true的非法情况。输入/事件玩家进入视野、玩家离开视野、进入攻击范围、生命值过低、攻击冷却结束。转移每个状态都定义了对哪些输入做出何种反应并可能切换到另一个状态。例如在巡逻状态下如果玩家进入视野事件发生则转移到追逐状态。2.2 状态模式如何实现FSM状态模式通过引入一个状态接口和一系列具体状态类来优雅地实现FSM。状态接口 (IEnemyState)这是一个契约规定了所有状态类必须实现的方法。通常至少包括Enter进入状态、Exit退出状态、Update每帧更新和HandleInput处理事件如发现玩家。具体状态类 (Concrete States)如PatrolState、ChaseState、AttackState。每个类独立实现接口方法包含了在该状态下敌人应有的全部行为逻辑。关键点在于每个状态类“知道”在什么条件下应该转换到什么状态。上下文 (Context)也就是我们的Enemy类。它拥有一个指向IEnemyState的引用代表当前状态并将自身的更新逻辑和事件处理委托给这个当前状态对象。当状态对象决定需要切换时它会通知上下文通常通过返回一个新状态对象或调用上下文的方法上下文负责进行状态对象的切换并调用新旧状态的Exit和Enter方法。这种设计的精髓在于将易变的状态行为与相对稳定的上下文对象解耦。Enemy类不再需要知道巡逻的具体路径点算法也不需要知道攻击的伤害计算公式它只需要说“我现在的状态是巡逻那么巡逻状态请你来更新我。” 当需要从巡逻切换到追逐时PatrolState的逻辑会说“我发现玩家了接下来应该追逐。”然后Enemy类销毁PatrolState实例创建并切换到ChaseState实例。注意这里有一个重要的设计决策点——状态对象的生命周期。如果状态对象是无状态的比如只包含纯逻辑不包含每次进入时需要重置的临时数据那么我们可以使用静态实例享元模式所有敌人共享同一个状态实例这能节省内存和CPU开销避免频繁new/delete。如果状态需要维护特定于某次进入的数据比如PatrolState需要记录当前巡逻到了第几个路径点那么就必须为每个敌人实例化独立的状态对象。在Unity中由于MonoBehaviour的生命周期管理我们通常采用后者并将状态数据保存在Enemy上下文中通过参数传递给状态方法。3. 实战从零构建一个状态模式驱动的敌人AI理论说得再多不如一行代码。让我们用C#以Unity环境为例但核心思想通用来构建一个经典的“巡逻-追逐-攻击”三状态敌人AI。3.1 第一步定义状态接口与上下文首先我们定义所有状态类都需要遵守的契约。// IEnemyState.cs public interface IEnemyState { // 进入该状态时调用用于初始化 void Enter(Enemy enemy); // 退出该状态时调用用于清理 void Exit(Enemy enemy); // 每帧更新处理状态内的持续逻辑如移动、计时 void Update(Enemy enemy); // 处理特定事件可选也可通过Update中检测实现 void OnPlayerDetected(Enemy enemy, Transform player); void OnPlayerLost(Enemy enemy); }接着创建我们的上下文Enemy类。它持有当前状态引用并提供一个方法来切换状态。// Enemy.cs public class Enemy : MonoBehaviour { public Transform player; // 假设玩家引用已赋值 public float sightRange 10f; public float attackRange 2f; public float moveSpeed 3.5f; private IEnemyState _currentState; void Start() { // 初始状态为巡逻 ChangeState(new PatrolState()); } void Update() { // 将更新委托给当前状态 _currentState?.Update(this); // 这里可以放置所有状态都需要检测的通用逻辑比如距离检测 // 但更常见的做法是将检测放在具体状态中以获得更精细的控制。 } // 核心方法切换状态 public void ChangeState(IEnemyState newState) { // 退出旧状态 _currentState?.Exit(this); // 切换引用 _currentState newState; // 进入新状态 _currentState?.Enter(this); } // 一些辅助方法供状态对象调用 public bool IsPlayerInSightRange() { return Vector3.Distance(transform.position, player.position) sightRange; } public bool IsPlayerInAttackRange() { return Vector3.Distance(transform.position, player.position) attackRange; } public void MoveTowards(Vector3 targetPosition) { Vector3 direction (targetPosition - transform.position).normalized; transform.position direction * moveSpeed * Time.deltaTime; // 简单转向目标 if (direction ! Vector3.zero) { transform.forward direction; } } // ... 其他属性如生命值、动画控制器等 }3.2 第二步实现具体状态类现在实现三个核心状态。巡逻状态 (PatrolState)这个状态负责在几个预设点之间循环移动。当发现玩家时切换到追逐状态。// PatrolState.cs public class PatrolState : IEnemyState { private Transform[] _waypoints; private int _currentWaypointIndex 0; private float _waitTimer 0f; private bool _isWaiting false; public void Enter(Enemy enemy) { Debug.Log(Enemy: 进入巡逻状态); // 假设敌人身上有WaypointContainer组件存储路径点 var container enemy.GetComponentWaypointContainer(); if (container ! null container.waypoints.Length 0) { _waypoints container.waypoints; _currentWaypointIndex 0; enemy.MoveTowards(_waypoints[_currentWaypointIndex].position); } else { // 如果没有路径点可以原地待机或随机游走 Debug.LogWarning(未找到巡逻路径点); } } public void Exit(Enemy enemy) { Debug.Log(Enemy: 退出巡逻状态); _isWaiting false; } public void Update(Enemy enemy) { // 1. 状态内逻辑巡逻 if (_waypoints null || _waypoints.Length 0) return; if (_isWaiting) { _waitTimer - Time.deltaTime; if (_waitTimer 0) { _isWaiting false; GoToNextWaypoint(enemy); } } else { // 检查是否到达当前路径点 if (Vector3.Distance(enemy.transform.position, _waypoints[_currentWaypointIndex].position) 0.5f) { // 到达等待一段时间 _isWaiting true; _waitTimer UnityEngine.Random.Range(1f, 3f); // 随机等待1-3秒 } else { // 未到达继续移动 enemy.MoveTowards(_waypoints[_currentWaypointIndex].position); } } // 2. 状态转换条件检测发现玩家 if (enemy.IsPlayerInSightRange()) { enemy.ChangeState(new ChaseState()); } } private void GoToNextWaypoint(Enemy enemy) { _currentWaypointIndex (_currentWaypointIndex 1) % _waypoints.Length; enemy.MoveTowards(_waypoints[_currentWaypointIndex].position); } public void OnPlayerDetected(Enemy enemy, Transform player) { } public void OnPlayerLost(Enemy enemy) { } }追逐状态 (ChaseState)这个状态唯一的目标就是靠近玩家。当玩家进入攻击范围切换到攻击状态如果玩家跑出视野范围则回到巡逻状态。// ChaseState.cs public class ChaseState : IEnemyState { public void Enter(Enemy enemy) { Debug.Log(Enemy: 进入追逐状态); // 可以在这里播放追逐的动画或音效 } public void Exit(Enemy enemy) { Debug.Log(Enemy: 退出追逐状态); } public void Update(Enemy enemy) { // 1. 状态内逻辑追逐玩家 if (enemy.player ! null) { enemy.MoveTowards(enemy.player.position); } // 2. 状态转换条件检测 if (!enemy.IsPlayerInSightRange()) { // 玩家丢失回到巡逻 enemy.ChangeState(new PatrolState()); } else if (enemy.IsPlayerInAttackRange()) { // 进入攻击范围开始攻击 enemy.ChangeState(new AttackState()); } } public void OnPlayerDetected(Enemy enemy, Transform player) { } public void OnPlayerLost(Enemy enemy) { } }攻击状态 (AttackState)这个状态执行攻击行为比如播放攻击动画、造成伤害并有一个攻击冷却时间。攻击结束后如果玩家还在攻击范围内则继续攻击如果玩家跑出攻击范围但还在视野内则切回追逐如果玩家完全丢失则回到巡逻。// AttackState.cs public class AttackState : IEnemyState { private float _attackCooldown 2.0f; private float _cooldownTimer 0f; private bool _isAttacking false; public void Enter(Enemy enemy) { Debug.Log(Enemy: 进入攻击状态); _cooldownTimer _attackCooldown; // 进入后可以立即攻击一次 } public void Exit(Enemy enemy) { Debug.Log(Enemy: 退出攻击状态); _isAttacking false; // 可以在这里停止攻击动画 } public void Update(Enemy enemy) { // 1. 状态内逻辑攻击冷却与执行 _cooldownTimer - Time.deltaTime; if (_cooldownTimer 0 !_isAttacking) { PerformAttack(enemy); _cooldownTimer _attackCooldown; // 重置冷却 } // 2. 状态转换条件检测通常在攻击间隔或攻击后检查 if (!_isAttacking) // 避免在攻击动画中途切换状态 { if (!enemy.IsPlayerInSightRange()) { enemy.ChangeState(new PatrolState()); } else if (!enemy.IsPlayerInAttackRange()) { enemy.ChangeState(new ChaseState()); } // 如果玩家仍在攻击范围内则继续留在攻击状态等待下次冷却 } } private void PerformAttack(Enemy enemy) { _isAttacking true; Debug.Log(Enemy 发动攻击); // 这里应播放攻击动画并触发伤害检测 // 假设有一个协程或动画事件在攻击结束后将_isAttacking设为false // 为简单演示我们用一个延时模拟 enemy.StartCoroutine(ResetAttackFlag(1.0f)); // 假设攻击动画持续1秒 } private System.Collections.IEnumerator ResetAttackFlag(float delay) { yield return new WaitForSeconds(delay); _isAttacking false; } public void OnPlayerDetected(Enemy enemy, Transform player) { } public void OnPlayerLost(Enemy enemy) { } }3.3 第三步组装与测试将Enemy脚本挂载到敌人GameObject上并设置好玩家引用、视野和攻击范围。创建一个空的GameObject作为WaypointContainer并将其子物体设为路径点然后把这个容器拖给敌人。运行游戏你会看到敌人按照预设路径巡逻。当你控制的玩家进入其视野范围敌人会立即转向并追逐你。当你进入其攻击范围它会停下并开始“攻击”控制台输出。如果你跑出攻击范围但仍在视野内它会继续追逐如果你完全跑出视野它会疑惑地停顿一下在我们的简单逻辑里是立即切换然后回到最近的路径点继续巡逻。实操心得在Update中检测状态转换条件时顺序很重要。在ChaseState中我们先检测“玩家是否丢失”再检测“是否进入攻击范围”。这是因为如果玩家既不在视野也不在攻击范围逻辑上应该先触发“丢失”回到巡逻。如果把顺序反过来可能会出现在边缘情况下玩家刚跑出攻击范围但还在视野内却被错误地判定为“丢失”。根据你的游戏逻辑仔细设计检测顺序和阈值。4. 高级技巧应对复杂场景的状态模式变体基础的FSM和状态模式已经能解决80%的问题。但当AI逻辑变得复杂时我们会遇到一些限制这时就需要引入更高级的模式。这些模式本质上是状态模式的组合与扩展。4.1 分层状态机Hierarchical State Machine消除重复代码观察我们的PatrolState、ChaseState和AttackState你会发现它们有一些共同行为比如都需要检测玩家是否丢失!enemy.IsPlayerInSightRange()然后切换回PatrolState。在ChaseState和AttackState中也都需要检测玩家是否进入攻击范围。当状态增多时这种重复代码会非常恼人。分层状态机通过引入“父状态”的概念来解决这个问题。我们可以创建一个AlertState警觉状态作为ChaseState和AttackState的父状态。AlertState的Update方法负责处理“玩家丢失则返回巡逻”这个通用逻辑。子状态ChaseState,AttackState继承AlertState并重写Update方法在调用base.Update()即父类逻辑后再加入自己特有的逻辑如追逐移动或攻击。// AlertState.cs (父状态) public class AlertState : IEnemyState { public virtual void Enter(Enemy enemy) { } public virtual void Exit(Enemy enemy) { } public virtual void Update(Enemy enemy) { // 通用逻辑如果玩家丢失回到巡逻 if (!enemy.IsPlayerInSightRange()) { enemy.ChangeState(new PatrolState()); } } // ... 其他方法 } // ChaseState.cs (子状态) public class ChaseState : AlertState // 继承自AlertState { public override void Enter(Enemy enemy) { base.Enter(enemy); Debug.Log(Enemy: 进入追逐状态); } public override void Update(Enemy enemy) { // 1. 先执行父类的通用逻辑检测玩家丢失 base.Update(enemy); // 2. 执行子类特有逻辑追逐 if (enemy.player ! null) { enemy.MoveTowards(enemy.player.position); } // 3. 子类特有的转换条件进入攻击范围 if (enemy.IsPlayerInAttackRange()) { enemy.ChangeState(new AttackState()); } } }这样ChaseState和AttackState就不再需要重复编写“玩家丢失检测”的代码了。这是一种利用面向对象继承来实现的代码复用非常适合处理有清晰“is-a”关系的状态。4.2 下推自动机Pushdown Automaton实现状态堆栈考虑这样一个场景敌人在巡逻时玩家按下一个“嘲讽”技能键敌人进入被嘲讽状态不受控制地走向某个地点。嘲讽效果结束后敌人应该回到之前的状态继续巡逻。如果用基础FSM我们需要在被嘲讽状态中记住前一个状态是什么这增加了复杂性。下推自动机通过维护一个状态堆栈来解决这个问题。不是简单地替换当前状态而是可以将新状态压入Push栈顶当前状态被暂停但保留在栈中。当新状态结束时比如嘲讽结束将其从栈顶弹出Pop之前被暂停的状态就重新成为当前状态继续执行。// 在Enemy类中修改用StackIEnemyState代替单一的_currentState private StackIEnemyState _stateStack new StackIEnemyState(); public void PushState(IEnemyState newState) { _currentState?.Exit(this); // 暂停当前状态 _stateStack.Push(newState); _currentState newState; _currentState.Enter(this); } public void PopState() { if (_stateStack.Count 1) // 确保至少保留一个状态如Idle { _currentState?.Exit(this); _stateStack.Pop(); _currentState _stateStack.Peek(); _currentState.Enter(this); // 重新进入之前的状态 } } // 在TauntState嘲讽状态的Update中当嘲讽时间结束 // enemy.PopState(); // 回到之前的状态下推自动机非常适合处理临时中断性状态比如受伤硬直、播放一次性动画如开门、对话、菜单界面等。这些状态结束后游戏应该无缝回到之前的状态。4.3 并发状态机Concurrent State Machines管理正交行为有时候一个实体的行为是由多个独立维度组合而成的。例如一个敌人可能同时具有“移动状态”站立、行走、奔跑和“战斗状态”和平、战斗、逃跑。这两个维度是相对独立的你可以在“行走”时进入“战斗”状态边走边打也可以在“奔跑”时“逃跑”。用单一FSM来建模会导致状态数量爆炸3种移动 x 3种战斗 9个组合状态。并发状态机的方案是运行两个或多个独立的FSM。Enemy类持有两个状态引用_movementState和_combatState。在Update中同时更新这两个状态机。void Update() { _movementState?.Update(this); _combatState?.Update(this); } public void HandleInput(Input input) { _movementState?.HandleInput(this, input); _combatState?.HandleInput(this, input); }两个状态机之间可以通过共享的上下文Enemy对象进行有限的通信。例如战斗状态切换到“逃跑”时可以设置一个Enemy.IsFleeing标志移动状态在更新时检测到这个标志就强制切换到“奔跑”状态。这种方式极大地增加了AI行为的丰富性和组合性在RTS游戏中的单位AI移动、攻击、技能或复杂RPG的角色AI中非常常见。注意事项并发状态机增加了系统的复杂性需要仔细设计状态间的交互协议避免出现矛盾比如一个状态机命令单位前进另一个命令单位停止。通常我们会定义一个状态优先级或者让某些状态机拥有“否决权”。5. 常见问题、调试技巧与性能优化即使理解了模式在实战中依然会遇到各种坑。下面是一些常见问题和我积累的排查技巧。5.1 状态切换的“闪烁”问题问题描述敌人在两个状态间快速来回切换。例如在攻击范围的边界敌人可能每帧都在AttackState和ChaseState之间跳动。根本原因状态转换条件过于“敏感”且检测频率过高每帧。玩家位置或敌人位置的微小波动导致条件在真假之间震荡。解决方案增加滞后Hysteresis为转换条件设置不同的“进入阈值”和“退出阈值”。例如进入攻击范围需要距离 2.0f但退出攻击范围则需要距离 2.5f。这中间0.5f的缓冲区可以防止抖动。状态最小持续时间为某些状态设置一个最短持续时间。例如AttackState一旦进入至少在0.5秒内不允许切换出去无论玩家是否跑出范围。这模拟了攻击动作的“前摇”或“不可取消”特性。延迟检测不在每帧都检测所有转换条件。可以设置一个计时器每隔0.1-0.2秒检测一次距离减少状态切换的频率。5.2 状态数据持久化与初始化问题描述从PatrolState切换到ChaseState再切换回来敌人可能从第一个路径点重新开始巡逻而不是接着上次的路径点。根本原因每次进入PatrolState都创建新的实例_currentWaypointIndex被重置为0。解决方案将需要持久化的数据如当前路径点索引、巡逻计时器等存储在上下文Enemy中而不是具体的状态类实例里。状态类在Enter方法中从上下文读取数据在Update或Exit中更新回上下文。这样无论状态对象如何创建销毁关键数据都得以保留。// 在Enemy类中添加 public int currentWaypointIndex 0; public float patrolWaitTimer 0f; public bool isPatrolWaiting false; // 在PatrolState中使用 public void Enter(Enemy enemy) { _currentWaypointIndex enemy.currentWaypointIndex; _waitTimer enemy.patrolWaitTimer; _isWaiting enemy.isPatrolWaiting; // ... 其他初始化 } public void Update(Enemy enemy) { // ... 巡逻逻辑更新_index, _timer等 // 在Update或Exit中同步回上下文 enemy.currentWaypointIndex _currentWaypointIndex; enemy.patrolWaitTimer _waitTimer; enemy.isPatrolWaiting _isWaiting; }5.3 调试与可视化复杂的AI状态机在运行时是黑盒调试起来很痛苦。以下是几个提升调试效率的方法状态名称显示在敌人头顶的UI或Debug.Log中输出当前状态名。可以用_currentState.GetType().Name。自定义编辑器可视化在Unity编辑器中为Enemy类编写一个自定义的Editor脚本在Inspector窗口用GUILayout.Label或EditorGUILayout.EnumPopup直观地显示和手动强制切换当前状态这对调试特定状态下的行为极其有用。绘制调试图形使用Gizmos或Debug.DrawLine在Scene视图中绘制敌人的视野范围、攻击范围、当前目标、巡逻路径等。颜色可以随状态改变如巡逻时画蓝线追逐时画红线。状态转换历史在Enemy中维护一个状态转换的日志列表最近10次当AI行为异常时查看这个日志能快速定位问题发生的时机和转换序列。5.4 性能优化考量状态对象池如果状态切换频繁比如大量小怪频繁地new和垃圾回收状态对象会产生GC垃圾回收压力。可以为每种状态类型创建一个简单的对象池。在ChangeState时从池中获取一个状态对象而不是创建新实例状态退出时将其还回池中并重置。避免每帧进行昂贵的检测如射线检测、物理OverlapSphere等。将这些检测的频率降低例如每3帧一次或者使用触发器Trigger结合OnTriggerEnter/Exit事件来驱动状态转换这比在Update里每帧做距离检测要高效得多。状态共享如果成百上千的敌人共享完全相同的逻辑且状态类是无状态的或状态数据存储在上下文中那么所有敌人可以共享同一个状态实例静态实例这能节省大量内存。这在大型战场游戏中是常见的优化手段。6. 状态模式 vs. 行为树与其他AI方案状态模式并非游戏AI的唯一解。对于超复杂的AI如《光环》或《最后生还者》中的敌人开发者会使用更强大的工具如行为树Behavior Tree和效用理论Utility Theory。行为树将AI决策建模为一棵树。节点类型包括序列按顺序执行子节点、选择器执行第一个成功的子节点、条件检查布尔值、动作执行具体行为等。行为树通过从根节点开始遍历来决策。它的优势在于可读性极高像流程图易于模块化和复用并且能很好地处理优先级和中断。很多商业游戏引擎如Unreal Engine, Unity的Asset Store插件都内置了行为树系统。效用理论为每个可能的行为如“攻击”、“寻找掩体”、“吃药”计算一个“效用分”基于距离、血量、弹药等因素然后选择分数最高的行为执行。这能产生更“聪明”、更不可预测的AI因为它总是在多个选项中做“最优”选择。那么如何选择选择状态模式/FSM当AI行为是顺序的、明确的、状态数量有限通常少于10个。逻辑相对简单状态转换条件清晰。你需要快速原型开发或者项目规模较小。FSM简单、直观、性能开销极低。考虑行为树当AI行为非常复杂有大量的条件分支、并行行动、需要频繁打断当前行为高优先级事件。你需要非程序员如策划也能理解和编辑AI逻辑。你对AI的可维护性和扩展性要求极高。考虑效用系统当你希望AI看起来更“有机”、更“智能”能根据动态环境在多个合理行为中做出权衡而不是死板的“if-else”。对于大多数中小型项目尤其是动作、平台、休闲游戏一个精心设计的状态模式配合分层、下推等技巧完全够用而且能保持代码的简洁和高效。它是我个人在游戏开发前期原型设计和实现核心敌人逻辑时的首选方案。它的直白和可控性在需要精细调校敌人行为的场景下是无可替代的。
返回列表