Unity——有限狀態機FSM修改

小紫蘇發表於2021-11-15

FSM狀態機改

一.前言

FSM狀態機初版

之前寫過一版有限狀態機,後來發現很多問題;

前一個版本是記錄了當前的狀態,切換狀態時,要等下一幀狀態機Update的時候才會調動上個狀態的退出,總會有一幀的延遲

除了導致動作延遲外,狀態很多的情況報錯也無法追述,斷點只能回到狀態機中;

因此做了如下修改;

1.狀態機不再繼承MonoBehaviour,只需要是單例,儲存所有狀態基類;

2.狀態機提供切換狀態的方法SwitchAction,傳參下個狀態ID;

3.切換狀態時呼叫上一個狀態的退出週期,再呼叫當前狀態的開始週期;

4.同時將當前狀態的引用重新賦值為傳入的狀態;

5.狀態機提供Run方法給角色控制器呼叫,角色控制器Update只執行當前狀態的Run;

效果展示:

Honeycam 2021-11-15 17-18-07

二.修改

修改後FSM,除增刪查外新增切換狀態函式SwitchState;

提供FSM狀態機的生命週期;FSMInit,FSMRun,FSMEnd;

public class FSM<T>
{
    private Dictionary<int, StateBase<T>> FSMActDic;
    private StateBase<T> curState;
    
    //切換狀態時呼叫
    public void SwitchState(int nextID)
    {
        curState.OnExit();
        curState = FSMActDic[nextID];
        curState.OnEnter();
    }
    
    public int GetCurState()
    {
        foreach (var kv in FSMActDic)
        {
            if (kv.Value == curState)
                return kv.Key;
        }

        return -1;
    }
    
    public FSM()
    {
        FSMActDic = new Dictionary<int, StateBase<T>>();
    }

    //增
    public void AddState(int id, StateBase<T> state)
    {
        if(FSMActDic.ContainsKey(id))
            return;

        FSMActDic.Add(id, state);
    }

    //刪
    public void RemoveSatate(int id)
    {
        if (FSMActDic.ContainsKey(id))
            FSMActDic.Remove(id);
    }

    //獲取
    public StateBase<T> GetState(int id)
    {
        if (!FSMActDic.ContainsKey(id))
            return null;

        return FSMActDic[id];
    }

    //狀態機初始化呼叫,給curState賦值並呼叫其OnStay
    public void FSMInit(int id)
    {
        curState = FSMActDic[id];
        curState.OnStay();
    }

    //每幀執行
    public void FSMRun()
    {
        curState.OnStay();
    }
    
    //退出狀態機執行
    public void FSMEnd()
    {
        curState.OnExit();
    }
}

三.測試程式碼

使用狀態先初始化,同時設定初始狀態;

角色控制類負責初始化和執行FSM狀態機;

public class PlayerControl : MonoBehaviour
{
    public enum PlayerState
    {
        none = 0,
        idle,
        move,
        jump,
    }

    public FSM<PlayerControl> mPlayerFSM;  
    public PlayerState mState;
    public Animator mAnimator;
    public float mSpeed;
    public Vector3 moveDir;

    private void InitFSM()                       
    {
        mPlayerFSM.AddState((int) PlayerState.idle, new ActIdle((int) PlayerState.idle, this));
        mPlayerFSM.AddState((int) PlayerState.move, new ActMove((int) PlayerState.move, this));
        mPlayerFSM.AddState((int) PlayerState.jump, new ActAttack((int) PlayerState.jump, this));
        mPlayerFSM.FSMInit((int)PlayerState.idle);
    }

    void Start()
    {
        mAnimator = GetComponentInChildren<Animator>();
        mSpeed = 10;
        mPlayerFSM = new FSM<PlayerControl>();
        InitFSM();
        mState = PlayerState.idle;
    }   

    void Update()
    {
        //單純為了在inspector皮膚中看到當前狀態
        mState = (PlayerState)mPlayerFSM.GetCurState();
        mPlayerFSM.FSMRun();
    }
}

在不同的行為類中,監聽輸入按鍵通過owner呼叫fsm的switch方法,切換狀態;

舉例移動行為類,監聽兩個軸的輸入,切換idle,同時監聽攻擊按鍵切換攻擊狀態;

public class ActMove : StateBase<PlayerControl>
{
    public ActMove(int id, PlayerControl t) : base(id, t)
    {
    }

    //給子類提供方法
    public override void OnEnter(params object[] args)
    {
        owner.mAnimator.Play("Run");
    }

    public override void OnStay(params object[] args)
    {
        owner.transform.position += owner.moveDir * Time.deltaTime * owner.mSpeed;
        
        if (Input.GetAxis("Horizontal") > 0 && Input.GetAxis("Vertical") == 0)
        {
            owner.moveDir = owner.transform.right;
        }
        else if (Input.GetAxis("Horizontal") < 0 && Input.GetAxis("Vertical") == 0)
        {
            owner.moveDir = -owner.transform.right;
        }
        else if (Input.GetAxis("Horizontal") > 0 && Input.GetAxis("Vertical") < 0)
        {
            owner.moveDir = owner.transform.right - owner.transform.forward;
        }
        else if (Input.GetAxis("Horizontal") < 0 && Input.GetAxis("Vertical") < 0)
        {
            owner.moveDir = -owner.transform.right - owner.transform.forward;
        }
        else if (Input.GetAxis("Horizontal") > 0 && Input.GetAxis("Vertical") > 0)
        {
            owner.moveDir = owner.transform.right + owner.transform.forward;
        }
        else if (Input.GetAxis("Horizontal") < 0 && Input.GetAxis("Vertical") > 0)
        {
            owner.moveDir = -owner.transform.right + owner.transform.forward;
        }
        else if (Input.GetAxis("Horizontal") == 0 && Input.GetAxis("Vertical") < 0)
        {
            owner.moveDir = -owner.transform.forward;
        }
        else if (Input.GetAxis("Horizontal") == 0 && Input.GetAxis("Vertical") > 0)
        {
            owner.moveDir = owner.transform.forward;
        }
        
        if (Mathf.Abs(Input.GetAxis("Horizontal")) < 0.1f && Mathf.Abs(Input.GetAxis("Vertical")) < 0.1f)
            owner.mPlayerFSM.SwitchState((int)PlayerControl.PlayerState.idle);
        
        if (Input.GetAxis("Jump") != 0)
            owner.mPlayerFSM.SwitchState((int)PlayerControl.PlayerState.jump);
    }

    public override void OnExit(params object[] args)
    {
            
    }
}

自從出了行為樹之後,有限狀態機就沒太大的用武之地了,後面有機會介紹官方的BehaviourTree外掛吧;

相關文章