Unity进阶开发-FSM有限状态机

发布时间 2023-10-28 21:10:22作者: 清风扶云雾

# Unity进阶开发-FSM有限状态机

前言

我们在进行开发时,到了一定程度上,会遇到数十种状态,继续使用Unity的Animator控制器会出现大量的bool,float类型的变量,而这些错综复杂的变量与Animatator控制器如同迷宫版连线相结合会变得极其的复杂且无法良好维护扩展,出现一个BUG会导致开发过程中开发者承受极大的精神力,而这时候,使用有限状态机或者AI行为树便成为了一个极佳的选择,本文只记录了有限状态机的开发

使用有限状态机进行状态管理与切换可以大幅度的减少开发时候的难度,在开发过程中只需要关注各个状态间的切换即可

图示FSM工作过程:

我们可以看到,FSM的脚本一共分为两大块儿,一块儿继承自IState,这里面保存着状态内执行的行为,例如进入状态,离开状态,逻辑切换(用于判断是否切换到其他状态),PlayerState实现这个接口,并继承ScriptableObject类,看到ScriptableObject我们就知道了PlayerState的作用了,ScriptableObject类的对象不依赖场景中的对象可,是独立存在的数据文件。而在这里,PlayState保存的是状态的行为逻辑函数,这些行为逻辑函数的本质作用就是满足条件就去执行相关行为。当然,在FSM中用一个例子来说明,当玩家处于跑步状态时,PlayState文件发现满足执行跑步行为的逻辑,就需要去播放跑步动画。我们知道,ScriptableObject的数据是不会自己变化的,那么所有判断逻辑只能使用一遍,我们该如何解决这个问题,这就需要FSM第二的板块儿的类来实现了

我们不难发现,StateMachine是继承MonoBehaviour的,这就需要挂载到场景中了,并且也可以使用Update函数来时刻改变检测到的信息,那么,上文已经说了,PlayerState的数据不能主动变化,而StateMachine的数据可以进行变化,所以我们就可以通过一个函数来讲StateMachine中检测到数据来发给PlayerState,以此来实现PlayerState创建的数据文件可以连续进行逻辑判断

好了,现在开始上代码,用代码继续说

No.1 IState

public interface IState
{
    //进入状态
    void Enter() { }
    //退出状态
    void Exit() { }
    //状态逻辑更新
    void LogicUpdate() { }
}

在这里就是状态内部需要执行的行为函数

No.2 StateMachine

// <summary>
/// 持有所有状态类,并对他们进行管理和切换
/// 负责当前状态的更新
/// </summary>
public class StateMachine : MonoBehaviour
{
    IState currentState;                                           //当前状态
    public Dictionary<System.Type, IState> stateTable;             //字典,用于保存状态以及查询状态,方便状态切换时                                                                    //进行查找
    public void Update()
    {
        currentState.LogicUpdate();                                //执行每个状态的逻辑切换函数,可以使状态实现检测                                                                    //变换,类似于Update内的函数实时接收信息
    }
    //切换状态
    protected void SwitchOn(IState newState)
    {
        //当前状态变为新状态
        currentState = newState;
        //进入新状态
        currentState.Enter();
    }
    //
    public void SwitchState(IState newState)
    {
        //退出状态
        currentState.Exit();
        //进入新状态
        SwitchOn(newState);
    }
    //切换状态函数重载
    public void SwitchState(System.Type newStateType)
    {
        SwitchState(stateTable[newStateType]);                    //将字典内的状态传入
    }

}

No.3 PlayetState

public class PlayerState : ScriptableObject, IState
{
    /*************物理检测*************/
    protected bool isGround;                          //是否在地面
    /*************基础信息*************/
    protected bool isRun;                             //是否跑步
    protected bool isJump;                            //是否跳跃
    protected bool isIdle;                            //是否静止
    /*************相关组件*************/
    protected Rigidbody2D my_Body2D;                  //刚体组件,用于获取物体刚体属性
    protected Animator animator;                      //动画组件,用来播放动画
    protected PlayerStateMachine stateMachine;        //PlayerStateMachine,玩家状态机类,执行状态间的切换
    public void Initiatize(Animator animator, Rigidbody2D my_Body2D, PlayerStateMachine stateMachine)
    {//获取PlayerStateMachine传递进来的 动画,刚体,状态机类
        this.animator = animator;
        this.my_Body2D = my_Body2D;
        this.stateMachine = stateMachine;
    }
    public void PhysicalDetection(bool isGround)
    {
        this.isGround = isGround;
    }
    /// <summary>
    /// 状态信息传递
    /// </summary>
    public void BasicInformation(bool isRun,bool isJump,bool isIdle)
    {
        this.isRun = isRun;
        this.isJump = isJump;
        this.isIdle = isIdle;
    }
    //进入状态
    public virtual void Enter() { }
    //离开状态
    public virtual void Exit() { }
    //逻辑切换
    public virtual void LogicUpdate() { }
}

No.4 PlayerStateMachine

/// <summary>
/// 玩家状态机类
/// </summary>
public class PlayerStateMachine : StateMachine
{
    /*************************检测信息***************************/
    PlayerPhysicalDetection playerPhysicalDetection;                      //物理检测组件,这个是继承                                                                                       //MonoBehaviour,挂载到玩家身上来检测                                                                           //玩家的物理信息,例如是否位于地面
    /*************************状态信息***************************/
    //PlayerState资源文件
    [SerializeField] PlayerState[] states;
    Animator animator;                            //获取动画组件
    Rigidbody2D my_Body2D;                        //获取刚体组件
    void Awake()
    {
        /*************************物理检测信息***************************/
        playerPhysicalDetection=GetComponent<PlayerPhysicalDetection>();            //获取物理检测组件

        /*************************状态信息组件***************************/
        stateTable = new Dictionary<System.Type, IState>(states.Length);            //初始化字典
        animator = GetComponent<Animator>();                                        //获取动画组件
        my_Body2D = GetComponent<Rigidbody2D>();                                    //获取刚体组件
        //迭代器循环获取状态
        foreach (PlayerState state in states)
        {
            state.Initiatize(animator, my_Body2D, this);//将动画组件,刚体组件以及PlayerStateMachine传入进去
            //状态存入字典
            stateTable.Add(state.GetType(), state);
        }
    }
    private void Start()
    {//在开始时执行Idle,进入Idle状态
        SwitchOn(stateTable[typeof(PlayerState_Idle)]);
    }
    private new void Update()
    {
        base.Update();//执行父类StateMachine的Update函数
        foreach (PlayerState state in states)
        {//将检测信息传入进去
            state.PhysicalDetection(playerPhysicalDetection.isGround);
            state.BasicInformation( isRun, isJump, isIdle);
        }
    }
}

No.5 PlayerState_Idle

[CreateAssetMenu(menuName = "StateMachine/PlayerState/Idle", fileName = "PlayerState_Idle")]//创建文件
public class PlayerState_Idle : PlayerState
{
    /*****************物理检测*******************/
    public override void Enter()
    {
        //执行该状态数据文件,首先执行进入状态函数,在进入状态函数执行相关的行为
        //进入状态,默认播放Idle动画
        animator.Play("PlayerIdle");
    }
    
    //逻辑切换函数,当检测到处于某种状态,立刻执行该状态的数据文件
    public override void LogicUpdate()
    {
        if(isRun)
        {
            stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Run)]);
        }
        if(isJump)
        {
            stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Jump)]);
        }
        if(my_Body2D.velocity.y<0&&!isGround)
        {
            stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Fall)]);
        }
    }
}

No.6 PlayerState_Jump

[CreateAssetMenu(menuName = "StateMachine/PlayerState/Jump", fileName = "PlayerState_Jump")]//创建文件
public class PlayerState_Jump : PlayerState
{
    /*****************物理检测*******************/
    public override void Enter()
    {
        //执行该状态数据文件,首先执行进入状态函数,在进入状态函数执行相关的行为
        //进入状态,默认播放Idle动画
        animator.Play("PlayerJump");
    }
    
    //逻辑切换函数,当检测到处于某种状态,立刻执行该状态的数据文件
    //该脚本中,跳跃后无法执行其它行为,若有需要可以添加判断
    public override void LogicUpdate()
    {
        if(my_Body2D.velocity.y<0&&!isGround)
        {
            stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Fall)]);
        }
    }
}

No.7 PlayerState_Fall

[CreateAssetMenu(menuName = "StateMachine/PlayerState/Fall", fileName = "PlayerState_Fall")]//创建文件
public class PlayerState_Jump : PlayerState
{
    /*****************物理检测*******************/
    public override void Enter()
    {
        //执行该状态数据文件,首先执行进入状态函数,在进入状态函数执行相关的行为
        //进入状态,默认播放Fall动画
        animator.Play("PlayerFall");
    }
    
    //逻辑切换函数,当检测到处于某种状态,立刻执行该状态的数据文件
    //该脚本中,掉落时无法执行其它行为,若有需要可以添加判断
    public override void LogicUpdate()
    {
        if(isGround)
        {//落地进入Idle状态
            stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Idle)]);
        }
    }
}

No.7 PlayerState_Run

[CreateAssetMenu(menuName = "StateMachine/PlayerState/Run", fileName = "PlayerState_Run")]//创建文件
public class PlayerState_Idle : PlayerState
{
    /*****************物理检测*******************/
    public override void Enter()
    {
        //执行该状态数据文件,首先执行进入状态函数,在进入状态函数执行相关的行为
        //进入状态,默认播放Run动画
        animator.Play("PlayerRun");
    }
    
    //逻辑切换函数,当检测到处于某种状态,立刻执行该状态的数据文件
    public override void LogicUpdate()
    {
        if(isIdle)
        {
            stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Idle)]);
        }
        if(isJump)
        {
            stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Jump)]);
        }
        if(my_Body2D.velocity.y<0&&!isGround)
        {
            stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Fall)]);
        }
    }
}

以上便是一个简单的状态机

总结与拓展延伸

FSM可以大幅减少玩家动画间的判断和切换,只需要关注一个状态到另一个状态的切换条件,满足条件就切换,不满足就继续执行

FSM在玩家上的使用并不明显,因为在这上面,进入某种状态后,我们只播放了动画,并没有执行其他行为。当我们用在怪物AI中会更加的方便,例如怪物处于巡逻状态,播放巡逻动画,执行巡逻的脚本文件(建立一个类,专门用来存怪物AI个状态行为函数,通过传递的方式将该类对象传递给EnemyState,由EnemyState进行操作),怪物处于追击玩家状态,播放追击动画,并执行追击玩家的行为函数,在怪物AI中,FSM的应用会更加轻松的管理怪物,怪物可以放技能了,就进入技能状态,发现玩家就进入追击玩家,一个条件一个行为,更加有利于FSM进行管理