跳到主要內容

實作鋤草機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
大功告成!

留言

這個網誌中的熱門文章

猜數字遊戲 (電腦猜人)

前幾天午睡時突然被告知要參加公司內部的程式設計比賽,題目是用C寫一支文字模式的4位數字猜數字遊戲,由使用者來猜電腦的數字。在上星期時其實就已經有公佈了,但我沒有注意到所以是臨時加入,還好這是個簡單的題目,不用花多少時間就可以寫出來。 規則: - 這是一對一比賽,雙方各選擇一4位數字,不讓對方知道。 - 4位數字由數字0至9組成,每位數不得重複。 - 雙方輪流猜對方的數字,直到一方猜中為止。 - A方猜B方的數字後,B方根據A方的猜測回答幾A幾B。 - 一個A表示猜中一個數字且位置正確,一個B表示猜中一個數字但位置不正確。 - 當一方猜中4A0B時即表示猜中對方全部4個數字且位置正確,贏得比賽。 - 例:B的謎底是4208,底下箭頭左測是A的猜測,箭頭右測是B的回答。    1234 ==> 1A1B    5678 ==> 1A0B    2406 ==> 1A2B    ...    4208 ==> 4A0B ; 寫個程式讓玩家來猜電腦的數字不難,不過我從來沒有寫過讓電腦來猜玩家數字的版本,所以花了點時間想想怎麼寫。 研究後歸納出二個點。 1, 使用窮舉法將所有可能數字組合列出。 2, 每次猜測後根據結果排除不可能是答案的組合,重複這個動作直到猜中答案為止。 第1點只是實作問題,第2點概念也很簡單,但要過濾不是答案的組合根據的是什麼?乍看之下沒什麼頭緒,不過想通之後就非常簡單了。 它的基本原理如下:假如謎底是4561,如果猜1524則會得到1A2B。從相反的角度來看,如果謎底是1524,則猜4561時也會得到1A2B的回答。 利用這個方法,每一次猜測一個數字X後,再以這個數字當作答案,來和所有剩下來的候選答案作比對,如果得到的結果(幾A幾B)和數字X是一樣的話,就把這個數字保留下來繼續作為候選答案,否則就過把這個數字過濾掉。下一把,繼續從候選答案裡選一個出來猜,重複上面的動作,直到猜中為止。 ; C++ STL的algorithm裡有個叫作next_permutation的函數,可以用來生成排列。 #include <iostream> #include <algorithm> using namespace std; int main () {   int myints[] = {1,2,3};  ...

單人撲克牌遊戲 - 蒙地卡羅

新增一個簡單的單人撲克牌遊戲: 蒙地卡羅 ,簡單介紹一下玩法。 下載 事先排列好5x5張牌。 每次移動一張可以配對的牌,並消除這對牌。在上下、左右及斜向相隣的二張牌,只要擁有同樣數字(不計花色),即可配對。 消除二張配對的牌後,剩餘的牌以往左往上的方式補滿空隙,接著在發新牌補滿後面的空格。 重覆步驟2~3,直到沒有牌可以配對及發完所有牌為止。 結果有二種。一個是勝利,成功的消除掉所有牌。另一個是Gameover沒有牌可以再作配對。

以lex/yacc實作算式計算機

前面我們透過 手工的方式 實作了一個簡易的算式計算機,現在我們要開始使用工具來作同樣的事,比較看看手工和使用工具有什麼不同的差別。首先要介紹的就是lex&yacc。 lex & yacc lex(Lexical Analyzar)及yacc(Yet Another Compiler Compiler)是用來輔助程式設計師製作語法剖析器的程式工具。lex的工作就是幫助我們將輸入的資料文字串流分解成一個個有意義的token,而yacc的工作就是幫我們分析這些token和我們定義的規則作匹配。下圖中所表示的是使用lex及yacc的一般工作流程。 首先看到yacc會讀入一個.y檔案,這裡.y檔案的內容就是我們使用類似(E)BNF語法定義的語法規則,yacc會分析這些語法規則後,幫我們產生可以用來解析這些規則的程式碼,而這個檔案一般名稱預設為y.tab.c,產生的程式碼裡面最重要的一個的函式叫作yyparse。 同yacc類似,lex也會讀入一個.l的檔案,這個檔案裡面定義的是如何從文字流裡解出token的規則,使用的方法是常規表示式(regular expression)。在圖的左側中間我們還可以看到有一個叫作y.tab.h的檔案從yacc產生出來並餵給lex作輸入,這個檔案是yacc根據在讀入的.y檔裡面所定義的token代號所產生出來的一個header,這樣yacc及lex產生出來的程式碼裡面就可以使用共通定義的代碼而不必各寫個的。lex分析過.l檔案後也會產生一個一般預設叫作lex.yy.c的原始碼檔案,裡頭最重要的一個函式叫作yylex。 最後,我們把yacc產生出來的y.tab.c還有lex產生出來的lex.yy.c,以及其它我們自己撰寫的原始碼檔案一起拿來編譯再作連結,最後產生出來的就是一個可以用來解析我們定義的語法的解析器工具。以上是整個lex及yacc的使用流程概觀。 常規表示式 在正式使用lex之前,我們首先來對常規表示法作一個基本的認識。常規表示法是一種用來表示字串樣式(pattern)的中繼語言,就好比前文所介紹的(E)BNF表示式一樣,都是用來描述其它語言的語言,只不過用途不太一樣罷了。 常規表示式使用一些中繼符號(meta-symbol)以及ASCII字元定義字串樣式,以下列出一些常規表示式所使用的符號。 . 表示除了換行字元...