目錄
注:寫這篇文章的時候,筆者所用的是quick-cocos2d-x 2.2.1rc版本。
quick狀态機
狀态機的設計,目的就是為了避免大量狀态的判斷帶來的複雜性,消除龐大的條件分支語句,因為大量的分支判斷會使得程式難以修改和擴充。但quick狀态機的設計又不同設計模式的狀态模式,TA沒有将各個狀态單獨劃分成單獨的狀态類,相反根據js、lua語言的特點,特别設計了寫法,使用起來也比較友善。
quick架構中的狀态機,是根據javascript-state-machine重新設計改寫而成,同時
sample/statemachine
範例也是根據js版demo改寫而來。該js庫現在是
2.2.0
版本。基于js版的README.md,結合廖大的lua版重構,我針對狀态機的使用做了點說明,如果有不對的地方,感謝指出:)。
推薦大家在了解的時候結合
sample/statemachine
範例進行了解,注意player設定成豎屏模式,demo裡面的按鈕在橫屏模式下看不見。
sample圖示
用法
建立一個狀态機
local fsm = StateMachine.new()
-- (注:和demo不同的是,demo采用元件形式完成的初始化)
fsm:setupState({
initial = "green",
events = {
{name = "warn", from = "green", to = "yellow"},
{name = "panic", from = "green", to = "red" },
{name = "calm", from = "red", to = "yellow"},
{name = "clear", from = "yellow", to = "green" },
}
})
之後我們就可以通過
- fsm:doEvent("start")-從"none"狀态轉換到"green"狀态
- fsm:doEvent("warn")-從"green"狀态轉換到"yellow"狀态
- fsm:doEvent("panic")-從"green"狀态轉換到"red"狀态
- fsm:doEvent("calm")-從"red"狀态轉換到"yellow"狀态
- fsm:doEvent("clear")-從"yellow"狀态轉換到"green"狀态
同時,
- fsm:isReady()-傳回狀态機是否就緒
- fsm:getState()-傳回目前狀态
- fsm:isState(state)-判斷目前狀态是否是參數state狀态
- fsm:canDoEvent(eventName)-目前狀态如果能完成eventName對應的event的狀态轉換,則傳回true
- fsm:cannotDoEvent(eventName)-目前狀态如果不能完成eventName對應的event的狀态轉換,則傳回true
- fsm:isFinishedState()-目前狀态如果是最終狀态,則傳回true
- fsm:doEventForce(name, ...)-強制對目前狀态進行轉換
單一事件的多重from和to狀态
如果一個事件允許我們從多個狀态(from)轉換到同一個狀态(to), 我們可以通過用一個集合來建構from狀态。如下面的"rest"事件。但是,如果一個事件允許我們從多個狀态(from)轉換到對應的不同的狀态(to),那麼我們必須将該事件分開寫,如下面的"eat"事件。
local fsm = StateMachine.new()
fsm:setupState({
initial = "hungry",
events = {
{name = "eat", from = "hungry", to = "satisfied"},
{name = "eat", from = "satisfied", to = "full"},
{name = "eat", from = "full", to = "sick" },
{name = "rest", from = {"hungry", "satisfied", "full", "sick"}, to = "hungry"},
}
})
在設定了事件
events
之後,我們可以通過下面兩個方法來完成狀态轉換。
- fsm:doEvent("eat")
- fsm:doEvent("rest")
rest
事件的目的狀态永遠是
hungry
狀态,而
eat
事件的目的狀态取決于目前所處的狀态。
注意1:如果事件可以從任何目前狀态開始進行轉換,那麼我們可以用一個通配符來替代
*
狀态。如
from
事件,我們可以寫成
rest
{name = "rest", from = "*", to = "hungry"}
。
注意2:上面例子的
事件可以拆分寫成4個,如下:
rest
{name = "rest", from = "hungry", to = "hungry"}, {name = "rest", from = "satisfied", to = "hungry"}, {name = "rest", from = "full", to = "hungry"}, {name = "rest", from = "sick", to = "hungry"}
回調
quick的狀态機支援4種特定事件類型的回調:
-
- 在特定事件EVENT開始前被激活onbeforeEVNET
-
- 在離開舊狀态STATE時被激活onleaveSTATE
-
- 在進入新狀态STATE時被激活onenterSTATE
-
- 在特定事件EVENT結束後被激活onafterEVENT
注解:編碼時候,EVENT/STATE應該被替換為特定的名字
為了便利起見,
-
可以簡寫為onenterSTATE
onSTATE
-
可以簡寫為onafterEVENT
onEVENT
是以假如要使用簡寫的話,為了避免
onSTATE
和
onEVENT
的STATE/EVENT被替換成具體的名字後名字相同引起問題,
to
狀态和
name
名字盡量不要相同。比如
-- 角色開火
{name = "fire", from = "idle", to = "fire"}
--假如使用簡寫
--onSTATE --- onfire
--onEVENT --- onfire,回調會引起歧義。
--如果不使用簡寫
--則onenterSTATE --- onenterfire
--onafterEVENT --- onafterfire
另外,我們可以使用5種通用型的回調來捕獲所有事件和狀态的變化:
-
- 在任何事件開始前被激活onbeforeevent
-
- 在離開任何狀态時被激活onleavestate
-
- 在進入任何狀态時被激活onenterstate
-
- 在任何事件結束後被激活onafterevent
-
- 當狀态發生改變的時候被激活onchangestate
注解:這裡是任何事件、狀态, 小寫的event、state不能用具體的事件、狀态名字替換。
回調參數
所有的回調都以
event
為參數,該event為表結構,包含了
- name 事件名字
- from 事件表示的起始狀态
- to 事件表示的目的狀态
- args 額外的參數,用來傳遞使用者自定義的一些變量值
local fsm = StateMachine.new()
fsm = fsm:setupState({
initial = "green",
events = {
{name = "warn", from = "green", to = "yellow"},
{name = "panic", from = "green", to = "red" },
{name = "calm", from = "red", to = "yellow"},
{name = "clear", from = "yellow", to = "green" },
},
callbacks = {
onbeforestart = function(event) print("[FSM] STARTING UP") end,
onstart = function(event) print("[FSM] READY") end,
onbeforewarn = function(event) print("[FSM] START EVENT: warn!") end,
onbeforepanic = function(event) print("[FSM] START EVENT: panic!") end,
onbeforecalm = function(event) print("[FSM] START EVENT: calm!") end,
onbeforeclear = function(event) print("[FSM] START EVENT: clear!") end,
onwarn = function(event) print("[FSM] FINISH EVENT: warn!") end,
})
fsm:doEvent("warn", "some msg")
如上例子,
fsm:doEvent("warn", "some msg")
中的
some msg
作為額外的參數字段
args
結合
name
from
to
被添加到
event
,此時
event = {
name = "warn",
from = "green",
to = "yellow",
args = "some msg"
}
而
event
表正是回調函數的參數。
回調順序
用{name = "clear", from = "red", to = "green"}舉例,我畫個示意圖來說明
注意:之前的
onbeforeEVENT
,這裡
EVENT
就被具體替換為
clear
,于是是
onbeforeclear
,而
onbeforeevent
類似的通用型則不用替換。
- onbeforeclear - clear事件執行前的回調
- onbeforeevent - 任何事件執行前的回調
- onleavered - 離開紅色狀态時的回調
- onleavestate - 離開任何狀态時的回調
- onentergreen - 進入綠色狀态時的回調
- onenterstate - 進入任何狀态時的回調
- onafterclear - clear事件完成之後的回調
- onafterevent - 任何事件完成之後的回調
3種影響事件響應的方式
- 在
方法中傳回false來取消事件onbeforeEVENT
- 在
方法中傳回false來取消事件onleaveSTATE
- 在
方法中傳回onleaveSTATE
來執行異步狀态轉換ASYNC
異步狀态轉換
有時候,我們需要在狀态轉換的時候執行一些異步性代碼來確定不會進入新狀态直到代碼執行完畢。
舉個例子來說,假如要從一個
menu
狀态轉換出來,或許我們想讓TA淡出?滑出螢幕之外?總之執行完動畫再進入
game
狀态。
我們可以在
onleavestate
或者
onleaveSTATE
方法裡傳回
StateMachine.ASYNC
,這時狀态機會被挂起,直到我們使用了event的
transition()
方法。
...
onleavered = function(event)
self:log("[FSM] LEAVE STATE: red")
self:pending(event, 3)
self:performWithDelay(function()
self:pending(event, 2)
self:performWithDelay(function()
self:pending(event, 1)
self:performWithDelay(function()
self.pendingLabel_:setString("")
event.transition()
end, 1)
end, 1)
end, 1)
return "async"
end,
...
提示:如果想取消異步事件,可以使用event的 cancel()
方法。
初始化選項
- 狀态機的初始化選項一般根據我們遊戲需求來決定,quick狀态機提供了幾個簡單的選項。在預設情況下,如果你沒指定
狀态,狀态機會指定目前狀态為initial
狀态,是以需要定義一個能将none
狀态轉換出去的事件。none
local fsm = StateMachine.new() fsm = fsm:setupState({ events = { {name = "startup", from = "none", to = "green" }, {name = "panic", from = "green", to = "red" }, {name = "calm", from = "red", to = "green"} } }) echoInfo(fsm:getState()) -- "none" fsm:doEvent("start") echoInfo(fsm:getState()) -- "green"
- 如果我們特别指定了
狀态,那麼狀态機在初始化的時候會自動建立initial
事件,并且被執行。startup
local fsm = StateMachine.new() fsm = fsm:setupState({ initial = "green", events = { -- 當指定initial狀态時,這個startup事件會被自動建立,是以可以不用寫這一句 {name = "startup", from = "none", to = "green" }, {name = "panic", from = "green", to = "red" }, {name = "calm", from = "red", to = "green"} } }) echoInfo(fsm:getState()) -- "green"
- 我們也可以這樣指定
狀态:initial
local fsm = StateMachine.new() fsm = fsm:setupState({ initial = {state = "green", event = "init"}, events = { {name = "panic", from = "green", to = "red" }, {name = "calm", from = "red", to = "yellow"} } }) echoInfo(fsm:getState()) -- "green"
- 如果我們想延緩初始化狀态轉換事件的執行,我們可以添加
defer = true
local fsm = StateMachine.new() fsm = fsm:setupState({ initial = {state = "green", event = "init", defer = true}, events = { {name = "panic", from = "green", to = "red" }, {name = "calm", from = "red", to = "green"} } }) echoInfo(fsm:getState()) -- "none" fsm:doEvent("init") echoInfo(fsm:getState()) -- "green"
異常處理
在預設情況下,如果我們嘗試着執行一個目前狀态不允許轉換的事件,狀态機會抛出異常。如果選擇處理這個異常,我們可以定義一個錯誤事件處理。在quick中,發生異常的時候
StateMachine:onError_(event, error, message)
會被調用。
local fsm = StateMachine.new()
fsm:setupState({
initial = "green",
events = {
{name = "warn", from = "green", to = "yellow"},
{name = "panic", from = "green", to = "red" },
{name = "calm", from = "red", to = "green"},
{name = "clear", from = "yellow", to = "green" },
}
})
fsm:doEvent("calm") -- fsm:onError_會被調用,在目前green狀态下不允許執行calm事件
本文如果有寫的不對的地方,還請大家指出,交流學習:)
如果朋友們有關于狀态機的使用心得,也非常歡迎分享。
暗黑項目實際使用情況:
在main的最後
---------------場景管理
g_sceneManager = require "src.SceneManager".new()
g_sceneManager:initEventData()
---------------場景管理
cc.Director:getInstance():replaceScene(g_sceneManager.scene)
其中狀态機是在initEventData()裡初始化的,
function SceneManager:initEventData()
Component.addComponent(self, "src.common.components.behavior.StateMachine")
local cfg =
{
initial = "login",
events = {},
callbacks =
{
onenterstate = function(event) return self:onenterstate(event) end,
onleavestate = function(event) return self:onleavestate(event) end,
},
}
self.stateList =
{
{"changeToLogin", "login", "LoginScene"},
{"changeToCity", "city", "CityScene"},
{"changeToBattle", "battle", "BattleScene"},
{"changeToBoss", "boss", "BossScene"},
{"changeToLoginLoading", "loginLoading", "LoginLoadingScene"},
{"changeToUnionFight", "unionFight", "UnionFightScene"},
{"changeToChangeState", "changeState", "ChangeStateScene"},
{"changeToChangeFight", "changeFight", "ChangeFightScene"},
{"changeToTeam", "team", "TeamScene"},
{"changeToJiebao", "jiebao", "JiebaoScene"},
{"changeToStory", "story", "StoryScene"},
{"changeToGlory", "glory", "GloryScene"},
{"changeToQunMo", "qunmo", "QunMoScene"},
{"changeToBigUnion", "bigunion", "BigUnionScene"},
{"changeToSoulStone", "soulstone", "SoulStoneScene"},
{"changeToPaTa","pata","PaTaScene"},
{"changeToShenShou","shenshou","ShenShouScene"},
{"changeToQiangDao","qiangdao","QiangDaoScene"},
}
for i, state in ipairs(self.stateList) do
local t = {name = state[1], to = state[2]}
table.insert(cfg.events, t)
self["onenter"..state[2] ] = function(self)
print("onenter"..state[2])
self.gameScene = import("src.scene."..state[3]).new()
self.gameScene:onenter(self.scene)
end
self[state[1] ] = function(self)
self:doEvent(state[1])
end
end
self:setupState(cfg)
end
Component.addComponent(self, "src.common.components.behavior.StateMachine")是将場景管理綁定了狀态機,分析底層代碼,我們看到
function Component.addComponent(target, name)
if not target.components_ then
Component.extend_(target)
end
target:addComponent(name):exportMethods()
end
先執行Component.extend_(target)
繼續跟進這個函數裡面 function Component.extend_(target)
target.components_ = {}
function target:checkComponent(name)
return self.components_[name] ~= nil
end
function target:addComponent(name)
local component = Registry.newObject(name)
self.components_[name] = component component:bind_(self)
return component
end
function target:removeComponent(name)
local component = self.components_[name]
if component then component:unbind_() end
self.components_[name] = nil
end
function target:getComponent(name)
return self.components_[name]
end
end 其實這裡面啥也沒幹,隻是給target注冊了幾個接口,緊接着
target:addComponent(name):exportMethods() 就調用了裡面的接口,我們先着重分析接口 target:addComponent(name) ,它裡面實際上是把StateMachine這個檔案加載了進來并進行了初始化 ,
function Registry.newObject(name, ...)
local cls = Registry.classes_[name]
if not cls then
-- auto load
pcall(function()
cls = require(name)
Registry.add(cls, name)
end)
end
assert(cls ~= nil, string.format("Registry.newObject() - invalid class \"%s\"", tostring(name)))
return cls.new(...)
end
然後将狀态機綁定了在場景管理器中
function Component:bind_(target)
self.target_ = target
for _, name in ipairs(self.depends_) do
if not target:checkComponent(name) then
target:addComponent(name)
end
end
self:onBind_(target)
end
target:addComponent(name)它傳回的是一個StateMachine對象,然後調用了它的 exportMethods() ,繼續跟進去我們發現
function StateMachine:exportMethods()
self:exportMethods_({
"setupState",
"isReady",
"getState",
"isState",
"canDoEvent",
"cannotDoEvent",
"isFinishedState",
"doEventForce",
"doEvent",
})
return self.target_
end
function Component:exportMethods_(methods)
self.exportedMethods_ = methods
local target = self.target_
local com = self
for _, key in ipairs(methods) do
if not target[key] then
local m = com[key]
target[key] = function(__, ...)
return m(com, ...)
end
end
end
return self
end
他其實是把 "setupState",
"isReady",
"getState",
"isState",
"canDoEvent",
"cannotDoEvent",
"isFinishedState",
"doEventForce",
"doEvent", 這些函數接口提供了給它的綁定對象scenemanager使用,使得它可以像使用自己的接口一樣的使用, 如SceneManager裡面的接口initEventData的最後一行, self:setupState(cfg) ,跟進這個接口去
function StateMachine:setupState(cfg)
assert(type(cfg) == "table", "StateMachine:ctor() - invalid config")
-- cfg.initial allow for a simple string,
-- or an table with { state = "foo", event = "setup", defer = true|false }
if type(cfg.initial) == "string" then
self.initial_ = {state = cfg.initial}
else
self.initial_ = clone(cfg.initial)
end
self.terminal_ = cfg.terminal or cfg.final
self.events_ = cfg.events or {}
self.callbacks_ = cfg.callbacks or {}
self.map_ = {}
self.current_ = "none"
self.inTransition_ = false
if self.initial_ then
self.initial_.event = self.initial_.event or "startup"
self:addEvent_({name = self.initial_.event, from = "none", to = self.initial_.state})
end
for _, event in ipairs(self.events_) do
self:addEvent_(event)
end
if self.initial_ and not self.initial_.defer then
self:doEvent(self.initial_.event)
end
return self.target_
end
它裡面是初始化了初始狀态,即登陸狀态,而且注冊了其他的狀态。當我們點選登陸之後,這個時候需要向伺服器請求資料,當所有的資料都到來之後就切換到主城狀态,是通過g_sceneManager:changeToCity(),這個函數是在initEventData裡定義的,
self[state[1] ] = function(self)
self:doEvent(state[1])
end
我們着重分析doEvent,前面基本都是一些是否可以切換狀态的判斷,重點關注
event.transition = function()
self.inTransition_ = false
self.current_ = to -- this method should only ever be called once
self:enterState_(event)
self:changeState_(event)
self:afterEvent_(event)
return StateMachine.SUCCEEDED
end
這個就是轉換的過程, 然後我們分析enterState_
function StateMachine:enterState_(event)
self:enterThisState_(event) --進入特定狀态的回調
self:enterAnyState_(event) --進入任何狀态的回調
end
function StateMachine:enterThisState_(event)
return doCallback_(self.callbacks_["onenter" .. event.to] or self.callbacks_["on" .. event.to], event)
end doCallback_是這樣定義的:
local function doCallback_(callback, event)
if callback then return callback(event) end
end
而它的參數callback在這裡self.callbacks_["onenter" .. event.to] or self.callbacks_["on" .. event.to],其中self.callbacks_是在初始化接口
function StateMachine:setupState(cfg)
self.callbacks_ = cfg.callbacks or {}
end
也就是SceneManager裡面的
local cfg =
{
initial = "login",
events = {},
callbacks =
{
onenterstate = function(event) return self:onenterstate(event) end,
onleavestate = function(event) return self:onleavestate(event) end,
},
}
它裡面隻定義了兩個回調,進入任何狀态和離開任何狀态的回調,再分析這兩個接口
function SceneManager:onenterstate(event)
self["onenter"..event.to](self)
end
進入的時候又轉換成了特定的事件,self["onenter"..event.to](self)的定義如下:
self["onenter"..state[2] ] = function(self)
print("onenter"..state[2])
self.gameScene = import("src.scene."..state[3]).new()
self.gameScene:onenter(self.scene)
end
state[2]是特定的狀态,也即要切換的狀态event.to
function SceneManager:onleavestate(event)
print("onleave"..event.from)
if self.gameScene then
self.gameScene:onleave(self.scene)
self.gameScene = nil
end
end
離開可以在特定的場景裡的onleave做些處理 ,切換狀态的時候可以是要先離開上一個狀态,然後才可以切換到下一個狀态,在doEvent裡面它是這麼處理的:
self.inTransition_ = true --表示正在切換狀态,會在event.transition裡面把這個标志改回false
local leave = self:leaveState_(event)--離開目前狀态
if leave == false then --如果離開失敗,則停留在目前狀态什麼都不做
event.transition = nil
event.cancel = nil
self.inTransition_ = false
return StateMachine.CANCELLED
elseif string.upper(tostring(leave)) == StateMachine.ASYNC then --如果目前是異步,則挂起等待上一個狀态的代碼執行完畢再切換
return StateMachine.PENDING
else
-- need to check in case user manually called transition()
-- but forgot to return StateMachine.ASYNC
if event.transition then
return event.transition() --切換下一個狀态
else
self.inTransition_ = false
end
end
至此整個狀态機的流程就分析完了。