使用面向對象的思想寫有限狀態機
好久好久沒更新了,現在俺又回來了嘻嘻。
——————————————————————分割線————————————————————————————
今天要講的呢,是通過使用面向對象的思想,寫一個簡單的有限狀態機。說起面向對象,相信大多數人都不陌生,雖然在一開始學這種思想的時候會有點難,而且因爲抽象思維很重,所以一開始也不太敢去真的使用這種思想,但是我覺得,其實只要慢慢多去寫代碼,多去理解使用類,接口等的意義,後面學起來還是可以很容易上手的,而且會有一種,萬物皆可面向對象的感覺。
至於什麼是對象這種有點哲學的問題我就不回答了,但是有一點,就是時刻要問自己什麼是“類”,什麼是“接口”。我記得我曾經在一次實習的時候,他們的技術總監問了我一個問題——什麼是類??然後我回答了一個——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鍵時,便看到狀態之間的切換,說明狀態類和狀態管理器已經可以運行,然後下一節我們來具體完善。