目录
注:写这篇文章的时候,笔者所用的是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:上面例子的
rest
事件可以拆分写成4个,如下:{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种特定事件类型的回调:
onbeforeEVNET
- 在特定事件EVENT开始前被激活onleaveSTATE
- 在离开旧状态STATE时被激活onenterSTATE
- 在进入新状态STATE时被激活onafterEVENT
- 在特定事件EVENT结束后被激活
注解:编码时候,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种影响事件响应的方式
- 在
onbeforeEVENT
方法中返回false来取消事件 - 在
onleaveSTATE
方法中返回false来取消事件 - 在
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
至此整个状态机的流程就分析完了。