狀態機在quick中是一個亮點,如果我們做一款RPG遊戲,一個角色一般會擁有idle,attack,walk,run,death這些狀態,如果遊戲角色的狀態採用分支條件判斷的話,會造成非常龐大而難以維護,但一旦使用了狀態機這種模式,就會顯得簡單方便。
對於quick中的狀態機是如何實現的咱們先不去了解,首先看看如何去使用它。
總結起來,如果讓一個類擁有狀態機,主要有兩步:
1.創建狀態機對象
2.初始化狀態機,主要包括事件和回調函數
1.創建狀態機組件
- self.fsm = {}
- cc.GameObject.extend(self.fsm):addComponent("components.behavior.StateMachine"):exportMethods()
這樣就創建了一個狀態機對象,接下來我們要對其初始化,其實也就是設置各個狀態的邏輯。
2.初始化狀態機(設置狀態邏輯)
設置狀態邏輯是重寫setupState方法,這其中有這麼幾個字段參數,
- initial:狀態機的初始狀態
- terminal (final):結束狀態
- events:狀態發生轉變時對應的事件
- callbacks:發生轉變時的回調函數
一般我們會設置initial,events和callbacks這三個。
先看events,在events中需要分清楚“事件”和“狀態”,events採用table結構,例如我們來寫一個
- events = {
- {name = "move", from = {"idle", "jump"}, to = "walk"},
- }
這其中move是事件,就像觸摸事件event.name那樣,name表示事件名稱,而from和to後面跟的idle,jump,walk表示狀態。所以上面的意思就是,當執行move事件時,如果狀態是idle或者jump,那麼都會跳轉到walk狀態上。
from的狀態可以是單一狀態,也可以使集合狀態,就是幾個狀態,但to的狀態只能唯一,不然程序還給你來個隨機狀態?肯定不行的。
所以這裏需要想好我們的主角有哪些狀態,當什麼事件發生時,他會從什麼狀態變到什麼狀態上去。例如我簡單這麼寫,
- events = {
- {name = "move", from = {"idle", "jump"}, to = "walk"},
- {name = "attack", from = {"idle", "walk"}, to = "jump"},
- {name = "normal", from = {"walk", "jump"}, to = "idle"},
- },
解釋一下,如果是normal事件,不管主角在走路walk還是跳躍jump,都會變成閒置idle狀態。其他同理。
接下來一個重點是callbacks參數,
即所謂回調了,就是事件觸發,會執行一系列的函數。
- onbeforeEVNET: 在事件EVENT開始前被激活
- onleaveSTATE: 在離開舊狀態STATE時被激活
- onenterSTATE 或 onSTATE:在進入新狀態STATE時被激活
- onafterEVENT 或 onEVENT:在事件EVENT結束後被激活
例如
- callbacks = {
- onenteridle = function () --或者 onidle
- print("idle")
- end,
- },
此外還有5種通用型的回調來捕獲所有事件和狀態的變化:
- onbeforeevent: 在任何事件開始前被激活
- onleavestate: 在離開任何狀態時被激活
- onenterstate:在進入任何狀態時被激活
- onafterevent :在任何事件結束後被激活
- onchangestate :當狀態發生改變的時候被激活
這裏面的名稱是不可以修改的,它是針對於任何事件和任何狀態的。
所以大家可以想象一下這其中有多少事件回調和多少狀態回調,它們的先後順序,咱們可以自己分別print一下就知道調用的先後了,這裏就不演示了。
最後,就是調用這些事件了,通過self.fsm:doEvent(event)就可以了,參數event對應events參數名稱。此外還有這些,
- 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, ...) :強制對當前狀態進行轉換
接下來在實際運用一下,我們創建一個Player類,爲其添加一個狀態機,
- local Player = class("Player", function ()
- return display.newSprite("icon.png")
- end)
- function Player:ctor()
- self:addStateMachine()
- end
- function Player:doEvent(event)
- self.fsm:doEvent(event)
- end
- function Player:addStateMachine()
- self.fsm = {}
- cc.GameObject.extend(self.fsm):addComponent("components.behavior.StateMachine"):exportMethods()
- self.fsm:setupState({
- initial = "idle",
- events = {
- {name = "move", from = {"idle", "jump"}, to = "walk"},
- {name = "attack", from = {"idle", "walk"}, to = "jump"},
- {name = "normal", from = {"walk", "jump"}, to = "idle"},
- },
- callbacks = {
- onenteridle = function ()
- local scale = CCScaleBy:create(0.2, 1.2)
- self:runAction(CCRepeat:create(transition.sequence({scale, scale:reverse()}), 2))
- end,
- onenterwalk = function ()
- local move = CCMoveBy:create(0.2, ccp(100, 0))
- self:runAction(CCRepeat:create(transition.sequence({move, move:reverse()}), 2))
- end,
- onenterjump = function ()
- local jump = CCJumpBy:create(0.5, ccp(0, 0), 100, 2)
- self:runAction(jump)
- end,
- },
- })
- end
- return Player
比較簡單,回調函數只是寫了進入三個狀態的回調,然後爲Player添加一個doEvent函數,調用狀態機中doEvent。
回到我們的MyScene.lua中,
- local Player = import("..views.Player")
- local MyScene = class("MyScene", function ()
- return display.newScene("MyScene")
- end)
- function MyScene:ctor()
- local player = Player.new()
- player:setPosition(display.cx, display.cy)
- self:addChild(player)
- local function menuCallback(tag)
- if tag == 1 then
- player:doEvent("normal")
- elseif tag == 2 then
- player:doEvent("move")
- elseif tag == 3 then
- player:doEvent("attack")
- end
- end
- local mormalItem = ui.newTTFLabelMenuItem({text = "normal", x = display.width*0.3, y = display.height*0.2, listener = menuCallback, tag = 1})
- local moveItem = ui.newTTFLabelMenuItem({text = "move", x = display.width*0.5, y = display.height*0.2, listener = menuCallback, tag = 2})
- local attackItem = ui.newTTFLabelMenuItem({text = "attack", x = display.width*0.7, y = display.height*0.2, listener = menuCallback, tag = 3})
- local menu = ui.newMenu({mormalItem, moveItem, attackItem})
- self:addChild(menu)
- end
- return MyScene
添加我們剛纔的Player,記得import或者require,這裏爲了方便我就通過菜單按鈕的形式來分別doEvent了。