quick-cocos2d-x遊戲開發【14】——StateMachine狀態機

狀態機在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了。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章