【状态模式】

状态模式主要解决的是当控制一个对象状态转换的条件判断过于复杂时,把判断逻辑转移到表示不同状态的一系列类中,从而简化复杂的逻辑判断。(如果状态转换条件判断很简单,就没必要用状态模式了)

【FSM的相关概念】

输入:在上一篇文章中,我们并没有显式给状态机输入,在这个实现中给显式的输入,我们暂且认为所有输入都是string类型的。在实际运用中,可以写一个输入类。

状态:表示对象的某种形态,在当前形态下可能会拥有不同的行为和属性。

状态机:每一个时刻只能处于一个状态;拥有对象所有状态的集合;会接受输入,并根据对象当前的状态转换到下一个状态

输出:确切的说,输出指转换到某个状态后,在这个状态内对象所有的行为、表现等都是输出。

动作:指状态机的行为,包括进入状态动作,离开状态动作

【FSM的实现】

using System.Collections.Generic;

public class FSM //不要继承MonoBehaviour
{
    //状态机运行时状态参数,当前状态,上一个状态,下一个状态。
    //复杂的状态机状态关联多,需要这三个参数,简单的状态机不需要那么多
    public StateBase curState { get; private set; }
    public StateBase lastState { get; private set; }
    public StateBase nextState { get; private set; }

    //状态机内所有对象集合,简单的状态机不需要
    private Dictionary<RoleState, StateBase> states = new Dictionary<RoleState, StateBase>();


    //状态机整体的参数,简单的状态机不需要
    private StateBase defaultState;//状态机默认状态,也即第一个状态
    private StateBase exitState;//状态机退出状态,也即最后一个状态

    public void Init(StateBase defaultState)//状态机初始化
    {
        this.defaultState = defaultState;
        curState = defaultState;
        curState.Enter();
    }

    public void Update(string input)//状态机的执行,即切换状态
    {
        if(curState == null)
            return;
        nextState = curState.HandleInput(input);//处理当前输入,获取下一状态
        if (nextState != curState && nextState != null)
        {
            curState.Exit();
            lastState = curState;
            curState = nextState;//状态切换
            curState.Enter();
        }
        curState.Update();
    }

    #region 状态机对外提供的方法

    public void AddState(RoleState stateId,StateBase state)
    {
        states.Add(stateId,state);
    }

    public bool Remove(RoleState stateId)
    {
        if (states.ContainsKey(stateId))
        {
            states.Remove(stateId);
            return true;
        }

        return false;
    }

    public StateBase GetState(RoleState stateId)
    {
        if (states.ContainsKey(stateId))
            return states[stateId];
        return null;
    }


    #endregion
}

public class StateBase//可以是接口,也可以是基类,具体使用哪个看情况
{
    public RoleState stateId { get;private set; }//状态ID
    public FSM parent { get;private set; }//状态归属哪个状态机

    public StateBase(FSM parent, RoleState stateId)
    {
        this.parent = parent;
        this.stateId = stateId;
    }  
  
    public virtual void Enter(){}//进入动作,可以对状态做初始化
    public virtual void Exit(){}//退出动作,可以对状态对重置

    //在这个状态内要执行的操作,可以有多个
    public virtual void Update(){}//在状态内执行相关输出

    public virtual StateBase HandleInput(string input)//在当前状态内处理输入,状态转换条件判断
    {
        return null;
    }
}

在这个实现中,我们只在状态机内进行状态切换,这正是状态机的单一职责。将状态转换的条件判断和状态内的输出移出了,并将这两者移入到表示不同状态的一系列类中。

回顾我们之前的最简单的状态机的实现,先是将状态转换、状态转换的条件判断、状态内的输出都放在状态机内;随后将状态转换的条件判断移出,可以在任何类中进行状态转换条件判断。

【FSM的使用】

首先,我们要将对象的所有状态类写出来。

public class JumpState : StateBase
{
    public JumpState(FSM parent, RoleState stateId) : base(parent, stateId) { }

    public override void Enter()
    {
        base.Enter();
        //该状态的初始化行为
    }

    public override void Exit()
    {
        base.Exit();
        //离开状态时重置初始化参数
    }

    public override void Update()
    {
        base.Update();
        //状态内执行相关输出
    }

    public override StateBase HandleInput(string input)
    {
       return base.HandleInput(input);
    }

}

public class AttackState : StateBase
{
    public AttackState(FSM parent, RoleState stateId) : base(parent, stateId)
    {
    }
    public override void Enter()
    {
        base.Enter();
        //该状态的初始化行为
    }

    public override void Exit()
    {
        base.Exit();
        //离开状态时重置初始化参数
    }

    public override void Update()
    {
        base.Update();
        //状态内执行相关输出
    }

    public override StateBase HandleInput(string input)
    {
        return base.HandleInput(input);
    }

}

//剩下的不写了,对象有多少个状态就要写多少个状态类

随后,列出状态转换图/表,按照这个图/表写好每个状态内的HandleInput和Update类,Enter和Exit去决定于具体的逻辑,这里不作表述。(代码以跳跃状态和行为状态为例)

public class WalkState : StateBase
{
    public WalkState(FSM parent, RoleState stateId) : base(parent, stateId)
    {
    }

    public override void Enter()
    {
        base.Enter();
        //该状态的初始化行为
    }

    public override void Exit()
    {
        base.Exit();
        //离开状态时重置初始化参数
    }

    public override void Update()
    {
        base.Update();
        //状态内执行相关输出
        Debug.Log("开始行走");
    }

    public override StateBase HandleInput(string input)
    {
        base.HandleInput(input);
        if (input.Equals("按下I键"))
        {
            return parent.GetState(RoleState.Idle);
        }
        else if(input.Equals("按下J键"))
        {
            return parent.GetState(RoleState.Jump);
        }
        else if(input.Equals("按下A键"))
        {
            return parent.GetState(RoleState.Attack);
        }
        else if(input.Equals("按下D键")||input.Equals("受到攻击"))
        {
            return parent.GetState(RoleState.Dead);
        }

        return null;
    }
}

public class JumpState : StateBase
{
    public JumpState(FSM parent, RoleState stateId) : base(parent, stateId) { }

    public override void Enter()
    {
        base.Enter();
        //该状态的初始化行为
    }

    public override void Exit()
    {
        base.Exit();
        //离开状态时重置初始化参数
    }

    public override void Update()
    {
        base.Update();
        //状态内执行相关输出
        Debug.Log("开始跳跃");
    }

    public override StateBase HandleInput(string input)
    {
       base.HandleInput(input);
       if (input.Equals("按下A键"))
       {
           return parent.GetState(RoleState.Attack);
       }

       if (input.Equals("按下D键") || input.Equals("受到攻击"))
       {
           return parent.GetState(RoleState.Dead);
       }

       return null;
    }

}

 之后,我们可以在对象内使用状态机,并给状态机输入。

public class Player:MonoBehaviour
{
    private FSM fsm;

    public Player()
    {
        //生成状态类实例并添加到状态机
        fsm.AddState(RoleState.Attack,new AttackState(fsm,RoleState.Attack));
        fsm.AddState(RoleState.Idle, new IdleState(fsm, RoleState.Idle));
        fsm.AddState(RoleState.Jump, new JumpState(fsm, RoleState.Jump));
        fsm.AddState(RoleState.Walk, new WalkState(fsm, RoleState.Walk));
        fsm.AddState(RoleState.Dead, new DeadState(fsm, RoleState.Dead));

        //状态机初始化
        fsm.Init(fsm.GetState(RoleState.Idle));
    }

    public void Update()
    {
        fsm.Update(input);
    }

    private string input;
    public void SetInput(string input)
    {
        this.input = input;
    }
}

当我们增加拾取状态时,需要修改状态转移图,并实现新的拾取状态类,这就是再做扩展。对拾取状态类中的HandleInput方法,我们需要重新写,但不需要更改状态机的Update方法。

【功能完善FSM】

上面实现的FSM只具有基本的功能,我们需要对外提供状态改变回调、强制状态转换、运行时间等,下面是实现。

using System.Collections.Generic;
using UnityEngine;

public class FSM //不要继承MonoBehaviour
{
    //状态机运行时状态参数,当前状态,上一个状态,下一个状态。
    //复杂的状态机状态关联多,需要这三个参数,简单的状态机不需要那么多
    public StateBase curState { get; private set; }
    public StateBase lastState { get; private set; }
    public StateBase nextState { get; private set; }

    private List<RoleState> history = new List<RoleState>();//状态切换历史记录

    //状态机内所有对象集合,简单的状态机不需要
    private Dictionary<RoleState, StateBase> states = new Dictionary<RoleState, StateBase>();


    //状态机整体的参数,简单的状态机不需要
    private StateBase defaultState;//状态机默认状态,也即第一个状态
    private StateBase exitState;//状态机退出状态,也即最后一个状态

    //状态机回调
    public delegate void OnStateChange(StateBase curState,StateBase lastState);//是否带参数/返回值,带什么参数/返回值,根据项目自己确定

    private OnStateChange OnPreStateChange;//状态改变前
    private OnStateChange OnPostStateChange;//状态改变后

    //状态机时间
    public float startTime { get; private set; }
    public float runTime { get; private set; }


    public void Init(StateBase defaultState)//状态机初始化
    {
        this.defaultState = defaultState;
        curState = defaultState;
        curState.Enter();
        history.Add(curState.stateId);
        startTime = Time.time;
        runTime = 0f;
    }

    public void Update(string input)//状态机的执行,即切换状态
    {
        if(curState == null)
            return;
        nextState = curState.HandleInput(input);//处理当前输入,获取下一状态
        if (nextState != curState && nextState != null)
        {
             ChangeState(curState,nextState);
        }
        curState.Update();
        runTime += Time.deltaTime;
    }

    #region 状态机对外提供的方法

    public void AddState(RoleState stateId,StateBase state)
    {
        states.Add(stateId,state);
    }

    public bool Remove(RoleState stateId)
    {
        if (states.ContainsKey(stateId))
        {
            states.Remove(stateId);
            return true;
        }

        return false;
    }

    public StateBase GetState(RoleState stateId)
    {
        if (states.ContainsKey(stateId))
            return states[stateId];
        return null;
    }

    public void ForceChangeCurState(StateBase state)//强制转换状态
    {
        ChangeState(curState,state);
    }

    public void ForceStop()//强制状态机退出
    {
        ForceChangeCurState(exitState);
    }

    public bool isStop => curState == exitState;//状态机是否停止


    //状态注册
    public void RegisterPostStateChange(OnStateChange onPostStateChange)
    {
        OnPostStateChange += onPostStateChange;
    }

    public void UnRegisterPostStateChange(OnStateChange onPostStateChange)
    {
        OnPostStateChange -= onPostStateChange;
    }

    public void RegisterPreStateChange(OnStateChange onPreStateChange)
    {
        OnPreStateChange += onPreStateChange;
    }

    public void UnRegisterPreStateChange(OnStateChange onPreStateChange)
    {
        OnPreStateChange -= onPreStateChange;
    }

    public void Reset()//状态机重置
    {
        OnPreStateChange = null;
        OnPostStateChange = null;

        curState = defaultState;
        lastState = null;
        nextState = null;

        startTime = Time.time;
        runTime = 0f;
    }
    #endregion

    #region 状态机的私有方法

     private void ChangeState(StateBase curState,StateBase nextState)
    {
        curState.Exit();
        OnPreStateChange?.Invoke(curState, lastState);
        lastState = curState;
        curState = nextState;//状态切换
        OnPostStateChange?.Invoke(curState, lastState);
        curState.Enter();
    }

    #endregion
}

public class StateBase//可以是接口,也可以是基类,具体使用哪个看情况
{
    public RoleState stateId { get;private set; }//状态ID
    public FSM parent { get;private set; }//状态归属哪个状态机

    //状态时间
    public float startTime { get; private set; }
    public float runTime { get; private set; }

    //状态可以不用回调,因为状态机的状态改变回调给了两个状态参数

    public bool active { get; set; } = true;//状态是否激活
    public StateBase(FSM parent, RoleState stateId)
    {
        this.parent = parent;
        this.stateId = stateId;
    }

    public virtual void Enter()
    {//进入动作,可以对状态做初始化;初始化也可以在生成状态类实例时做
     //前者初始化的方式可能与运行时参数有关,后者无关
        startTime = Time.time;
    }
    public virtual void Exit()//退出动作,可以对状态对重置
    {
        runTime = 0;
    }

    //在这个状态内要执行的操作,可以有多个
    public virtual void Update()//在状态内执行相关输出
    {
        runTime += Time.deltaTime;
    }

    public virtual StateBase HandleInput(string input)//在当前状态内处理输入
    {
        if (!active)
            return null;
        return null;
    }
}

【添加转移的FSM】

可以发现,当增加拾取状态时,需要修改行走状态的HandleInput方法,这仍需要修改。因此,这种FSM只适合状态关联弱的对象。如果状态关联强,每添加一个新的状态,都会使得原有状态类的HandleInput要修改,那么我们添加转移类,将状态转换条件判断从状态类中移出。

public class StateBase//可以是接口,也可以是基类,具体使用哪个看情况
{
    public RoleState stateId { get;private set; }//状态ID
    public FSM parent { get;private set; }//状态归属哪个状态机

    //状态时间
    public float startTime { get; private set; }
    public float runTime { get; private set; }

    //状态可以不用回调,因为状态机的状态改变回调给了两个状态参数

    public bool active { get; set; } = true;//状态是否激活

    //状态的全部转换,需要在Player对象类中初始化生成并添加进对应的状态中
    public List<TransitionBase> transitions = new List<TransitionBase>();
    public void AddTransition(TransitionBase transition)
    {
        transitions.Add(transition);
    }

    public StateBase(FSM parent, RoleState stateId)
    {
        this.parent = parent;
        this.stateId = stateId;
    }

    public virtual void Enter()
    {//进入动作,可以对状态做初始化
        startTime = Time.time;
    }
    public virtual void Exit()//退出动作,可以对状态对重置
    {
        runTime = 0;
    }

    //在这个状态内要执行的操作,可以有多个
    public virtual void Update()//在状态内执行相关输出
    {
        runTime += Time.deltaTime;
    }

    public virtual StateBase HandleInput(string input)//在当前状态内处理输入
    {
        if (!active)
            return null;
        foreach (TransitionBase item in transitions)//判断该输入是否满足某个转移,轮询所有转移
        {
            if (item.fromState != this)
            {
                continue;
            }
            if(item.CanTransition(input))
            {
                return item.toState;
            }
        }
        return null;
    }
}

public class TransitionBase
{
    //从某个状态转换到另一个状态
    public StateBase fromState;
    public StateBase toState;

    public TransitionBase(StateBase fromState, StateBase toState)
    {
        this.fromState = fromState;
        this.toState = toState;
    }

    public virtual bool CanTransition(string input)
    {
        //从某个状态转换到另一个状态的逻辑判断
        return true;
    }

}

添加转移前,某个状态到其他所有状态的转换条件判断都是该在状态类中完成的;添加转移后,某个状态到其他所有状态的转换条件判断在一个新的转移类中,如果一个状态A可以转换到另外五个状态,那么需要写五个转移类,如果新增加了一个状态B,只需要新写一个状态A到状态B的转移即可。使用时,在Player类中,还需要实例化这些转移类并添加到对应的状态类当中。

其缺点是,如果有很多状态都可以转换到状态B,那么要写很多个转移类。也即新增一个状态,要写一个状态类和很多的转移类。 

值得注意的是,添加转移类并没有解决,从一个状态转换到另一个状态的条件判断复杂的问题。这意味着我们需要修改转移类的CanTransition(string input)方法,同样会违背开放封闭原则。

在实际的项目中,如果我们的转移条件简单,采用这种方式是OK的。

如果转移条件复杂,我们需要采用更好的实现方式。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐