详解cocos2dx状态机

目录

注:写这篇文章的时候,笔者所用的是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

所以假如要使用简写的话,为了避免onSTATEonEVENT的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结合namefromto被添加到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种影响事件响应的方式
  1. onbeforeEVENT方法中返回false来取消事件
  2. onleaveSTATE方法中返回false来取消事件
  3. 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   

至此整个状态机的流程就分析完了。                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
       



  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值