首页 > 代码库 > 简单实用的 Unity 状态机设计

简单实用的 Unity 状态机设计

状态机是非常常用的游戏编程模式,状态机的设计也有简单或复杂的区别。

 

我脑海里的状态机

状态机是什么样的?这是一个非常典型的状态机设计(随手写的):

// 状态类    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,你可以根据实际需求选择不同的更新模式。另外为了方便编程添加了状态计时。

如果状态类还是需要多层次的继承呢?委托的好处之一就是自由。在继承的类里,你可以获得、替换或增加委托的方法,还有什么是不可实现的?

 

简单实用的 Unity 状态机设计