使用面向對象的思想寫有限狀態機

使用面向對象的思想寫有限狀態機

好久好久沒更新了,現在俺又回來了嘻嘻。
——————————————————————分割線————————————————————————————
今天要講的呢,是通過使用面向對象的思想,寫一個簡單的有限狀態機。說起面向對象,相信大多數人都不陌生,雖然在一開始學這種思想的時候會有點難,而且因爲抽象思維很重,所以一開始也不太敢去真的使用這種思想,但是我覺得,其實只要慢慢多去寫代碼,多去理解使用類,接口等的意義,後面學起來還是可以很容易上手的,而且會有一種,萬物皆可面向對象的感覺。
至於什麼是對象這種有點哲學的問題我就不回答了,但是有一點,就是時刻要問自己什麼是“類”,什麼是“接口”。我記得我曾經在一次實習的時候,他們的技術總監問了我一個問題——什麼是類??然後我回答了一個——Class唄。反正現在想起來怪怪的,因爲之前對面向對象這種思想不是很通透,學的不夠深入,能有這種回答也是正常。但是在C#等一些編程語言中,類的確就是class。
在這裏的話,我覺得可以這樣理解,類就是一些具有相同性質的事務的總稱。比如在我們開發遊戲的時候,爲角色寫攻擊的代碼,角色可能有不同的技能或者說攻擊方式,但是這些技能和攻擊方式都是會造成傷害或者是會消耗魔法值,這就是他們的一些共性,然後我們可能就會寫攻擊類,或者是技能類,要用的時候只要調用或者繼承。
好了,廢話不多說,我們開始進行我們的有限狀態機的開發吧。這裏的有限狀態機我們可以引用unity維基百科的一個案例來進行操作和學習。原地址:unity有限狀態機
首先打開unity,新建兩個腳本,一個取名爲FSMState,一個取名爲FSMSystem,然後打開兩個腳本。
在這裏插入圖片描述
好,建好了兩個腳本,先問一個問題:什麼是有限狀態機???
————————————————————10秒鐘冥想————————————————————————————
冥想結束。有限狀態機,顧名思義,就是擁有有限個狀態的機器,FSM的全稱是Finite State Machine。那麼根據有限狀態機的意思,我們便可以推測出,狀態個數永遠會大於等於1個,且小於n。而且所有狀態都是某個事物身上發生的,狀態之間都是有類似的共性。於此,我們便有FSMstate這個類文件,它主要是所有狀態的一個抽象化,並不是具體的狀態,**但它有狀態需要的方法或者條件。**而FSMSystem則是管理所有狀態的一個管理器,也就是狀態管理器系統。理解了這兩個的意義,對寫代碼會有很大的幫助。
————————————————————分割線————————————————————————————————
現在呢,我們通過爲一個NPC怪物寫巡邏狀態機讓它動起來。在unity商店下載一個怪物的資源包:Dragon the Terror Bringer and Dragon Boar,然後導入,選擇到一個做好的龍的預製體,把動畫組件去掉,改名爲NpcDragon,然後調整攝像機位置,這個龍就是我們要爲它寫狀態的對象。
在這裏插入圖片描述
場景佈置好了,我們再回到它的一個動畫目錄裏,看一下它有哪些動畫
在這裏插入圖片描述
好像動畫還挺多的,不過沒事,我們先畫一個簡單的圖:
在這裏插入圖片描述
根據上面的圖,其實我們發現,主要的三種狀態無非是攻擊,移動,靜止,然後再添加其他的一些別的狀態。但是這些狀態之間的轉換是有條件的,比如攻擊狀態,必須要在攻擊範圍之內且主角是活着的才能攻擊。根據這樣的一種構思,我們開始寫FSMState的腳本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 狀態類
/// </summary>
public enum Transition//狀態所對應的一個轉換條件,也可以理解爲判斷是否可以進入該狀態的一個條件
{
NullTransition=0,
LostPlayer,//失去玩家目標,繼續巡邏
Rest,//進行休息,對應靜止待命
Getplayer,//發現目標,進入攻擊狀態
}
public enum StateID//四個主要狀態的ID,我在這裏進行了簡化處理
{
NullStateID=0,
Patrol,//巡邏
Idle,//靜止狀態
Attack,//處於攻擊狀態,即發現了目標準備進行攻擊

}
public abstract class FSMState//抽象類
{
    protected FSMSystem fsm;//定義一個狀態管理器
    //定義一個字典,用來保存狀態對應的條件和狀態本身
    protected Dictionary<Transition, StateID> map = new Dictionary<Transition, StateID>();
    //封裝狀態ID
    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("該狀態條件爲空");
            return;
        }
        if (id == StateID.NullStateID)
        {
            Debug.LogError("該狀態爲空");
            return;
        }
        if (map.ContainsKey(trans))
        {
            Debug.LogError("已經實例化一次該狀態,不能再次添加");
            return;
        }

        map.Add(trans, id);//如果該狀態完整且擁有對應的條件,則進行添加
        if (fsm != null)//這一部可以精簡操作,比官方的更方便一點
        {
            //Debug.Log(this);
            fsm.AddState(this);
        }
    }
    public void DeleteTransition(Transition trans)//刪除狀態
    {
        if (trans == Transition.NullTransition)
        {
            Debug.LogError("該狀態爲空");
            return;
        }
        if (map.ContainsKey(trans))
        {
            map.Remove(trans);
            return;
        }
    }
    public StateID GetOutputState(Transition trans)//根據狀態所對應的條件,來獲取對應的狀態ID
    {
        foreach (FSMState state in fsm.states)//遍歷管理器的所有狀態
        {
            if (state.map.ContainsKey(trans))//判斷狀態匯中是否含有該字典的鍵值
            {
                return state.map[trans];
            }

        }
        return StateID.NullStateID;
    }
    public virtual void DoBeforeEntering() { }//進入下一狀態時進行的操作
    public virtual void DoBeforeLeaving() { }//離開當前狀態時可以進行的操作

    public abstract void Act(GameObject player, GameObject npc);//具體的狀態實際操作
    public abstract void Reason(GameObject player, GameObject npc);//根據條件改變,更改狀態
}

然後是FSMSystem的腳本:

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

public class FSMSystem 
{
    public List<FSMState> states;//保存所要管理的狀態的鏈表

    private FSMState currentState;
    public FSMState CurrentState { get { return currentState; } }//當前運行的狀態

    private StateID currentStateID;
    public StateID CurrentStateID { get { return currentStateID; } }//當前狀態的ID
    public FSMSystem()//實例化
    {
        states = new List<FSMState>();
    }
    public void AddState(FSMState s)//進行添加狀態的函數
    {
        if (s == null)
        {
            Debug.LogError("狀態爲空");
        }
        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("已存在該狀態的類");
                return;
            }
        }
        states.Add(s);
    }
    public void DeleteState(StateID id)
    {
        if (id == StateID.NullStateID)
        {
            Debug.LogError("不存在空狀態,無法刪除");
            return;

        }
        foreach (FSMState state in states)
        {
            if (state.ID == id)
            {
                states.Remove(state);
                return;
            }
        }
    }//刪除狀態
    public void PerformTransition(Transition trans)
    {
        if (trans == Transition.NullTransition)
        {
            Debug.LogError("該狀態條件爲空");
            return;
        }
        StateID id = currentState.GetOutputState(trans);//獲得條件對應的狀態ID
        if (id == StateID.NullStateID)
        {
            Debug.LogError("不存在該狀態");
            return;
        }
        currentStateID = id;
        foreach (FSMState state in states)
        {
            if (state.ID == currentStateID)
            {
                currentState.DoBeforeLeaving();
                currentState = state;
                currentState.DoBeforeEntering();
                break;
            }
        }

    }//根據狀態條件更改狀態
    public void Update(GameObject player, GameObject npc)//具體的執行
    {
        currentState.Act(player, npc);
        currentState.Reason(player, npc);
    }

}
	

然後新建四個腳本,三個爲狀態腳本,一個是要掛到龍身上的腳本。
在這裏插入圖片描述
然後分別打開這些腳本,我們先進行簡單的測試,看一下狀態機能不能驅動,腳本如下:
Attack腳本:

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

public class Attack : FSMState//繼承抽象類
{
    public Attack(FSMSystem fsm) : base(fsm)
    {
        stateID = StateID.Attack;//初始化該狀態的ID
    }
    public override void Act(GameObject player, GameObject npc)//操作行爲
    {
        Debug.Log("攻擊中");
    }

    public override void Reason(GameObject player, GameObject npc)//改變狀態的方法
    {
        if (Input.GetKeyDown(KeyCode.Q))//當按下Q鍵時,改變狀態爲巡邏
        {
            fsm.PerformTransition(Transition.LostPlayer);//根據改變狀態的條件,設置當前狀態
        }
        if (Input.GetKeyDown(KeyCode.W))
        {
            fsm.PerformTransition(Transition.Rest);
        }
      
    }

    
}

Idle腳本:

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

public class Idle : FSMState
{
    public Idle(FSMSystem fsm) : base(fsm)
    {
        stateID = StateID.Idle;
    }
    public override void Act(GameObject player, GameObject npc)
    {
        Debug.Log("靜止中");
    }

    public override void Reason(GameObject player, GameObject npc)
    {
     
        if (Input.GetKeyDown(KeyCode.E))
        {
            fsm.PerformTransition(Transition.Getplayer);
        }
        if (Input.GetKeyDown(KeyCode.Q))
        {
            fsm.PerformTransition(Transition.LostPlayer);
        }
    }

   
}

Patrol腳本:

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

public class Patrol : FSMState
{
    public Patrol(FSMSystem fsm) : base(fsm)
    {
        stateID = StateID.Patrol;
    }
    public override void Act(GameObject player, GameObject npc)
    {
        Debug.Log("巡邏中");
    }

    public override void Reason(GameObject player, GameObject npc)
    {
        if (Input.GetKeyDown(KeyCode.W))
        {
            fsm.PerformTransition(Transition.Rest);
        }
        if (Input.GetKeyDown(KeyCode.E))
        {
            fsm.PerformTransition(Transition.Getplayer);
        }
    }

  
}

然後打開Dragon腳本:

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

public class Dragon : MonoBehaviour
{
    FSMSystem fsm;
    void Start()
    {
        fsm = new FSMSystem();
        Attack a = new Attack(fsm);
        a.AddTransition(Transition.Getplayer, StateID.Attack);
        Idle i = new Idle(fsm);
        i.AddTransition(Transition.Rest, StateID.Idle);
        Patrol p = new Patrol(fsm);
        p.AddTransition(Transition.LostPlayer, StateID.Patrol);

    }

    // Update is called once per frame
    void Update()
    {
        fsm.Update(this.gameObject,this.gameObject);
    }
}

上面的代碼是用來測試的,不是最終的狀態機,具體的實現功能還沒有實現,不過此時我們已經可以運行遊戲試一下:
當我們按下QWE鍵時,便看到狀態之間的切換,說明狀態類和狀態管理器已經可以運行,然後下一節我們來具體完善。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章