状态机是非常常用的游戏编程模式,状态机的设计也有简单或复杂的区别。
我脑海里的状态机
状态机是什么样的?这是一个非常典型的状态机设计(随手写的):
// 状态类 class State { // 保存的状态机引用 StateMachine _machine; // 构造状态,保存状态机引用 public State(StateMachine machine) { _machine = machine; } // 进入状态 public virtual void OnEnter() { } // 离开状态 public virtual void OnLeave() { } // 更新状态 public virtual void OnUpdate() { } } // 状态机类 class StateMachine { // 当前状态 State _state; // 改变状态 public void ChangeState(State newState) { _state.OnLeave(); _state = newState; _state.OnEnter(); } // 更新当前状态 void Update() { _state.OnUpdate(); } }
这段代码主要表达了这样的思想:
1.状态含有进入、更新、离开 3 个可重写的方法;
2.状态机和状态的关系是 1 对多;
3.一个状态机内同一时间最多只有一个状态是“活动”的。
为什么没有看到 Transition 的存在?因为我认为添加这个类失去的比获得的更多。一方面状态转移应该存在于设计图上而不是代码里,另一方面转移的本质就是执行一个状态切换(可能还要做一些其他事情),这样的“一个过程”没有对象化的充足理由。这只是我的个人观点,如果你有想说的可以给我留言。
这个设计存在的问题
当你使用这个设计时,你需要继承状态类来实现自己的状态,重写 OnEnter,OnLeave 或 OnUpdate。想象一下为了执行一个简单的操作而定义一个新的类,然后使用这个类来创建一个状态实例,并且很可能这个类只需要用来创建一个状态实例。时间久了你可能会怀疑这一切的意义是什么。如果这还不算什么:你可能注意到了,状态保存了所属状态机的引用。在这些重写方法里,你不可避免的需要访问所属状态机的属性或方法,所以先保存状态机的引用是必要的。而且你可能还需要重新定义一个 get 属性,来将基本的 StateMachine 转换为自定义的状态机类型。想象一下每一次读取角色的生命值都要加一个“character.”的前缀。怎么样才能让事情变的更简单?
最后的解决办法
// 状态 public class State { // 进入 public Action onEnter; // 离开 public Action onLeave; // 更新 public Action<float> onUpdate; } // 状态机 public class StateMachine : MonoBehaviour { // 当前状态 private State _state; public State state { // 获取当前状态 get { return _state; } // 切换状态 set { if (_state != null && _state.onLeave != null) _state.onLeave(); _stateTime = 0; _state = value; if (_state != null && _state.onEnter != null) _state.onEnter(); } } // 状态时间 private float _stateTime; public float stateTime { get { return _stateTime; } } // 更新状态(在 FixedUpdate,Update 或 LateUpdate 中调用) protected void UpdateState(float deltaTime) { _stateTime += deltaTime; if (_state != null && _state.onUpdate != null) _state.onUpdate(deltaTime); } }
状态没有虚拟或抽象方法了,被替换为委托,并且不再保存状态机的引用。在实际使用中,大多数情况下都不需要继承 State 类了,转而为各个状态的委托初始化,用来初始化的所有方法可以直接写在状态机类里,因此这些方法可以直接访问状态机的所有成员。为了更方便在 Unity 中使用,状态机继承了 Monobehaviour,你可以根据实际需求选择不同的更新模式。另外为了方便编程添加了状态计时。
如果状态类还是需要多层次的继承呢?委托的好处之一就是自由。在继承的类里,你可以获得、替换或增加委托的方法,还有什么是不可实现的?