有限狀態機機制介紹及在Android中應用的實例

上來可能有疑問
什麼是狀態機?能幹什麼?

第一次聽說狀態機 是從遊戲開發技術中得來的。在遊戲開發中,可以使用狀態機控制npc的行爲,控制角色在 站立,追蹤敵人,攻擊敵人,逃跑等狀態進行切換。每一個狀態都會執行該狀態進行時執行的一系列操作。

簡而言之,就是控制對象狀態的切換。

用圖示來表示一下

這是一個遊戲demo,注意敵人的各種行爲

在這裏插入圖片描述
敵人沒有發現玩家時,呆立不動,這時,敵人可以說是處於 “空閒” 的狀態

在這裏插入圖片描述
當敵人發現玩家,會向玩家靠近,這時,可以說敵人處於 “追擊” 狀態

在這裏插入圖片描述
當敵人追上玩家,就會對玩家進行攻擊,這時,可以說敵人處於 “攻擊” 狀態。

在這裏插入圖片描述
當然 玩家不是喫素的,當玩家反擊並擊敗敵人,這時 敵人說敵人處於 “死亡” 狀態。

在這裏插入圖片描述
當然 ,玩家可以不選擇擊敗敵人,也可以戰鬥中逃離,當玩家逃出一定視野範圍,npc無法看到玩家,則npc 又可以回到 “空閒” 狀態。

注意到,NPC在某個狀態中,還需要進行各種行爲,例如 “空閒”狀態時,會不停的 東張西望 。 “追擊” 狀態時,會朝向敵人奔跑。“攻擊”狀態時,會向玩家揮動武器。 “死亡”狀態時 會倒地不起。

以上的這些行爲是文字描述的,是遊戲策劃設計的。

作爲程序員,是有職責將上述的設計轉換成代碼實現的。

那麼,代碼將如何實現這些設計呢?可能有人會想,那麼我就設計一個控制類,裏面有許多控制變量,通過判斷控制變量來進行控制,例如下面僞代碼

if(distance(player,npc)<SEE_DISTANCD){
	npc.move(player)
}
if(distance(player,npc)<ATTACK_DISTANCD){
	mpc.attack(player)
}
if(npc.health<=0){
	mpc.death()
}
if(distance(player,npc)>=SEE_DISTANCD)){
	npc.idle();
}
...

通過一定數量的if-else來控制這些行爲。然鵝,實際情況下這種做法是不可行的,因爲實際的狀態轉換條件有很多種,必須進行if-else嵌套才能完成所有的邏輯,如果寫全這些邏輯的話,代碼是不可維護的(不必懷疑,自己試一下便知)。

這次的主角要登場了 這就是有限狀態機(finite state machine)簡稱爲FSM。

在介紹狀態機之前,我們要用一個圖示來表示上述遊戲中的NPC的各種狀態之間的關係。
在這裏插入圖片描述
圖中,藍色框代表有限的所有的狀態,綠色有向箭頭代表狀態間可以進行切換的條件 ,整個圖示叫做狀態圖

這個圖非常重要。在編寫狀態機代碼前,必須先畫出這個狀態圖。

狀態機 可以理解爲一臺不停工作的機器,運行一段循環執行的代碼,來控制狀態間的切換和處於每個狀態期間需要執行的操作

一個狀態機的接口是這樣的

interface IFSM{
	update();
	addState(State state);
	changeState(int stateID);
	setupFSM();
}

其中 update()是系統提供的更新機制(例如Unity中的 update()方法,Android 中的 Handle.postDelay())
每當達到更新時機時,通知狀態機更新當前狀態,也即是調用狀態機的update()。
更新當前狀態包括 檢查當前狀態是否滿足條件,查詢另一個滿足條件的狀態,切換當前狀態到應該到達的狀態,每當update()時,就完成了狀態圖的一個藍框到另一個藍框的過程,當然狀態沒有變化則繼續處於當前的藍框狀態。

addState()提供了一個交由狀態機管理的所有狀態的添加功能,狀態機本質上是管理了所有的狀態,並把某一個設置爲當前狀態

再回到代碼層面

實現狀態機,還有兩個類必不可少
狀態類
代表狀態圖中的藍框,職責是處於某種狀態時,執行狀態中的操作,比如 攻擊玩家 或者 尋找玩家位置,向玩家前進。
一個狀態類的接口是這樣的

interface IState{
	void check(IFSM fsm);
	void onEnter(IFSM fsm);
	void onAction(IFSM fsm);
	void onExit(IFSM fsm);
}

其中,
onEnter()提供了一個剛進入狀態中執行的一次性操作 比如 開始循環播放空閒動畫。
onAction()提供了一個處於狀態中需要持續進行的操作 比如 攻擊玩家。
onExit()提供了一個狀態轉換時本狀態退出時需要進行的操作 比如 停止攻擊。
check()提供了當當前狀態已經不滿足條件時,通過滿足條件的條件編號找到本狀態可以切換的下一個條件,來交給狀態機設置爲當前狀態
函數參數中都有一個狀態機的引用,是方便再各種操作時,能方便訪問到狀態機管理的外部引用(例如player 玩家,npc 等)

以上是狀態類,接下來是最後一個關鍵類
條件類條件類代表了狀態圖中的的綠色箭頭,職責是判斷對應的狀態是否滿足,比如追擊這個條件是否滿足,如果滿足則返回true。

條件類的接口如下所示

interface ITrigger{
	boolean checkTrigger(IFSM fsm);
}

我們把條件類命名爲Trigger 因爲條件如果滿足的話,說明會觸發一次條件的轉換。所以相當於條件轉換是由條件類觸發的,所以叫Trigger.

checkTrigger()提供了一個判斷某個狀態對應的條件是否滿足的依據,例如攻擊狀態對應的條件攻擊條件中,checkTrigger()應該如下所示

AttackTrigger implements ITrigger{
	@Override
	boolean checkTrigger(IFSM fsm){
		return distance(fsm.getPlayer(),fsm.getNpc())<=ATTACK_DISTANCE;
	}
}

這裏,大家看到了最初龐大的的if-else語句中的某些代碼了。這裏體現了狀態機機制相比大塊if-else邏輯的一個優點,就是單一職責原則,每個條件通過一個類來負責,各條件之負責本條件對應的判斷。
狀態類的方法中也有一個狀態機參數,同樣是爲了方便訪問到公用的數據(這裏是遊戲對象player及npc)。

讓我們把 狀態機類,條件類,狀態類整合起來,讓整個系統運行起來吧。

到這裏,相信大部分人還是不懂狀態機是怎麼運作的,能控制NPC做出上面的反應。這就需要將上述的重要的類整合起來了。

總體來說,上述的類放在一起是這樣的
在這裏插入圖片描述
各類之間是包含的關係。

首先 狀態機有一個狀態列表,保存了管理的所有的有限的狀態。

狀態機還負責切換狀態,切換狀態的意思是 把某一個新狀態設爲當前狀態。

每一個狀態類都管理了一個條件類列表,代表該狀態能夠到達的其他狀態應該滿足的條件
還包括了一個條件-狀態映射 一般使用Map來表示。映射是當某一個條件滿足時,從該映射中取出對應的狀態,作爲準備切換到的狀態,交由狀態機切換。

接下來來實現每個類
首先是狀態機

public class BasicFSM implements IFSM {

    List<IState> states = new ArrayList<>();

    IState currentState;

    public IState getCurrentState() {
        return currentState;
    }

    public void setCurrentState(IState currentState) {
        this.currentState = currentState;
    }
    @Override
    public void changeState(int stateId) {
        for (IState state:states) {
            if(state.getId()==stateId){
                if(currentState!=null){
                    currentState.onExit(this);
                }
                currentState = state;
                currentState.onEnter(this);
                break;
            }
        }
    }

    @Override
    public void addState(IState state) {
        states.add(state);
    }

    @Override
    public void update() {
    	currentState.check(this);
        currentState.onAction(this);
    }
    @Override
    public void setupFSM() {

    }
}

狀態機的實現,包含了兩個成員變量,一個是currentState 標記當前處於的狀態,另一個是所有狀態列表。當切換狀態時,從列表中找到對應的狀態設置爲當前狀態。

接下來是條件類(具體的條件類,需要繼承該基類,來做條件判斷)

public class BasicTrigger implements ITrigger {

    int id;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public BasicTrigger(int id) {
        this.id = id;
    }

    @Override
    public boolean checkTrigger(IFSM fsm) {
        return false;
    }
}

條件類有一個id成員變量,來標記條件的類型,條件構造的時候需要指定。

然後是狀態類基類的實現(具體的狀態類,需要繼承該基類,來做狀態處理邏輯)

public class BasicState implements IState {

    Map<Integer,Integer> stateTriggerMap = new HashMap<>();
    List<ITrigger> triggers = new ArrayList<>();
    int id;

    public int getId() {
        return id;
    }

    public BasicState(int id) {
        this.id = id;
    }

    @Override
    public void onAction(IFSM fsm) {

    }

    @Override
    public void onEnter(IFSM fsm) {

    }

    @Override
    public void onExit(IFSM fsm) {

    }

    @Override
    public void addTrigger(ITrigger trigger) {
        triggers.add(trigger);
    }

    @Override
    public void addMap(int triggerId, int stateId) {
        stateTriggerMap.put(triggerId,stateId);
    }

    @Override
    public void check(IFSM fsm) {
        for (ITrigger trigger:triggers) {
            if(trigger.checkTrigger(fsm)){
                int nextStateId = stateTriggerMap.get(trigger.getId());
                fsm.changeState(nextStateId);
                break;
            }
        }
    }
}

狀態類的成員變量triggers 保存了當前條件可以切換到的所有其他狀態對應的條件,在狀態機每次判斷時會調用check()當前是否有其他條件滿足,需要切換到其他狀態。每次從條件列表中對每個條件進行判斷,如果某個條件判斷結果爲true,則根據滿足的條件id從保存的條件–狀態映射stateTriggerMap中找到對應的狀態的狀態id,告訴狀態機需要切換到新的狀態了。

這時,狀態機主題框架都有了,應用到實際因業務邏輯,還需要一些輔助。

首先是標明條件和狀態類型的id值。這裏其實可以使用枚舉作爲ID,但用Integer作爲id 有一些好處,比如說多個狀態機的情況下可以繼承同一個基類。當然這裏沒那麼複雜。

我們創建一個類保存狀態id常量值

public class StateID {
    public static int STATE_IDLE = 0;//空閒
    public static int STATE_PERSUIT = 1;//追逐
    public static int STATE_ATTACK = 2;//攻擊
    public static int STATE_DIE = 3;//死亡
}

還有一個保存條件id常量值

public class TriggerID {
    public static int STATE_IDLE = 0;//空閒
    public static int STATE_PERSUIT = 1;//追逐
    public static int STATE_ATTACK = 2;//攻擊
    public static int STATE_DIE = 3;//死亡
}

有了id值,我們就可以創建具體的條件對象和狀態對象了。以攻擊作爲例子

public class AttackState extends BasicState {

    public AttackState(int id) {
        super(id);
    }

    @Override
    public void onAction(IFSM fsm) {
        NPCControlFSM npcControlFSM = (NPCControlFSM) fsm;
        npcControlFSM.getNpc().attack();
    }
}

這裏,處於攻擊狀態的時候,需要不停的攻擊 所以 我們只複寫了 onAction()方法,來處理處於該狀態中的操作。就是不停的砍啊砍。

接下來實現具體的攻擊條件類,也就是滿足什麼條件時,要進入攻擊狀態,也以它作爲具體條件類來舉個例子

public class AttackTrigger extends BasicTrigger {

    public AttackTrigger(int id) {
        super(id);
    }

    @Override
    public boolean checkTrigger(IFSM fsm) {
        NPCControlFSM npcControlFSM = (NPCControlFSM) fsm;
        return npcControlFSM.
        calculateDistance(npcControlFSM.getPlayer(),
        npcControlFSM.getNpc())>NPCControlFSM.ATTACK_DISTANCE;
    }
}

這裏最重要的是 重寫checkTrigger()方法,具體處理滿足攻擊狀態切換的條件是什麼。這裏引用了狀態機的公用方法做了邏輯判斷。

現在可能發現了,在具體的條件類狀態類中,接受的狀態機引用被轉換成了一另一個類型的狀態機實例。是因爲之前的狀態機也是基礎類,具體業務還要創建繼承自基礎狀態機的狀態機來使用。

進一步說明一下,這個狀態機框架是一個通用的框架,不僅可以用於遊戲中,你可以將它變成C#腳本放到Unity引擎中使用,也可以在Android應用開發中使用,比如在Android中可以用它來操作一些複雜邏輯的動畫切換,或者帶有狀態切換性質的功能,比如視頻播放器的控制。視頻播放器的 暫停,停止,播放狀態也可以使用這個框架來控制。這時,具體的狀態機類就是MediaPlayerControlFSM了。

我們來看下NPCControlFSM的實現

public class NPCControlFSM extends BasicFSM {

    public static final int ATTACK_DISTANCE = 200;

    public Player getPlayer() {
        return player;
    }

    public void setPlayer(Player player) {
        this.player = player;
    }

    public Npc getNpc() {
        return npc;
    }

    public void setNpc(Npc npc) {
        this.npc = npc;
    }

    private Player player;
    private Npc npc;

    public int calculateDistance(Player player,Npc npc){
        int distance = 256;//舉個例子,這裏根據遊戲進行改寫
        return distance;
    }
    @Override
    public void setupFSM() {

    }
}

可以看到,具體功能的狀態機,往往引用了一些業務邏輯相關的成員,可以通過狀態機傳給具體狀態,條件類來進行業務邏輯判斷,或者執行某種狀態相關的操作。

具體狀態有了,狀態機也有了,最後就是給告訴狀態機,需要負責管理哪些狀態,最終才能讓狀態機自動地切換狀態。

我們需要在具體狀態機裏增加一個配置狀態機的方法 setupFSM();
下面是增加了配置的NPCControlFSM 類實現。

public class NPCControlFSM extends BasicFSM {

    public static final int ATTACK_DISTANCE = 200;

    /***
     * 配置狀態機需要增加狀態機管理的所有
     * 狀態類,在生成每個狀態類的同時
     * 配置每個狀態類的條件狀態映射
     */
    public void setupFSM(){
        IdleState idleState = new IdleState(StateID.STATE_IDLE);//創建空閒狀態類
        PursuitTrigger pursuitTrigger = new PursuitTrigger(TriggerID.STATE_PURSUIT);
        //空閒狀態有切換到追逐狀態的路線,需要在在空閒狀態內部添加追逐切換條件
        idleState.addTrigger(pursuitTrigger);
        //空閒狀態有切換到追逐狀態的路線,需要在在空閒狀態內部註冊追逐狀態映射
        idleState.addMap(TriggerID.STATE_PURSUIT,StateID.STATE_PURSUIT);
        //將空閒狀態添加至狀態機管理
        addState(idleState);

        //創建追逐狀態,配置追逐狀態能夠切換到的其他條件
        PursuitState pursuitState = new PursuitState(StateID.STATE_PURSUIT);
        AttackTrigger attackTrigger = new AttackTrigger(TriggerID.STATE_ATTACK);
        pursuitState.addTrigger(attackTrigger);
        pursuitState.addMap(TriggerID.STATE_ATTACK,StateID.STATE_ATTACK);
        IdleTrigger idleTrigger = new IdleTrigger(TriggerID.STATE_DIE);
        pursuitState.addTrigger(idleTrigger);
        pursuitState.addMap(TriggerID.STATE_IDLE,StateID.STATE_DIE);
        addState(pursuitState);

        //創建攻擊狀態,配置攻擊狀態能夠切換到的其他條件
        AttackState attackState = new AttackState(StateID.STATE_ATTACK);
        DieTrigger dieTrigger = new DieTrigger(TriggerID.STATE_DIE);
        attackState.addTrigger(dieTrigger);
        attackState.addMap(TriggerID.STATE_DIE,StateID.STATE_DIE);
        attackState.addTrigger(pursuitTrigger);
        attackState.addMap(TriggerID.STATE_PURSUIT,StateID.STATE_PURSUIT);
        addState(attackState);

        //創死亡擊狀態,配置死亡狀態能夠切換到的其他條件 死亡狀態不能切換到其他狀態所以沒有addTrigger和addMap
        DieState dieState = new DieState(StateID.STATE_DIE);
        addState(dieState);

        //指定默認狀態
        setCurrentState(idleState);
    }

    public Player getPlayer() {
        return player;
    }

    public void setPlayer(Player player) {
        this.player = player;
    }

    public Npc getNpc() {
        return npc;
    }

    public void setNpc(Npc npc) {
        this.npc = npc;
    }

    private Player player;
    private Npc npc;

    public int calculateDistance(Player player,Npc npc){
        int distance = 256;//舉個例子,這裏根據遊戲進行改寫
        return distance;
    }
}

在setupFSM() 設置了該狀態機需要管理的狀態,這裏增加了:空閒狀態,追逐狀態,攻擊狀態,死亡狀態。並且在末尾調用setCurrentState()將空閒狀態當作當前狀態。這樣可以在狀態機配置好時有一個默認的狀態。在添加條件和狀態的過程中,務必對照之前設計的狀態圖來添加,再次強調狀態圖的重要性。在這裏插入圖片描述

到這裏。具有控制npc自動對玩家做出反應的狀態機已經實現,剩下要做的就是在系統中創建該狀態機,並調用setupFSM()配置狀態機,並在框架(遊戲引擎提供的update機制)定時執行的更新方法中調用狀態機的update(),狀態機就能
進行NPC實時的行爲更新了。

說了半天,跟Android如何結合呢?

這就需要利用AndroidSDK自帶的機制去開啓一個定時執行的方法來觸發狀態機框架的update()更新實時狀態了。這裏可以選擇Handler來做這個事情。

public class FSMHelper {
    private  IFSM ifsm;
    private  Handler handler = new Handler();

    public FSMHelper(IFSM ifsm) {
        ifsm.setupFSM();//構造是,讓狀態機配置好
        this.ifsm = ifsm;
    }

    public void start(){//讓狀態機運轉起來,沒0.2秒進行更新
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                ifsm.update();
                handler.postDelayed(this, 200);//控制狀態機0.2秒更新狀態
            }
        };
        handler.post(runnable);
    }

}

上面的FSMHelper可以讓狀態機運轉起來,使用Android的handler機制,可以讓它在指定的時間間隔內更新一次狀態機。這裏每0.2秒更新一次(這個頻率很低了其實,你可以自行調節 可以參考在遊戲裏一般0.01666秒(60FPS)甚至更少更新一次)。

在Android中,使用這個工具類FSMHelper 。可以在MainActivity 的onCreate時,新建一個狀態機實例,並創建一個FSMHelper 實例傳入狀態機實例。並調用start()方法,就可以啓動一個狀態機了。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        NPCControlFSM npcControlFSM = new NPCControlFSM();
        FSMHelper helper = new FSMHelper(npcControlFSM);
        helper.start();
        setContentView(R.layout.activity_main);

    }
}

至此,Android應用系統就有一個定時更新的狀態機系統在運行了,配置具體的狀態,條件類,就可以控制需要實時改變狀態的具體業務了,不管是媒體播放器播放,還是某個頁面動畫的切換。具體實現請自行探索。總之,如果涉及到時間相關的多個控制參數的自動切換狀態,可以考慮使用這個機制來實現。

說個題外話,可能會想,Android中狀態執行的onAction()時肯定要跟某個界面l例如Activity發生聯繫啊,當前的狀態機並麼有什麼與Activity相關連的地方。這裏推薦EventBus來實現。
當某個狀態處於進入,執行,退出時,需要Activity有所響應,可以在狀態的執行方法中post一個事件,Activity接收這個事件,從而做出界面的響應。

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