在使用状态机的时候,很容易出现这种情况
在游戏开发中,当其他类(比如敌人、道具、环境等)触发了某个事件,想要改变玩家的状态 ,而玩家使用的是状态机(State Machine)来管理行为和状态 ,那么关键在于:如何优雅地让外部事件通知状态机,进而触发玩家状态的变更或行为的调整。
下面是几种常见且合理的实现方式,按照推荐程度和清晰度排序:
一、核心思路
状态机通常封装了玩家当前的状态以及状态之间的切换逻辑。外部不应该直接修改玩家的状态(比如强制将状态设为"跳跃"或"受伤") ,而是应该通过事件/消息/接口通知玩家对象,由玩家内部的状态机决定如何响应。
二、推荐的实现方案
方案1:通过事件/消息系统通知玩家(解耦推荐 ✅✅✅)
适用场景: 游戏架构较为复杂,希望各系统之间低耦合,比如使用观察者模式、事件总线(Event Bus)、信号槽(Signal/Slot)等。
实现步骤:
-
定义事件类型
- 比如
PlayerHitEvent、PowerUpCollectedEvent、EnemyNearbyEvent等。
- 比如
-
其他类触发事件
-
当敌人攻击玩家时,敌人不直接调用玩家的方法,而是向事件系统发送一个事件,如:
csharp
EventSystem.Publish(new PlayerHitEvent(damage));
-
-
玩家对象监听事件
-
玩家类(或状态机类)注册监听这些事件,当收到事件后,通知当前状态或者直接让状态机处理。
-
例如:
csharp
// 在玩家初始化时 EventSystem.Subscribe<PlayerHitEvent>(OnPlayerHit); private void OnPlayerHit(PlayerHitEvent e) { stateMachine.HandleEvent(e); // 或直接调用状态机方法 }
-
-
状态机或具体状态处理事件
-
状态机有一个统一的事件处理入口,或者每个状态自己处理关心的事件。
-
例如,在状态机中:
csharp
public void HandleEvent(GameEvent e) { currentState.HandleEvent(e); } -
每个状态类实现自己的
HandleEvent方法,决定是否要切换状态或执行某些行为。
-
✅ 优点:高度解耦,易于扩展和维护,适合大型项目。
❌ 缺点:需要实现事件系统,稍微复杂一点。
方案2:通过玩家暴露的接口方法(直接但需控制访问 ✅✅)
适用场景: 中小型项目,事件系统未引入,但依然希望有一定封装性。
实现步骤:
-
在玩家类中提供"受保护"的方法或通过状态机暴露接口
-
比如:
csharp
public class Player { public StateMachine StateMachine { get; private set; } // 外部不直接调用 SetState,而是调用统一接口 public void TakeDamage(int damage) { stateMachine.HandleDamage(damage); } public void CollectPowerUp(PowerUpType type) { stateMachine.HandlePowerUp(type); } }
-
-
状态机或当前状态处理逻辑
-
状态机根据不同输入,决定是否切换状态。例如:
csharp
public void HandleDamage(int damage) { if (currentState is NormalState) { // 切换到受伤状态或减少血量后可能进入击倒状态 ChangeState(new HurtState()); } }
-
✅ 优点:比直接操作状态更安全,逻辑集中。
❌ 缺点:如果接口设计不好,仍可能导致外部误用。
方案3:直接调用状态机/设置状态(不推荐 ❌❌❌,除非非常简单)
即:其他类直接调用类似 player.StateMachine.ChangeState(new HurtState()) 或 player.SetState("Hurt")
⚠️ 为什么不推荐?
- 破坏封装性:状态机是玩家内部行为管理机制,外部直接控制状态,容易导致逻辑混乱。
- 难以维护和调试:多个系统随意切换玩家状态,难以追踪状态流转。
- 不符合面向对象设计原则:高耦合,低内聚。
🔒 除非是极其简单的原型阶段,否则应避免这种做法。
三、最佳实践建议总结
| 方法 | 耦合度 | 可维护性 | 推荐度 | 适用阶段 |
|---|---|---|---|---|
| 事件/消息系统(观察者/Event Bus) | 低 | 高 | ✅✅✅ | 中大型项目,复杂交互 |
| 玩家封装接口,由状态机处理 | 中 | 中高 | ✅✅ | 中小型项目,结构清晰 |
| 直接操作状态机/状态 | 高 | 低 | ❌❌❌ | 原型阶段,临时调试用 |
四、举个例子(伪代码/简化版)
假设:
- 玩家有状态机,当前可能是 Idle、Run、Hurt、KnockDown 等状态。
- 敌人攻击玩家,希望让玩家受伤。
事件驱动方式(推荐)👇
csharp
// 敌人攻击时
void AttackPlayer()
{
int damage = 10;
EventSystem.Publish(new PlayerHitEvent(damage, this));
}
// 玩家初始化时订阅事件
void Start()
{
EventSystem.Subscribe<PlayerHitEvent>(OnHit);
}
void OnHit(PlayerHitEvent e)
{
stateMachine.HandleDamage(e.Damage);
}
// 状态机或状态内部处理
public void HandleDamage(int damage)
{
if (CurrentState is NormalState)
{
ChangeState(new HurtState());
health -= damage;
}
}
五、补充:状态机设计建议
- 状态模式(State Pattern) 是实现状态机的经典方式,每个状态是一个类,实现统一的接口,比如
IState,包含Enter()、Update()、Exit()、HandleEvent()等方法。 - 上下文(Context)是玩家自己,它持有当前状态对象,并委托行为给状态。
- 通过这种方式,状态的切换和行为都封装在状态类中,外部只关心"发生了什么",而不关心"怎么切换"。
总结一句话:
🎮 当其他类需要影响玩家状态时,不要直接操作玩家状态机,而应该通过事件通知、接口调用等间接方式,最终由玩家的状态机根据逻辑决定是否以及如何切换状态。推荐使用事件系统实现解耦,保障代码清晰可维护。