上來可能有疑問
什麼是狀態機?能幹什麼?
第一次聽說狀態機 是從遊戲開發技術中得來的。在遊戲開發中,可以使用狀態機控制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接收這個事件,從而做出界面的響應。