2009年4月29日 星期三

貼圖背景物件功能擴展

現在關卡上的貼圖背景物件也能夠指定一個區塊範圍了。原來只能使用原貼圖的大小,現在除了可以指定一個大小外,也能夠指定一個偏移位置,這樣就能夠從原圖中隨意抓一塊矩形區塊出來使用了。

這個功能只是簡單的使用上次加上的位置大小屬性來應用。為了實現這個功能,同樣的除了執行時期需要作點修改外,編輯器部份也需要跟著作小變動,而程式介面則不需作任何修改。

2009年4月27日 星期一

從WIN32到iPhone移植good

good到iPhone的移植的作業雖不能說100%達成目標,但也有點小成果。這篇文章就來把整個過程大致的描述一下,這樣也能對移植過程有個大概的了解。


建立開發環境

要將一個程式從一個平台移植到另一個平台,第一個步驟就是先建立新平台的工作環境。要建立可以開發iPhone程式的環境的過程就不多說了,我只能建議說可能的話還是弄台Mac電腦省的不必要的麻煩。這算是和其它平台比較起來,一個較大的門檻。

有了可以跑Mac的電腦後,再來就是去下載Xcode,接著就是iPhone SDK


玩玩Sample

要了解一個新的平台,對我而言最容易上手的方式就是去玩玩它的範例,而且只挑作移植時需要看的就夠了。我首先要看的當然是怎麼畫圖,其它都不需要了。只要能畫圖我就可以初步知道我作的東西對不對。

我先找到一個非常簡單的範例,它只在畫面上貼二張靜態的2D圖形。透過這個簡單的範例再加上大概研究一下Objective-C的語法,就可以開始作點簡單的測試,玩玩怎麼畫圖。


決定測試移植的程式

在iPhone上的繪圖程式主要還是透過OpenGL ES作畫,所以我很快的決定拿25940p來作移植測試。

主要的原因是25940p已完成過跨平台移植測試(PSP),所以現在要移植到iPhone上,相對也會容易多。程式本身和平台無關的部份完全可以一次移植上去,不必作任何修改。而25940p的繪圖部份原本是使用OpenGL作簡單的幾何圖形表示,所以換成OpenGL ES也不會太難。

...(略)


準備移植

開始移植前,首先確認一下要移植的程式使用了那些模組,這些模組是否有支援目標平台。good使用了三個第三方程式庫,分別是SDLLuazlib。Lua及zlib基本上和任何平台無關,可以直接跨過去。比較有問題的是SDL。

雖然SDL本身有跨了包含了Mac的許多平台,但仔細確認後發現SDL是不支援iPhone的。搜尋一下後,發現有人作了iPhone的移植,但礙於iPhone的保密協議不能開放。這就麻煩了,要解決這個問題有三個方法。第一是自己移植SDL到iPhone上,第二是另外找一個代替品,第三是自己來。

稍微評估了第一個方法,結論是對我太難了,就算我能作也不是一天二天的事情。所以換第二個方案,開始上網找找有沒有替代品。

結果沒能很快找到合適的,最後還是決定用自己以前實作的一個小繪圖程式庫imgp。這是一個很小的工具,只提供基本的記憶體點陣圖的搬移,不過已經滿足good的最小需求。因為可以立刻派上用場,所以就決定用它不再繼續尋找替代方案。

而good使用到smallworld2裡的功能也是順利直接編譯通過,沒有作任何修改。


開始移植

首先在Xcode裡開個新的OpenGL ES for iPhone的新專案,把Lua和zlib加進去編譯看看,結果如預期正確無誤。

接下來寫點簡單的程式測試imgp同時作點相關的修正。因為imgp是對一塊記憶體作搬圖填色的操作,所以只要在最後的時候拿這塊記憶體來動態產生一個貼圖,再用二個三角型畫滿整個畫面就行了。測試結果也順利通過了。

再來就是使用imgp來實作good::gx模組裡的Graphics和Image介面。這二個介面非常的簡單,主要就是實作Graphics的drawImage和fillSolidColor二個函式。透過imgp三二下簡單作個包裝,這樣good就俱備繪圖能力了。

最後就是正式來。把good加到專案,作了點小修改,沒有花很大的功夫就可以在模擬器畫面上看到動起來的鋤草機!畫面上有東西後,再補上輸入的部份就完成了初步的移植。

2009年4月26日 星期日

色塊背景物件

新增了一個純色塊背景物件類別。底下是一個簡單的測試抓圖。


為了提供這個功能,有幾個地方需要跟著作修改。首先當然是核心部份要加上新的物件定義,再來是編輯器部份要可以作新增,最後是執行時期要可以畫出這樣的物件,以及新增幾條Script呼叫。


核心的支援

這部份其實也沒什麼,主要就是在原本的物件類別上新增一個色塊背景的子類別項目。同時還需要新增二個屬性來支援這個新類別,分別是物件的大小以及背景顏色。

物件大小是一個有四個數字欄位的複合屬性,分別是左上位置以及寬高,目前只用到其中的寬高二個屬性。


編輯器的修改

編輯器裡主要需要作修改的是關卡編輯器。在工具列上新增一個鈕,用來在關卡裡新增一個色塊背景。


可以加入後,還要在View裡把它畫出來。除了畫出來之外,還要處理它的HitTest、PropView的支援等等。


執行時期

執行時期的支援最單純,只是作畫而已。


新增Script呼叫

對應新物件,新增了四條Script API。分別是Good.GetDim、Good.SetDim、Good.GetBgColor及Good.SetBgColor。

SetDim及GetDim是用來存取物件的範圍屬性,目前只使用到寬高二個值來決定色塊的大小。SetBgColor及GetBgColor則是用來存取色塊顏色值。

另外Good.GenObj的定義也稍作調整。GenObj的第二個參數如果是指定一個小於等於0的值,也就是一個無效的值,則表示要建立一個色塊背景物件。


範例

大概說明一下上面看到的抓圖範例的程式。首先在關卡初始化時建立一堆測試物件。
math.randomseed(os.clock())
for i = 1,2000 do
  local cb = Good.GenObj(-1, 0, 'TestBlock')
end
這段Lua程式碼用來建立2000個用TestBlock控制行為的色塊物件(因為第二個參數不指定資源所以自動成為色塊物件)。

TestBlock定義如下。
TestBlock = {}

TestBlock.OnCreate = function(param)
  if (math.random(2) == 1) then
    param.dirx = -1 * math.random(1,5)
  else
    param.dirx = 1 * math.random(1,5)
  end
  if (math.random(2) == 1) then
    param.diry = -1 * math.random(1,5)
  else
    param.diry = 1 * math.random(1,5)
  end
  local id = param._id;
  Good.SetBgColor(id, math.random(65535))
  Good.SetDim(id, 0, 0, math.random(32,128), math.random(32,128))
  Good.SetPos(id, math.random(0,600), math.random(0,440))
end

TestBlock.OnStep = function(param)
  local id = param._id;
  local x,y = Good.GetPos(id)
  x = x + param.dirx
  y = y + param.diry
  Good.SetPos(id, x, y)
  if (600 <= x or 0 >= x) then
    param.dirx = -1 * param.dirx
  end
  if (440 <= y or 0 >= y) then
    param.diry = -1 * param.diry
  end
end

2009年4月25日 星期六

About smallworld2

smallworld2是個人設計實作的遊戲程式庫,因為最早是針對MMOG伺服器端的需求作開發,所以主要特點是系統底層及網路功能的支持。以C++語言實作,CppUnitTest作單元測試,具跨平台能力。

下圖是smallworld2的系統方塊圖,每一個方塊都是一個可以獨立使用的模組,愈上面的模組表示愈高階,同時也表示它還使用到底下模組所提供的功能。例如Network模組使用到了ObjectPool、BitStream及Socket模組來實作出高階的網路功能,而Smallworld模組則再架構在Network模組之上,建構出更高階抽象的功能。


smallworld2提供的功能比較偏底層資料結構,唯一和成像比較有直接關係的就只有Widget,這是一個GUI模組,不過要怎麼畫也是要根據繪圖平台自行實作,所以它也是和2D/3D等無關。而區域空間資料結構(Cells)則屬輔助性的資料結構,算間接和成像有點關係。

這個程式庫發展到現在還一直保持在一個精簡的規模,這也是因為我的設計思維比較偏極限編程(eXtreme Programming)的關係吧。我總是不會過早加入功能,只保持在可以工作的最簡單的規模。其它的需求假如找的到合適的現成資源,則儘量用整合的方式利用而避免重新發明。

話雖是這樣講,不過有時還是免不了犯了很多程式常犯的錯誤,這也是需要時時刻刻警惕自己的事!

2009年4月22日 星期三

About 25940p

25940p其實是我自己蠻喜歡的一個小作品,雖然說似乎不怎麼受歡迎的樣子。因為我原來就特別喜歡縱向捲軸射擊遊戲的緣故,才作了相關的研發作了這個作品。話雖說是小品,不過我後來實在退步太多了,自己也很難過關。


這個小遊戲花了我大約一個星期的時間製作,主要的時間都花在關卡編輯上。整個遊戲都是用script編輯出來的,所謂的整個,除了彈幕外、敵機什麼時候出來怎麼移動、粒子特效、選單、背景多重星空等等,全都是用script編輯出來的。所以其實說花了一星期製作是假的,我花了更多時間在底層的開發上,為了這個小遊戲我花了幾個月時間,設計開發了專門用來製作射擊遊戲的stge語言和虛擬機器。

本來我作小遊戲花的時間並不多,不過從鋤草機開始,我已經開始有那種花很多時間來製作一款可以在很短時間內破關的小遊戲的傾向。比如說鋤草機大概花一個月時間製作,但15分鐘可以過關。25940p也差不多15至20分鐘可以全破,但斷斷續續至少用半年以上時間研發製作。

+ + +

為了開發一套專門給射擊遊戲使用的script,我花了很多時間在設計上。完成後,首先是使用C++實作虛擬機器的部份,因為這樣至少可以先進入測試階段。虛擬機器的核心部份以smallworld2的ObjectPool模組為中心管理所有資源,把所有東西都當作粒子作為基本概念,不論是子彈或是敵機等等都用同樣的方式對待。然後所有粒子都能掛上一個可以控制它動作的緒,再加上一些和射擊相關的屬性支援,就可以運作動起來了。

Parser的部份一開始原本是想使用Lex/Yacc來作製作,但後來決定使用Boost::Spirit,因為沒碰過可以順便學習新東西,結果還出乎意料外的容易使用,缺點是Complie時間大增。接著再用WTL寫個簡單的工具程式,可以直接在上面編寫script並以視覺化的方式立即檢視執行結果。

最後才是撰寫遊戲程式本身。基本上遊戲本身的主要工作就是畫圖,把所有粒子根據遊戲自己的定義畫出來。子彈粒子就畫成子彈,爆炸碎片粒子就畫成爆炸碎片等等。成像部份使用OpenGL來作,因為也是順便學學OpenGL。

剛好因為成像是用OpenGL的緣故,所以在有支援OpenGL的平台上要作Porting就容易了。這也是為什麼我可以很容易的把25940p移植到PSP和iPhone上。因為除了成像,還有像是IO等和系統比較有直接關係的部份外,其餘部份在實作時都會考量到跨平台,所以一般來說都可以不作任何修改就直接通過編譯。

在製作good時也有考慮到把stge整合進來,這樣就能編出更有趣的東西來。只不過這部份我也還沒考慮清楚,短期內還沒辨法執行。


故事大概就是這個樣子~

2009年4月19日 星期日

如何在 Objective-C 中使用 C/C++ 的程式碼

這幾天在玩Mac。按照慣例,每次遇到新平台都會先透過HelloWorld範例作個初步入門。這個步驟花了我一個早上的時間,實際上大部份的時間都在學習Objective-C的語法。

第一眼看到Objective-C的時候,感覺實在很彆扭,不過透過網路上找到的資料,雖然不致於說己經會寫Objective-C的程式,但至少也看的懂它的語法。

有了基本知識後,接下來當然是直接進入Porting的動作。Porting的項目我選了25940p,因為這個小遊戲的成像是使用OpenGL,而且也有成功移植到PSP的經驗,所以感覺上應該也可以很容易的移植到iPhone上去。

結果想不到一開始卡關了差不多三個小時左右,全卡在不知道怎麼從Objective-C裡叫用我原來寫的C++程式碼(25940p是用C++寫成的)。可是我看到的Sample裡,明明混用了C和C++的程式碼,問題不知道出在那邊。

就這樣花了大概三小時才摸出一個所以然來。知道問題出在那裡後,很快的就把25940p的碼再加上使用到的部份smallworld2的碼在一行不改的情況下整合進去,接下來的就只有剩下成像和輸入的部份需要作點調整。

+ + +

其實要解決我遇到的問題非常簡單,重點就是 Objective-C不能直使用C++的碼,但可以直接使用C的碼。所以解決辨法很明顯了,使用間接的方法來使用C++的碼就行了。從Objective-C使用C,再透過C來使用C++就能達到目的。

底下用個十分簡單的例子來表達這個概念。

test.h裡定義了些東西,比如說有個Test類別。
...
class Test
{
...
};
...
接著在你的Objective-C定義檔main.m裡,無論是用#import "test.h"或#include "test.h"都會產生錯誤。我們需要另外產生一個.h,再定義一些純C的API,透過這些純C的API來使用test.h裡的東西,例如 Test類別。

如下,定義個test_wrap.h。
...
void UseTest1();
void UseTest2();
...
並且實作這些API,然後在main.m裡import或include test_wrap.h並呼叫純C的API,這樣就不會有什麼問題了。

2009年4月14日 星期二

薩爾達傳說:夢見島資源

薩爾達傳說:夢見島的地圖是由160乘128個Tile所組成,每個畫面大小是10乘8個Tile,也就是說完整的地圖是由16乘16共256個畫面組成。每次當林克移動到畫面邊緣的時候,整個畫面會往林克前進的方向捲動,一次捲動一整個畫面。

地圖的每個Tile大小是16乘16Pixel。下面是我千辛萬苦才找到的Tileset,實在是太感謝提供這張圖的善心人士。找到這張圖的時候,差點就沒哭出來。


有了Tileset之後就可以開始編輯地圖了,所以我又繼續上網尋找遊戲地圖,最後好不容易也讓我找到了。


參考這張全圖再加上Tileset就可以使用地圖編輯器"慢慢"畫了。作這事真的是需要些耐心,好在我是邊開發編輯器邊慢慢編輯地圖,還不會覺得太難受。最後的結果是完成了大約90%以上的地圖,因為有些部份少了Tile,有些部份不知怎麼編輯,所以就暫時先這樣。

+ + +

除了地圖Tileset之外,我也收集到一些Sprite,只不過這些Sprite資源沒辨法很簡單的直接使用,主要原因是Tileset裡面的元素不像地圖Tileset每一格都是對齊的,像是下面這張圖。

不過還好我還沒有真的要把夢見島完整的實作出來,只作了個小小的測試Demo,自己用小畫家拆幾塊出來拼倒是還好。

下次再來看看怎麼實作這個Demo。

2009年4月10日 星期五

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

2009年4月8日 星期三

最早的測試地圖

最早最早good在開發的時候是先從地圖編輯器開始的,所以需要編個東西作測試,不過當時拿來作測試用的地圖並不是鋤草機的地圖,而是薩爾達傳說:夢見島的地圖。

夢見島是我最愛的遊戲之一,本來除了想編輯鋤草機的一個關卡之外,還有另一個希望可以達成的目標,那就是我也想要使用good來製作夢見島。會選擇夢見島除了說這是我的最愛之外,另一個主要原因是我沒有美術支援,只好上網找現成的資源,也剛好讓我找到夢見島的一些圖形資源,所以很快的就決定是它了。

只不過後來目標又稍微改變了一下,變成先不要完整實作夢見島,改成實作一個使用夢見島的地圖還有武器系統的小型MMORPG,可以讓大家在地圖上拿著各種武器亂鬥,只不過這個計劃目前也還遙遙無期中。


至少我也好不容易把夢見島的世界地圖編的差不多了,有圖為證。另外也用Script寫了一個超小Demo,這個Demo下次再來作介紹。

2009年4月3日 星期五

good程式設計:資源 vs 物件

資源和物件(指RunTime物件)的差別,以最簡單的方法作解釋的話,可以這樣子說。所有在編輯器裡面編輯的東西都叫作資源,而所有在你點擊了Play後執行起來的遊戲的畫面上看到(包含看不到)的東西都叫作物件。

關於資源的部份用這樣子作解釋應該不會有什麼問題,只有一個小地方要再注意一下。在關卡資源裡擺設的東西,我們也叫作物件(關卡物件)。不過這點也不會有太大衝突,只要記住一點,因為我們是在編輯器裡編輯這個關卡,所以這關卡是個資源,而關卡裡的東西也同樣都是資源。

資源的類別主要有四類:貼圖、地圖、精靈和關卡。

+ + +

至於在RunTime時期,所有東西都是物件這件事應該也不難理解。既然說所有東西都是物件,自然也包含了關卡本身。

RunTime物件會在建立時根據它所參考資料類別作初始化。例如說參考了一個精靈資源來建立一個物件,RunTime就會建立出一個精靈物件給你。而參考了一個關卡物件,RunTime就會建立一個關卡物件給你,當然也連帶的將關卡底下的所有子物件一起建立起來。

物件的類別對應到資源的類別,主要也有四種:貼圖物件、地圖物件、精靈物件和關卡物件。

+ + +

最後再補充一點。在RunTime時期關卡物件只能有唯一個,而其它類別的物件則數量不定,且它們的Parent都是那個唯一的關卡物件。

2009年4月1日 星期三

good程式設計:ID

在進入good的程式設計之前,有一些基本的觀念有必要先了解,否則在實作GamePlay時遇到了某些問題,可能會莫名其妙而且陷入一直找不到問題所在的困境。

這篇文章內容要介紹在good裡面非常重要的ID的概念。

所有東西都有ID

在前面編輯器的簡單介紹裡面可以發現到,所有資源不論是什麼樣的種類,在屬性檢視器裡面都可以看到有個ID屬性。除此之外,在關卡資源裡的所有物件也都擁有自己的ID。(ID是個數字)

每一個ID都是唯一

所有使用到的ID,無論是資源項目的ID或者是物件的ID,每一個都是獨一無二的。也就是說如果有個精靈資源的ID是12,那麼12這個ID就會只有一個,不會再有另一個資源或是物件的ID也是12。

ID可以重覆使用

good裡所使用的ID都是由編輯器或是RunTime維護及分配使用,當一個資源或物件被刪除時,這個資源或物件原來使用的ID就會被回收。而被回收的ID並不一定會被立即重覆使用,有可能在你作了幾次新增動作之後才發現到,新增的資源或物件被分配到一個被回收的ID。當然也有可能在你刪除了一個資源或物件之後,被回收的ID立刻就被重覆使用在下一個新增的資源或物件上。

以上這三點是對於good編輯器在ID上的使用需要了解的基本知識。

+++

那麼RunTime呢?

當你在編輯器工具列上點擊Play鈕時,good RunTime會載入第一個關卡資源,根據第一個關卡資源的內容,建立起所有的物件,這些物件包含貼圖物件、地圖物件和精靈物件等。good RunTime會根據在關卡編輯器裡所編輯的物件次序和類別以及其相關屬性,一個一個把這些物件建立起來。

其中最重要的是,當我們使用關卡編輯器作編輯時,關卡中的每一個物件都會自動分配到一個ID。而RunTime在建立這些物件時,同樣的也會分配一模一樣的ID給這些RunTime物件。

因為在執行過程中的ID使用原則一樣遵守上面所列的三項原則,所以動態刪除的物件的ID會被回收,而動態產生出來的新物件的ID也可能會是重覆使用舊ID。


Related Posts Plugin for WordPress, Blogger...