天天看點

詳解cocos2dx狀态機

目錄

詳解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圖示

詳解cocos2dx狀态機

用法

建立一個狀态機

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"}舉例,我畫個示意圖來說明

詳解cocos2dx狀态機

注意:之前的

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   

至此整個狀态機的流程就分析完了。