實作鋤草機GamePlay

設定物件類別

good的GamePlay使用Lua實作,所以繼續閱讀以下內容之前,我會先假設你已俱備使用Lua的基本能力,當然這也表示你對什麼是程式設計有基本的概念。

首先開啟編輯好一個關卡的鋤草機專案,接著在資源樹上點擊關卡開啟關卡編輯器。


在屬性檢視器裡,原本空白的Script欄位要填入個物件類別名稱。當我們執行遊戲時,RunTime除了會根據關卡資源建立所有物件外,如果物件有指定物件類別旳話,則會另外建立一個物件類別的Instance關聯到這個物件,然後在執行時期執行這個物件類別所定義的Script。物件類別對應於物件類別的Instance的關係,就相當於資源對應於物件。

物件類別

如果你OOP的經驗的話會更容易了解些。在good裡面的一個物件類別,相當於OOP裡的一個Class。它可以有Data Member,也可以有Member Function。所以在前面才會說,當物件建立時,假如有指定物件類別的話,會另外建立一個物件類別的Instance關聯到這個物件,這個意思和我們在作OOP的概念是一樣的,是Class和Instance的關係。

good的物件類別是以Lua的Table實作的。下面的範例使用Lua的語法建立一個空的類別,也就是一個空的Table。雖然沒什麼功能,不過這樣子就能使用了,可以把它填入Script欄位。
Level = {}
現在Level是個空類別,為了使它擁有GamePlay我們要為它加上處理Event的能力。
Level.OnStep = function(param)
end
我們替Level類別加上了一個叫作OnStep的事件處理函式。OnStep是個特殊的函式名稱,這個名稱被good使用來作為一個Event通知函式。每一個Frame這個函式會先被呼叫一次,也就是說假如FPS是60的話,那麼OnStep函式每秒鐘會被呼叫60次。當然前題是你指定給物件的類別必需要有提供了OnStep的實作。

OnStep這個函式有個叫作param的參數。這個param參數傳來的就是前面提到過的物件的Instance。

除了OnStep事件之外,目前還支援的事件有OnCreate以及OnDestroy事件,分別對應到物件建立完成後以及物件被刪除前的通知。這二個事件也同樣有一個param參數,功能和OnStep事件的param參數一模一樣。

鋤草機GamePlay簡介

這邊要先對鋤草機的GamePlay作一下簡介,這樣至少對於接下來要作什麼有個大概的了解。

每一個關卡的初始狀態會在特定位置擺好一台鋤草機朝向特定的方向。一台鋤草機分成頭和身體二部份,頭部和身體的方向是各自獨立的,一開始的時候頭和身體的朝向是一致的。遊戲開始時鋤草機是靜止不動作,要讓它動起來必須先按一下和鋤草機一開始朝向相同的方向鈕,它才會開始移動。移動過程中你可以隨時按方向鈕改變鋤草機的頭的方向,每走完一個格子後,鋤草機就會根據當時鋤草機的頭的方向來改變它的移動方向。

遊戲的目的是要把關卡裡所有的草都除光才能過關。限制是鋤草機只能走在草地上,除此之外都會導至鋤草機損毀GameOver。

偵測輸入

為了讓遊戲可以進行,我們必須要處理按鍵的輸入。good裡面按鍵的處理是以檢查按鍵狀態的方式來處理,而不是透過Event通知。偵測的功能是由Input模組所提供,共有三個方法,分別為:IsKeyDown、IsKeyPressed及IsKeyPushed。

如果你按住某鍵一直不放開,則IsKeyDown會一直回傳true。
如果你按住某鍵一直不放開,則只有當你在放開的那一瞬間IsKeyPressed才會回傳true。
如果你按住某鍵一段時間後再放開它,則只有當你在剛按下那個鍵的時候IsKeyPushed才會回傳true。

以上是這三個方法的區別。呼叫的方式如下面簡單的例子所示。
if (Input.IsKeyPressed(Input.KEYS_LEFT)) then
end
Input.KEYS_LEFT是一個按鍵代碼,表示我們要檢查的是Left方向鍵的狀態。除了KEYS_LEFT外,還有KEYS_RIGHT、KEYS_DOWN、KEYS_UP、KEYS_RETURN等等。

物件的階層關係

鋤草機的頭是接在身體上,身體移動的話,頭也會跟著移動到同樣的地方。假如有階層關係的話,要實作起來就會很簡單,只需要設定好頭的爸爸是身體,讓頭自動跟著身體去移動就行了,而不必每次移動都要同時設定身體和頭二個部位。

good的物件可以有階層關係,不過在編輯器上目前還未實作,所以我們需要在程式裡指定。這個功能由Good模組的AddChild來提供。
Good.AddChild(body, head)
這面這個簡單的範例示範把head這個物件加入到body物件的子物件串列裡,讓head的爸爸變成body。

判定鋤草機的前進方向

記得我們在編輯器裡,分別編輯了鋤草機的身體和頭的四個方向的精靈。因為同一時間,一個物件只能套用一個精靈,所以我們只需要檢查現在物件是套用了那個精靈物件,就可以用來判定鋤草機的方向。分別檢查身體和頭所使用的精靈資源,就可以分別知道身體和頭的方向。

要知道目前物件是套用了那個精靈資源,我們需要使用Good模組的GetSpriteId方法。
local idSpr = Good.GetSpriteId(34)
上面這行簡單的範例示範取得ID是34的物件的精靈ID,請注意這個精靈ID是個資源ID。

問題是34是怎麼來的,我們怎麼知道物件的ID是多少?這有很多辨法,最簡單的辨法是,我們可以直接從屬性檢視器去檢視。編輯關卡時,我們種到關卡上的每個物件都會有一個ID。當這個關卡在RunTime被建立起來時,也會以相同的ID配置建立起物件。這一點就很方便我們可以立刻找到特定物件的ID,我們在實作鋤草機時也是利用這個特點來找到關卡裡面鋤草機的頭和身體ID。


物件位置的存取

這是個非常基本且重要的功能,有了這個功能後,透過改變物件位置我們才可以讓物件動起來。這個功能由Good模組所提供。
local x,y = Good.GetPos(idObj)
上面這個簡單的範例示範如何取得一個物件的位置。注意這個位置是相對於父親物件的左上角,且原點是定義在物件自己的左上角上。
Good.SetPos(idObj, newx, newy)
如範例,設定新位置同樣簡單。

讓鋤草機動起來

把上面所介紹的知識綜合起來,已經可以讓鋤草機動起來了。首先我們開一個新的純文字檔案,把它和我們的專案檔放在同一個資料夾裡,檔名叫作weeder.lua。

根據GamePlay的設定,我們檢查一個叫作running的全域狀態變數,來判定是鋤草機是不是正在動,如果running是false則我們要再檢查User有沒有按一下和鋤草機面向相同的方向鈕。
local running = false
local body, head = 34, 35
local body_up, body_down, body_left, body_right = 9, 10, 11, 12

Level = {}

Level.OnStep = function(param)
if (nil == param.init) then
param.init = true
Good.AddChild(body, head)
Good.SetPos(head, 0,0)
end

if (not running) then
if (Input.IsKeyPushed(Input.KEYS_LEFT)) then
running = true
end
return
end

local spd = 0.5
local x,y = Good.GetPos(body)
local dir = Good.GetSpriteId(body)

if (body_left == dir) then
x = x - spd
elseif (body_right == dir) then
x = x + spd
elseif (body_up == dir) then
y = y - spd
elseif (body_down == dir) then
y = y + spd
end

Good.SetPos(body, x, y)
end
上面的程式碼中,第一段是用來檢查初始狀態決定是否要讓鋤草機動起來,第二段的作用則是根據當時鋤草機的面向,繼續往前移動。

到這邊為止,我們再回到編輯器。點擊新增Script鈕,選取weeder.lua加入到專案中。然後在點擊資源樹上的Level,在屬性檢視器裡的Script欄位上填入Level。


接著點擊執行鈕後,按一下左鍵。可以看到,鋤草機動起來了!只不過它現在只會往左邊直直走,一直走出畫面外面去,所以我們還要再加點東西。

讓鋤草機走格子

根據GamePlay鋤草機每走完一格後會依據當時頭的朝向改變移動方向。我們使用一個簡單的方法來實作這個功能。首先我們增加一個叫作movement的全域變數,每次我們移動時就把移動量累加到這個變數。當移動量累計到大於等於32時(也就是一個格子的大小),就可以依據當時頭的朝向改變移動方向。

另外移動過程中可以隨時使用方向鍵改變頭的方向。處理這件事情的時候有個小地方要再多考慮一下,那就是鋤草機不能向後轉。
local movement = 0
local head_up, head_down, head_left, head_right = 5, 6, 7, 8

Level.OnStep = function(param)
...
local dir = Good.GetSpriteId(body)
if (Input.IsKeyPushed(Input.KEYS_LEFT)) then
if (body_right ~= dir) then
Good.SetSpriteId(head, head_left)
end
elseif (Input.IsKeyPushed(Input.KEYS_RIGHT)) then
if (body_left ~= dir) then
Good.SetSpriteId(head, head_right)
end
elseif (Input.IsKeyPushed(Input.KEYS_UP)) then
if (body_down ~= dir) then
Good.SetSpriteId(head, head_up)
end
elseif (Input.IsKeyPushed(Input.KEYS_DOWN)) then
if (body_up ~= dir) then
Good.SetSpriteId(head, head_down)
end
end

movement = movement + spd

local headdir = Good.GetSpriteId(head)
if (32 == movement) then
movement = 0

if (head_up == headdir) then
Good.SetSpriteId(body, body_up)
elseif (head_down == headdir) then
Good.SetSpriteId(body, body_down)
elseif (head_left == headdir) then
Good.SetSpriteId(body, body_left)
elseif (head_right == headdir) then
Good.SetSpriteId(body, body_right)
end
end
end
HitTest & KillObj

我們還需要控制鋤草機只能走在草地上,走到草地之外的格子的話,就會GameOver。這個功能需要使用到對物件的HitTest,我們需要知道在某個位置是不是存在物件。Good模組提供了FindObj可以作到這件事情。
local hit = Good.FindObj(x, y, grass)
上面這個範例檢查在座標(x,y)的位置有沒有精靈ID是grass的物件,假如存在這個的物件則FindObj會回傳物件ID,否則回傳一個小於或等於0的值表示找不到。或者如果你要檢查的物件的精靈ID不作限制的話,第三個參數也可以忽略不填。

再來我們還需要刪除物件的功能,這個功能由Good模組的KillObj提供。
Good.KillObj(idObj)
有了這個二個功能就可以除草了。

讓鋤草機除草

在上面我們加了movement作為一個用來檢查判定是否轉向的counter,這邊我們繼續拿這個counter來使用。在movement滿32時要轉向然後歸0,而這邊我們在轉向movement歸0後,累加到1時檢查當時鋤草機腳底下的物件是不是草地,如果是的話把它清除,如果不是就GameOver。把程式碼再作一點修正,如下所示。
local grass = 4
local gameover = false

Level.OnStep = function(param)
...
if (32 == movement) then
Good.KillObj(Good.FindObj(x, y, grass))
...
elseif (1 == movement) then
if (head_right == headdir) then
x = x + 32
elseif (head_down == headdir) then
y = y + 32
end

local hit = Good.FindObj(x, y, grass)
if (0 >= hit) then
gameover = true
end
end
end
有個小地方注意一下,在上面的程式碼裡面我們有根據頭的面向對x或y作了一點小修正。這是因為我們物件的座標原點是定義在左上角的緣故。

現在只剩下最後一個功能就可以完成了,那就是要在GameOver時加上簡單的爆炸特效。

生成物件

Good模組提供了一個GenObj的方法可以讓我們動態生成一個物件。
local idNew = Good.GenObj(idParent, idRes, script)
這個方法有三個參數,第一個參數是用來指定新生的物件的爸爸是誰,第二個物件是用來指定新物件的類別,可以是個地圖或貼圖或精靈。而第三個物件是可有可無的,你可以根據需求指定一個物件類別(Script)給新物件。

有了這個功能,我們就可以用來產生爆炸效果了。

讀取地圖Tile

因為我們編輯了三種不同的爆炸效果,為了讓三種效果全都能派上用場,我們根據鋤草機撞到的不同Tile來決定要產生那一種效果。Resource模組的GetTileByPos提供一個類似FindObj的方法,不過它要找的是地圖上的Tile而不是物件。
local tile = Resource.GetTileByPos(idMap, x,y)
idMap是個地圖資源的ID,而x和y是相對於地圖左上角的座標。如果座標是有效的話,這個方法會回傳那個位置的地圖Tile的值,否則回傳0表示無效。那我們要怎麼知道我們要檢查的地圖Tile的值是多少?很簡單,打開地圖編輯器,檢視狀態列最右邊的欄位在括號中的數值就是滑鼠所指地圖Tile的值。


如圖中所示,可以看到石頭的Tile值是59。

產生爆炸效果

現在可以加上爆炸效果了。
local rock, ground, boom_small, boom_smallest, boom_big = 59, 57, 13, 14, 15

Level.OnStep = function(param)
...
if (32 == movement) then
...
elseif (1 == movement) then
...
local hit = Good.FindObj(x, y, grass)
if (0 >= hit) then
local tile = Resource.GetTileByPos(3, x - 224, y - 144)
local spr = boom_small
if (tile == rock) then
spr = boom_big
elseif (ground == tile) then
spr = boom_smallest
end
Good.GenObj(body, spr)
gameover = true
end
end
end
大功告成!

留言

這個網誌中的熱門文章

以lex/yacc實作算式計算機

猜數字遊戲 (電腦猜人)

KillSudoku 4顆星精彩數獨詳解 - 鍊技巧