基于条件检测的分层状态机设计思路P5
喵七叔
2022年11月23日 02:40
收录于文集
共12篇

# 前言

    - 上期我们使用组合模式代替了继承,但是忘记了修改表现层的逻辑,这里做一些补充

代码块
C#
自动换行
复制代码
public class StateBulider : IStateBuilder
{
  public Dictionary<string, State> Create(out string firstStateName)
  {
    firstStateName = "Idle";
    var ground = new GroundState();
    return new Dictionary<string, State>()
    {
       {"Idle", new IdleState().SetFather(ground)},
       {"Move", new MoveState().SetFather(ground)},
    };
  }
}
复制成功

我们将之前作为父类的 Ground 状态 提取出来 传递到子状态中 因为目前子类直接继承与状态基类,我们只需要重写抽象类即可。大致代码如下

代码块
C#
自动换行
复制代码
public class IdleState : State
{
    public override void Enter()
    {
        Debug.Log("IdleEnter");
    }
    public override void Exit()
    {
        Debug.Log("IdleExit");
    }
    public override void Update()
    {
        Debug.Log("IdleUpdate");
    }
    protected override string CheckCondition()
    {
        // 当按下 W 键 转换到移动状态
        if (Input.GetKeyDown(KeyCode.W)) return "Move";
        return null;
    }
}
复制成功

可以看到 我们不需要在重写函数中再次调用父类函数,这也简化了子类的调用!补充完毕,进入这一期内容!

# 可拓展生命周期

    - 上一期咱们留了个两个关键字哈!可拓展生命周期和规则状态机。我们先来看一下上图中的状态方法 除了必要的条件检测外,状态基类只提供了三个抽象方法,分别是进入、更新和退出,在状态转换时,需要让当前状态执行退出函数,变更状态后,新状态执行进入函数,而后进入状态的更新阶段,这部分就是生命周期和转换规则。那么我们来展望一下未来,在后期我们在应对不同的使用情况和使用场景时,可能会需要新的生命周期方法和新的转换规则来辅助我们进行更复杂的操作。所以一个可拓展的生命周期显得非常重要了,那么我们应该使用什么结构来拓展呢?答案是枚举。为什么呢?因为枚举相对比较轻量,而且定义和扩展都十分方便。我们可以将状态机基类 加入泛型,将泛型约束为 Enum 在基类中声明一个 泛型枚举 起名为Life,即生命周期!有了这个呢,我们的状态基类中就不需要提供具体的生命周期函数,只需提供一个触发方法即可,因为设计阶段生命周期并没有真正产生具体类型,所以我们还需要将状态基类设计成泛型类,约束为 Enum【枚举】修改如下

代码块
C#
自动换行
复制代码
public abstract class State<T> where T : Enum
{
    private State<T> mFatherState;
    public State<T> SetFather(State<T> father)
    {
        mFatherState = father;
        return this;
    }
    public bool Check(out string nextStateName)
    {
        if (mFatherState != null &&
            mFatherState.Check(out nextStateName)) return true;
        nextStateName = CheckCondition();
        return !string.IsNullOrEmpty(nextStateName);
    }
    // 可以传入对应枚举 调用 Trigger 的对应方法
    public void Trigger(T life) 
    {
        // 这里需要先判断父类状态是否存在,如果存在就执行父类状态行为
        mFatherState?.Trigger(life);
        // 执行当前状态 这里代码上一期漏掉了
        Execute(life);
    }
    protected abstract void Execute(T life);
    protected abstract string CheckCondition();
}
复制成功

上述代码补充了一些上一期漏掉的代码,将之前的生命周期函数简化成了Execute抽象方法让子类实现具体逻辑,对外给状态机类提供一个Trigger方法用于更新逻辑 ,修改完成之后,很多地方会报错,我们需要一一修复一下,比如构建器

代码块
C#
自动换行
复制代码
public interface IStateBuilder<T> where T : Enum
{
    Dictionary<string, State<T>> Create(out string firstStateName);
}
复制成功

在状态机类中,我们也需要更改一下调用逻辑 修改如下

代码块
C#
自动换行
复制代码
public abstract class HFSM<T> : MonoBehaviour where T : Enum
{
    // 状态生命周期 需配合规则使用
    protected T Life;
    // 状态集合
    private Dictionary<string, State<T>> mStates;
    // 保护的当前状态方便子类访问
    protected State<T> mCurState;
    // 构建器 需要适配 泛型
    protected abstract IStateBuilder<T> Builder { get; }
    private void Awake()
    {
        mStates = Builder.Create(out string defaultStateName);
        mCurState = mStates[defaultStateName];
        Life = FirstLife;
    }
    // 子类重写 首次执行的生命周期
    protected abstract T FirstLife { get; }
    // 对外提供状态切换方法
    protected void SwitchState(string nextStateName)
    {
        if (!mStates.TryGetValue(nextStateName, out var nextState))
            throw new Exception("状态不存在,请检查状态输入是否正确!");
        SwitchState(nextState);
    }
    protected abstract void SwitchState(State<T> nextState);
    // 转换规则
    protected abstract void SwitchLifeByRule(ref T life);
    // 更新当前状态
    protected virtual void Update()
    {
        if (mCurState == null) return;
        // 执行当前所有状态动作
        mCurState.Trigger(Life);
        // 转换状态生命周期
        SwitchLifeByRule(ref Life);
        // 对下一个状态进行检测 如果存在可转换的状态 就执行状态转换
        if (!mCurState.Check(out string nextStateName)) return;
        SwitchState(nextStateName);
    }
}
复制成功

在上述代码中可以看到 在Awake中 我取消了进入方法,但是因为当前状态是泛型,我们无法得知具体的类型,也就是没办法知道应该使用哪个值作为默认值,所以需要让子类重写一个 FirstLife 属性 让父类提前调用,避免初始值不正确,因为在父类中并不需要关注具体生命周期的规则逻辑,所以我在状态机基类中加入了一个用于状态转换的抽象方法和用于定义生命周期转换规则的抽象方法, 方便子类重写。因为子类有可能会用到Update方法,我们将Update写成保护的虚函数 让子类可以重写,这样主要的设计就做完了,这里的逻辑可能有点绕,大家不懂的可以在文章下方留言,Up会逐一解答。接下来我们来定义具体的规则状态机类

# 规则状态机

我们创建一个规则状态机类,通过继承和重写的方式,来定义状态转换的规则逻辑,代码如下

代码块
C#
自动换行
复制代码
public abstract class RuleFSM : HFSM<E_StateLife>
{
    protected override E_StateLife FirstLife => E_StateLife.Enter;
    protected override void SwitchState(State<E_StateLife> nextState)
    {
        mCurState.Trigger(E_StateLife.Exit);
        Life = E_StateLife.Enter;
        mCurState = nextState;
    }
    protected override void SwitchLifeByRule(ref E_StateLife life)
    {
        // 这里为了确保 Enter 方法只执行一次 当执行完 Enter 立刻转换到 Update
        if (life == E_StateLife.Enter) life = E_StateLife.Update;
    }
}
复制成功

上述代码中 我们参照基础状态机的规则样式 让状态机从Enter方法进入,切换状态时执行Trigger 函数的退出方法,生命周期的切换规则很简单,只需要保证进入方法只执行一次并且执行完成进入更新方法即可。我们来看一下状态子类部分的代码

代码块
C#
自动换行
复制代码
public class MoveState : State<E_StateLife>
{
    protected override void Execute(E_StateLife life)
    {
        switch (life)
        {
            case E_StateLife.Enter:
                Debug.Log("MoveEnter");
                break;
            case E_StateLife.Update:
                Debug.Log("MoveUpdate");
                break;
            case E_StateLife.Exit:
                Debug.Log("MoveExit");
                break;
        }
    }
    protected override string CheckCondition()
    {
        // 当抬起 W 键 转换到闲置状态
        if (Input.GetKeyUp(KeyCode.W)) return "Idle";
        return null;
    }
}
复制成功

可以看到我们通过一个Switch 关键字 可以很方便的加减生命周期方法数量,也极大的提高了程序的可拓展性。从这一期开始,我们的状态机设计算是完成了一大半,剩下就是一些辅助的功能,咱下一期来 继续聊,拜了个拜!