Unity advanced improvements--more convenient control system--application of finite state machine FSM

Preface

When we develop, to a certain extent, we will encounter dozens of states. If we continue to use Unity’s Animator controller, a large number of bool and float type variables will appear, and these intricate variables are closely related to the Animatator control. The combination of the machine and the maze version of the connection will become extremely complex and cannot be well maintained and expanded. The emergence of a BUG will cause the developer to bear great mental strength during the development process. At this time, using a finite state machine or AI behavior tree can It has become an excellent choice.This article only records the development of finite state machines

Using finite state machines for state management and switching can greatly reduce the difficulty of development. During the development process, you only need to pay attention to the switching between each state.

The diagram shows the working process of FSM:

We can see that the FSM script is divided into two large parts, one of which is inherited from IState, which stores the behaviors executed in the state, such as entering the state, leaving the state, and logical switching (used to determine whether to switch to other states) ), PlayerState implements this interface and inherits the ScriptableObject class. When we see ScriptableObject, we know the role of PlayerState. The objects of the ScriptableObject class do not depend on the objects in the scene, but are independent data files. Here, PlayState saves the behavioral logic functions of the state. The essential function of these behavioral logic functions is to execute relevant behaviors when conditions are met. Of course, use an example in FSM to illustrate that when the player is in the running state, the PlayState file finds that the logic for executing the running behavior is met, and the running animation needs to be played. We know that the data of ScriptableObject will not change by itself, so all judgment logic can only be used once. How should we solve this problem? This requires the class of the second section of FSM to implement it.

It is not difficult for us to find that StateMachine inherits MonoBehaviour, which needs to be mounted into the scene, and the Update function can also be used to change the detected information at all times. Then, as mentioned above, the data of PlayerState cannot be actively changed. The data of StateMachine can be changed, so we can use a function to detect the data in StateMachine and send it to PlayerState, so that the data files created by PlayerState can continuously make logical judgments.

Okay, now let’s start with the code and continue using the code.

No.1 IState

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

Here is the behavior function that needs to be executed inside the state

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.8 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)]);
        }
    }
}

The above is a simple state machine

Summary and expansion

FSM can greatly reduce the judgment and switching between player animations. You only need to pay attention to the switching conditions from one state to another. If the conditions are met, switch, and if not, continue execution.

The use of FSM on players is not obvious, because here, after entering a certain state, we only play animations and do not perform other actions. It will be more convenient when we use it in monster AI. For example, if the monster is in patrol state, play the patrol animation, and execute the patrol script file (create a class specifically to store the monster AI status behavior function, and pass the class to The object is passed to EnemyState, which is operated by EnemyState). The monster is in the state of chasing the player, plays the chasing animation, and executes the behavior function of chasing the player. In the monster AI, the application of FSM will make it easier to manage monsters, and monsters can use skills. Enter the skill state, and when a player is found, he will start chasing the player. One condition and one action are more conducive to FSM management.

Additional examples

The following is a simple EnemyAI diagram I wrote. It is only used to show the application of FSM in monster AI. It is not a complete illustration.

Guess you like

Origin blog.csdn.net/qq_63486332/article/details/133874614