1. 程式人生 > >Unity--FSM有限狀態機

Unity--FSM有限狀態機

有限狀態機,先理解是幹什麼的,有限表示這個是有限度的不是無限的,狀態,指的是所擁有的所有狀態,這麼來理解,人有情緒,比如說生氣,無感,喜悅,難過,生氣,幸福等,那麼這些情緒是固有的幾種,是所謂有限,那麼那些情緒就是不同的狀態,人可以在這些狀態之中進行轉換,此時是開心的,下一秒有可能就是生氣的,這就是有限狀態機的原理。

這一篇程式碼稍微多了一點,咱們一點一點來說:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum Transition
{
    NullTransition = 0, 
    NoSeePlayer,
    SeePlayer,
    GamePause
}

public enum StateID
{
    NullStateID = 0, 
    IdleState,
    PatrolState,
    ChaseState
}

public abstract class FSMState
{
    protected FSMSystem fsm;

    protected Dictionary<Transition, StateID> map = new Dictionary<Transition, StateID>();
    protected StateID stateID;
    public StateID ID { get { return stateID; } }
    
    public FSMState(FSMSystem fsm)
    {
        this.fsm = fsm;
    }

    public void AddTransition(Transition trans, StateID id)
    {
        if (trans == Transition.NullTransition)
        {
            Debug.LogError("FSMState ERROR: NullTransition is not allowed for a real transition");
            return;
        }

        if (id == StateID.NullStateID)
        {
            Debug.LogError("FSMState ERROR: NullStateID is not allowed for a real ID");
            return;
        }
        if (map.ContainsKey(trans))
        {
            Debug.LogError("FSMState ERROR: State " + stateID.ToString() + " already has transition " + trans.ToString() +
                           "Impossible to assign to another state");
            return;
        }

        map.Add(trans, id);
    }
    
    public void DeleteTransition(Transition trans)
    {
        if (trans == Transition.NullTransition)
        {
            Debug.LogError("FSMState ERROR: NullTransition is not allowed");
            return;
        }
        
        if (map.ContainsKey(trans))
        {
            map.Remove(trans);
            return;
        }
        Debug.LogError("FSMState ERROR: Transition " + trans.ToString() + " passed to " + stateID.ToString() +
                       " was not on the state's transition list");
    }
    
    public StateID GetOutputState(Transition trans)
    {
        if (map.ContainsKey(trans))
        {
            return map[trans];
        }
        return StateID.NullStateID;
    }
    
    public virtual void DoBeforeEntering() { }
    
    public virtual void DoBeforeLeaving() { }

    /// <summary>
    /// 這個方法決定在當前狀態的執行
    /// </summary>
    /// <param name="player"></param>
    /// <param name="npc"></param>
    public abstract void Act(GameObject player, GameObject npc);
    /// <summary>
    /// 這個方法決定狀態是否應該轉換到列表中的另一個狀態
    /// </summary>
    /// <param name="player"></param>
    /// <param name="npc"></param>
    public abstract void Reason(GameObject player, GameObject npc);

} 
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FSMSystem
{
    private List<FSMState> states;
    
    private StateID currentStateID;
    public StateID CurrentStateID { get { return currentStateID; } }
    private FSMState currentState;
    public FSMState CurrentState { get { return currentState; } }
    
    public FSMSystem()
    {
        states = new List<FSMState>();
    }
    
    public void Update(GameObject player,GameObject npc)
    {
        currentState.Act(player, npc);
        currentState.Reason(player, npc);
    }

    public void AddState(FSMState s)
    {
        if (s == null)
        {
            Debug.LogError("FSM ERROR: Null reference is not allowed");
        }
        
        if (states.Count == 0)
        {
            states.Add(s);
            currentState = s;
            currentStateID = s.ID;
            return;
        }
        foreach (FSMState state in states)
        {
            if (state.ID == s.ID)
            {
                Debug.LogError("FSM ERROR: Impossible to add state " + s.ID.ToString() +
                               " because state has already been added");
                return;
            }
        }
        states.Add(s);
    }

    public void DeleteState(StateID id)
    {
        if (id == StateID.NullStateID)
        {
            Debug.LogError("FSM ERROR: NullStateID is not allowed for a real state");
            return;
        }
        foreach (FSMState state in states)
        {
            if (state.ID == id)
            {
                states.Remove(state);
                return;
            }
        }
        Debug.LogError("FSM ERROR: Impossible to delete state " + id.ToString() +
                       ". It was not on the list of states");
    }
    
    
    public void PerformTransition(Transition trans)
    {
        if (trans == Transition.NullTransition)
        {
            Debug.LogError("FSM ERROR: NullTransition is not allowed for a real transition");
            return;
        }

        StateID id = currentState.GetOutputState(trans);
        if (id == StateID.NullStateID)
        {
            Debug.LogError("FSM ERROR: State " + currentStateID.ToString() + " does not have a target state " +
                           " for transition " + trans.ToString());
            return;
        }
        
        currentStateID = id;
        foreach (FSMState state in states)
        {
            if (state.ID == currentStateID)
            {
                currentState.DoBeforeLeaving();

                currentState = state;
                
                currentState.DoBeforeEntering();
                break;
            }
        }

    } 

}

有限狀態機最核心的兩個指令碼是FSMState和FSMSystem,其中FSMState是狀態基類,FSMSystem是狀態機的管理類,這兩個指令碼可以在Wiki.unity3d.com裡搜尋State找到,一個17k的檔案。

在FSMState類中,有兩個列舉,第一個列舉Transition存放所有狀態轉換的條件,第二個人列舉StateID存放的是所有的狀態,當我們增加和刪除狀態的時候直接在這兩個列舉中新增和刪除就可以了。FSMState中有一個鍵值對map,這個鍵值對存放的是狀態的轉換條件和目標狀態,是用於在後面我們轉換狀態的時候進行使用,判斷是否存在這樣的轉換條件和狀態。FSMState類中有七個函式,第一個AddTransition是進行狀態轉換的新增,第二個DeleteTransition是進行狀態轉換的刪除,第三個GetOutputState是獲取轉換條件相應的狀態,第四個DoBeforeEntering是當前狀態開始時的操作,第五個DoBeforeLeaving是當前狀態離開時的操作,第六個Act是當前狀態執行時的操作,最後一個Reason就是轉換狀態的操作了。在這些的基礎上我添加了一個建構函式,這個建構函式有兩個作用,一是方便我們後續對狀態的標記,二是能快速的獲取到FSMSystem的物件,方便操作。

FSMSystem類中的函式有三個,第一個AddState是註冊我們的狀態,第二個DeleteState是刪除狀態,第三個PerformTransition就是通過轉換條件進行狀態之間的切換了,同樣,在這個的基礎上,我添加了一個函式Update,這個函式的作用是把狀態的Act函式和Reason函式進行執行,因為每一個狀態都要一直執行這兩個函式,所以直接封裝起來,方便使用。

以上是FSMState和FSMSystem兩個核心類的一個說明,下面說一下有限狀態機的具體使用:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ChaseState : FSMState {
    public ChaseState(FSMSystem fsm) : base(fsm)
    {
        stateID = StateID.ChaseState;
    }

    public override void Act(GameObject player, GameObject npc)
    {

        npc.transform.LookAt(player.transform);
        npc.transform.Translate(Vector3.forward * 2 * Time.deltaTime);
    }

    public override void Reason(GameObject player, GameObject npc)
    {
       
        if (Vector3.Distance(player.transform.position, npc.transform.position)>= 3f)
        {
            Debug.Log(Vector3.Distance(player.transform.position, npc.transform.position));
            fsm.PerformTransition(Transition.GamePause);
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class IdleState : FSMState
{

    public IdleState(FSMSystem fsm) : base(fsm)
    {
        stateID = StateID.IdleState;
    }

    public override void Act(GameObject player, GameObject npc)
    {
    
    }

    public override void Reason(GameObject player, GameObject npc)
    {
        if (Time.time>2)
        {
            fsm.PerformTransition(Transition.NoSeePlayer);
        }
    }

    public override void DoBeforeLeaving()
    {

       
    }

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PatrolState : FSMState {

    GameObject[] path;
    int index = 0;

    public PatrolState(FSMSystem fsm) : base(fsm)
    {
        stateID = StateID.PatrolState;
        path = GameObject.FindGameObjectsWithTag("Path");
       
    }
    public override void Act(GameObject player, GameObject npc)
    {

       
        if (Vector3.Distance(npc.transform.position,path[index].transform.position) < 1)
        {
            index++;
        }
        if (index > path.Length - 1)
        {
            index = 0;
        }
        npc.transform.LookAt(path[index].transform);
        npc.transform.Translate(Vector3.forward * 3 * Time.deltaTime);
    }

    public override void Reason(GameObject player, GameObject npc)
    {
        if (Vector3.Distance(player.transform.position,npc.transform.position)<3f)
        {
            fsm.PerformTransition(Transition.SeePlayer);
        }
    }
    
}

首先先說ChaseState ,IdleState,PatrolState這三個類,這三個類就是具體的狀態類了,它們繼承了FSMState,在各自的建構函式中標記了各自表示的狀態,並在FSMState的StateID列舉中進行了註冊,然後根據自己不同的操作,實現出Act函式和Reason函式。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy : MonoBehaviour {

    FSMState state; //狀態
    FSMSystem fsm;  //有限狀態機的管理類

    private void Start()
    {
        fsm = new FSMSystem();

        state = new IdleState(fsm);
        state.AddTransition(Transition.NoSeePlayer, StateID.PatrolState);
        fsm.AddState(state); //新增狀態

        state = new PatrolState(fsm);
        state.AddTransition(Transition.SeePlayer, StateID.ChaseState);
        fsm.AddState(state);

        state = new ChaseState(fsm);
        state.AddTransition(Transition.GamePause, StateID.IdleState);
        fsm.AddState(state);
    }
    private void Update()
    {
        print("當前的狀態是:" + fsm.CurrentState);
        fsm.Update(GameController.player, this.gameObject);
    }



}

狀態寫好以後,我們就可以開始使用了,建立一個Enemy類進行使用,在Enemy類中,我們要獲取到狀態基類FSMState和FSMSystem類的物件,以備使用,在Start函式中賦值,同時取得每一種狀態的物件通過AddTransition函式把當前狀態可以轉換到的狀態和轉換條件註冊到FSMState的鍵值對map中,再通過FSMSystem的AddState函式註冊了當前狀態,這樣狀態就全部準備就緒了。

這裡有個點說一下,為什麼在鍵值對中註冊還要在AddState函式中註冊狀態呢?因為當轉換狀態時,首先會呼叫FSMSystem中的Update函式之後會執行Reason函式,在Reason函式中,我們會呼叫FSMSystem中的PerformTransition函式進行轉換操作,在PerformTransition中,首先要判斷當前的轉換條件是否不為null,然後通過GetOutputState函式得到轉換條件對應的狀態,這裡就需要用到FSMState中的鍵值對進行判斷了,如果不註冊轉換條件和目標狀態,在這一步就不會得到相應的狀態,拿到目標狀態之後,要判斷是否存在這個狀態,就會遍歷FSMSystem中的狀態列表,存在的話就開始具體的狀態切換了要把當前狀態變換為目標狀態,在此之前要執行當前狀態的DoBeforeLeaving函式,切換了狀態之後執行DeBeforeEntering函式,這樣就完成了整體的一個狀態切換了。

最後只需要在Enemy類的Update函式中執行FSMSystem類的Update函式就好了。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameController : MonoBehaviour {
    
    public static GameObject player;
  
    private GameController() { }
    private static GameController gameController;
    public static GameController GetGameController { get { return gameController; } }

    private void Awake()
    {
        gameController = this;
        player = GameObject.Find("Player");
    }
    
}

到此,FSM有限狀態機的一個操作和理解也就結束了,Over!