2009年12月31日 星期四

good基礎教學

這篇文章要來一步步製作一個彈跳球的範例,完成後相信對good會有一個基本的認識。

;

1, 首先打開good編輯器,這時專案是空白的,按下Ctrl+S將空白專案儲存為bounceball.txt。

2, 接著在工具列上點擊NewTexture或選單Project->NewTexture加入用來作為彈跳球的face.png。

會使用png格式的圖檔的目的是,因為我們這張圖會用到鏤空效果,png格式可以帶alpha,我們可以很容易利用png的alpha來作alpha鏤空。

* 所有會使用到的圖形,我們都需要以加入貼圖的方式事先加入到good專案內才能夠使用。

3, 再用同樣的方式加入一張準備用來作為背景的圖形lace_0103.gif。


到此先按Ctrl+S存檔。(隨時存檔是個需要養成的好習慣)

4, 接下來在工具列上點擊NewSprite或選單Project->NewSprite加入一個精靈。一般在遊戲中,在畫面上會動的人物怪物等等我們把它叫作精靈(Sprite)物件,以我們要製作的範例來說,我們現在要建立一個表示彈跳球的精靈。


我們前面共加入了二張圖,要用來作為彈跳球的圖是tex1,這個名稱我們也可以在加入圖形時自己命名。這張圖的大小是107x107,所以我們在TileWidth/Height上填入了107和107,表示要使用整張圖。同時我們在name欄位自己填入了ball這個名字,當然也可以像加入圖形時保留空白,讓編輯器為我們自動加上名字。


完成後要加入一個空白的精靈,我們需要加入影格才能在畫面上顯示東西。因為我們已經事先設定好要使用整個圖形的大小,所以可以看到在右下角的貼圖檢視區上選取了整張的圖(透明紅色),接著按下貼圖上方的新增影格鈕加入影格。這樣就完成了精靈的設定,再按下Ctrl+S存檔。

5, 接下來在工具列上點擊NewLevel或選單Project->NewLevel新增一個空白關卡,準備來建立我們的遊戲場景。

good的關卡是我們實際執行遊戲時會呈現在畫面上的東西,我們可以建立很多不同的關卡,但同一時間只會有一個正在執行。我們可以在關卡上擺放各種物件,像是背景地圖或精靈等等,這些物件在關卡執行時都會顯示在畫面上。

首先我們加入一個背景圖,在關卡編輯器上方的工具列點擊NewTextureBg選擇tex2也就是我們事先加入的背景貼圖,完成後可以在畫面上看到我們加入的物件。因為這只是一張小圖,我們需要它填滿整個畫面,所以我們在左下角的屬性編輯器內,將背景物件的RepeatX和RepeatY設為True,這樣在執行的時候貼圖物件就會以鋪地磚的方式在水平及垂直方向重覆的貼滿整個畫面。


接下來按下Ctrl+S存檔。現在這個遊戲已經可以執行了!點擊工具列上的紅色驚嘆號Play或選單Project->Play或快速鍵F5,直接執行看看結果如何。執行後,按ESC可以再回到編輯器繼續編輯。

6, 接著在關卡編輯器的工具列上點擊NewObject,選擇我們剛剛建立好的精靈,然後在關卡上隨便一個位置點一下,放入一個精靈物件。同樣存檔後按下F5執行看看結果,可以看到畫面上和編輯器一樣在同樣位置多了一個我們加入的彈跳球(笑臉)。

在進行下一步前,我們先點擊關卡編輯器上的精靈物件,在屬性編輯器上的Name欄位我們填入ball這個名字,這個名字在後面我們會使用到。

7, 現在我們要準備開始撰寫程式碼讓彈跳球動起來。在開始之前,我們先在工具列上點擊NewScript或選單Project->NewScript加入lua script檔,檔名叫作bounceball.lua。

在開始撰寫程式碼之前再回到關卡編輯器,在關卡的屬性Script欄位填入Level,這個名稱是接下來即將用來控制關卡的運作邏輯的script名稱。

8, good使用的是Lua語言,所有遊戲邏輯都是以Lua語言撰寫。基本概念是,我們先在編輯器內編輯好所有會使用到的各種資源,像是地圖精靈等,再以Lua script來操控這些資源物件等,完成遊戲邏輯。

現在再回到bounceball.lua要正式寫程式了,先寫下如下的程式碼。
Level = {}

Level.OnCreate = function(param)
end

Level.OnStep = function(param)
end


上面的Level就是我們填在關卡的Script欄位的名字,假如現在我們把這個名字換掉了,關卡的Script欄位也必需跟著換,否則執行時就會找不到script,東西就動不了了。

OnCreate和OnStep是掛在Level這支Script下面的事件處理函式,這是good預先定義的名稱,只要指定的Script有定義,就會在適當的時機自動執行。

當物件被建立時OnCreate會被呼叫,且只會呼叫一次,而OnStep在每次遊戲迴圈執行時都會呼叫一次。呼叫的頻率是根據FrameRate,目前是固定在60FPS,也就是OnStep每秒會被執行60次。

* param參數是關聯到物件的一個Lua table,除了它在建立時會預先填入一個代表物件的id(_id欄位)外,要怎麼使用自由應用。

* 你可以發現到在關卡編輯器內的所有物件都有Script欄位,這表示說每一個物件都可以由各別的一個Script控制。

9, 現在先把bounceball.lua的OnCreate改成如下的程式碼。
Level.OnCreate = function(param)
  param.ball = Good.FindChild(param._id, 'ball')
  param.speedx = 4
  param.speedy = 4
end


首先我們前面提過,param參數除了_id是自動設定好的關聯到關卡物件的id外,其餘的就看我們如何應用。如上,我們在param加入了speedx和speedy二個欄位,用來表示彈跳球的水平及垂直的移動速度。而ball欄位我們使用Good.FindChild這條API來找到我們在關卡編輯器裡加入的彈跳球物件,我們是拿物件的名稱來搜尋的,這就是為什麼我們在前面要將它命名為ball,當然我們也可以使用其它名稱,只要可以用來搜尋到物件即可。

Good.FindChild這條API的回傳值是個數字,表示物件的Id。在good裡面,所有的東西都有Id,而且每個Id都是獨一無二的,我們透過Id來操作物件或資源。

* 有點要注意的是,在編輯器裡填的名字並沒有保證是唯一的,所以有可能有多個物件使用相同名稱,這時Good.FindChild只會回傳第一個找到符合的物件Id。

10, 接著把bounceball.lua的OnStep改成如下的程式碼。
Level.OnStep = function(param)
  local l,t,w,h = Good.GetDim(param.ball)

  local x,y = Good.GetPos(param.ball)
  x = x + param.speedx
  y = y + param.speedy

  if (0 > x) then
    x = 0
    param.speedx = -param.speedx
  elseif (640 - w <= x) then
    x = 640 - w
    param.speedx = -param.speedx
  end
  if (0 > y) then
    y = 0
    param.speedy = -param.speedy
  elseif (480 - h <= y) then
    y = 480 - h
    param.speedy = -param.speedy
  end
  Good.SetPos(param.ball, x,y)
end


一開始我們以Good.GetDim取得彈跳球的大小(我們已在OnStep把彈跳球的Id存在param.ball),Good.GetDim會回傳4個數字,我們只會使用到後面二個數字,表示物件的寛及高,這個數值我們會用來計算是否和邊界碰撞。 接著我們以Good.GetPos取得目前彈跳球所在的座標(座標的原點和一般的銀幕座標系一樣是在左上角),然後加上水平及垂直的速度,計算出下一個移動座標值。 在以Good.SetPos設定彈跳球的新座標前,我們還需要檢查並修正新的座標值避免它跑到畫面外去,同時也根據狀狀更新一下移動速度,每次碰壁時就要讓它向移動。 以上,我們完成了簡單的彈跳球範例。存檔後按下F5執行,可以看到我們的彈跳球在畫面上彈來彈去的。你也可以試著多加幾顆球進去,作些實驗看看,相信這會幫助你更快熟悉good。 ;

下載範例

2009年12月28日 星期一

Stge基礎教學

這篇文章作為stge script的入門教學,會告訴你如何撰寫stge script來描述簡單的彈幕效果並整合到good裡,透過good的顯示功能呈現到畫面上。要補充說明的是,雖然stge script最初是為了射擊遊戲而設計的,但因為它也有基本足夠的彈性,所以也能夠作為粒子效果來應用。

;

1, 首先打開good Game Editor,按下Ctrl+S儲存空白專案,命名為StgeTest1.txt。
2, 接著在工具列上點擊新增空白關卡(New Level),或點擊選單Project->New Level...加入空白關卡。
3, 在新關卡(level1)的屬性檢視器上的ClearColor,開啟顏色選擇對話盒並設定成黑色。
4, 在工具列上點擊新增空白腳本(New Script),或點擊選單Project->New Script...加入空白腳本,檔案名稱填StgeTest1.lua。
5, 在資源樹上點擊level1回到空白關卡1的屬性檢視器,在Script欄位上填入Level。
6, 按下Ctrl+S儲存檔案。

現在已建立基本的資源,接下來全部使用script來建立我們要的功能。首先撰寫一些good script作出基本框架。
Level= {}

Level.OnCreate = function(param)
  Stge.RunScript('StgeTest1')
end

Level.OnNewParticle = function(param, particle)
  local obj = Good.GenObj(-1, -1)
  Good.SetDim(obj, 0,0, 3, 3)
  Good.SetBgColor(obj, 0xffff0000)
  Stge.BindParticle(particle, obj)
end

Level.OnKillParticle = function(param, particle)
  Good.KillObj(Stge.GetParticleBind(particle))
end


如上,我們有個叫作Level的table,並加入了三個空白的function,這三個function是good預先定義的event。OnCreate是當物件建立時會被呼叫,並且只會被呼叫一次,我們可以在這個event裡作些初始化相關工作。如上面的例子所示,我們在OnCreate事件中起動了一個叫作StgeTest1的stge script。

OnNewParticle和OnKillParticl才是和Stge有關的event。當有個新的粒子被建立出來時會觸發此事件,我們可以利用這個事件把stge粒子和good物件綁定(Bind),讓stge粒子來控制畫面上顯示的good物件。相反的當粒子要釋放時會觸發OnKillParticle,這時我們再把產生出來的good物件也一起釋放掉。

在上面的例子中,我們在OnNewParticle事件發生時,同時建立一個大小是8x8的紅色色塊,並把這個物件和粒子作綁定。而在OnKillParticle事件觸發時,再透過GetParticleBind取得所綁定的物件並把它一起Kill掉。

7, 接著在工具列上點擊新增空白stge腳本(New Particle),或點擊選單Project->New Particle...加入空白stge腳本,檔案名稱填StgeTest1.stge,接著在StgeTest1.stge文建內寫下如下的script。
script StgeTest1
  fire()
end


儲檔後可以按下工具列上的Play或F5執行看看,應該會顯示一個全黑的畫面,而畫面正中央有個紅色方塊。我們已發射了一枚粒子出來,並如我們所設定的和一個紅色色塊綁定,只不過它還不會動!

現在按下ESC退出程式,現在我們要加點程式讓粒子動起來。

;

首先要先說明一下需要注意的地方。good的座標系原點是在左上角,而stge的座標系原點在正中央,這就是為什麼這個粒子會在畫面正中央。同時good座標系和一般的銀幕座標系一樣往右往下是正的,而stge座標系是往右往上是正的,和一般我們在學習數學時的笛卡兒作標系定義相同。

;

stge script都是以一個script關鍵字開始,接著一個名稱,最後再以一個end關鍵字結尾,和lua的function有點類似。你可以寫很多個script,然後再用Stge.RunScript以script名稱來個別執行它。

fire指令是用來發射一枚粒子的。因為我們還沒有指定速度給它,預設速度是0,所以它才維持在畫面正中不動。現在我們要把它加上速度,同時也給它一個方向。
script StgeTest1
  direction(0)
  speed(120)
  fire()
end


direction是用來設定方向的,單位是度。0度是x軸的正方向也就是正右,90度是正下,所以角度是順時針方向。有效值是0-360間,比360大或比0小時會自動調整。方向的預設值也是0,所以direction(0)也可以省略不寫。speed則是用來設定移動速度,單位是每秒移動的pixel量。

現在再執行程式,可以看到這個粒子往右動了起來了!

;

如果要發射二枚粒子,只要呼叫二次fire指令就行了,如下所示。
script StgeTest1
  direction(0)
  speed(120)
  fire()
  direction(45)
  fire()
end


這次同時發射了二枚粒子,一個朝0度方向飛去,另一個朝45度方向飛去。

在上面的例子裡注意到,我在第二次射擊時沒有再以speed設定一次速度,因為只要設定過的方向或速度,在下次改變時會一直保留狀態,所以我們不必每次都作設定。

;

現在有個問題了,如果要一定射擊多發子彈,那就要呼叫很多次fire才行,這不是一個好辨法。所以我們需要迴圈,如下所示。
script StgeTest1
  speed(120)
  repeat(12)
    direction(30, add)
    fire()
  end
end


執行上面的範例,可以看到朝四面八方發射了一個有12個粒子的環。

repeat就是我們使用的迴圈指令,它最後需要以一個end關鍵字作結束。在這個區塊裡的指令,會重覆執行repeat所指定的數量。而direction的add參數則是指定說,以上一次的設定值,累加30度的意思,所以每一次執行會以間隔30度的角度發射一枚粒子出去。

接下來我們對程式作一點點如下的小修改,再執行看看。
script StgeTest1
  speed(120)
  repeat(-1)
    direction(30, add)
    fire()
    sleep(0.1)
 end
end


我們可以看到畫面上,不斷的以螺旋的方式發射出粒子。

把repeat的執行數量改成-1,就表示為無窮迴圈的意思,所以它才會不停的發射粒子。而我們在fire之後插入了一條sleep指令,這是用來暫停執行的指令,暫停的時間我們指定為0.1秒。所以執行結果會變成,每次間隔0.1秒會射擊一枚粒子,並一直不斷的重覆這個動作直到結束。

;

接著我們要來製作子母彈,程式改成如下。
script StgeTest1
  speed(120)
  repeat(-1)
    direction(30, add)
    fire(sub1)
    sleep(0.1)
 end
end

script sub1
  sleep(rand(2,4))
  speed(120)
  repeat(10)
    direction(36, add)
    fire()
  end
end


注意我們在StgeTest1裡的fire指令指定了一個叫sub1的參數,sub1又是另一個script,這表示說我們發射出來的粒子會和sub1綁定,由sub1來控制。而因為sub1又是另一個script,因此StgeTest1能作的事sub1也能作到,也就是說被發射出來粒子還能夠發射粒子!

sub1的執行的動作比較特別的是一開始的sleep指令,我給它一個rand(2,4)參數,透過rand指令我可以得到一個2-4間的亂數,這樣就能作出一些變化。rand指令除了有rand(a,b)的形式外,也還能用rand()產生0-1間的亂數,或rand(a)產生0-a間的亂數。

sub1暫停一小段時間後,會炸開一個環。同時最後一條clear指令會把自己給刪除,這樣畫面上就不會那麼礙眼。

;

底下要介紹的是userdata指令,讓我們可以傳遞一些資料給good,讓good根據這些資料產生不同的變化。
script StgeTest1
  speed(120)
  repeat(-1)
    direction(30, add)
    fire(sub1)
    sleep(0.1)
 end
end

script sub1
  sleep(rand(2,4))
  speed(120)
  userdata(1)
  repeat(10)
    direction(36, add)
    fire()
  end
end


如上,我們只在sub1裡加入一條userdata指令,參數傳入1。userdata最多可以傳遞4個參數,這裡我們只用了一個。那麼我們要怎麼應用?

現在我們回到StgeTest1.lua的Level.OnNewParticle事件,修改成如下程式碼。
Level.OnNewParticle = function(param, particle)
  local u1 = Stge.GetUserData(particle, 0)
  local obj = Good.GenObj(-1, -1)
  Good.SetDim(obj, 0,0, 8, 8)
  if (1 == u1) then
    Good.SetBgColor(obj, 0xff00ff00)
  else
    Good.SetBgColor(obj, 0xffff0000)
  end
  Stge.BindParticle(particle, obj)
end


如下所示,我們在一開始時透過Stge.GetUserData指令取得第一個參數(從0開始),接著我們在設定色塊顏色時用這個參數來決定要用什麼顏色,在此例中,我們檢查如果是1就用綠色,否則就用紅色。所以執行結果,我們可以看到炸開的環都是綠色子彈!在實際應用上,我們可以自行利用這4個參數來作更多的變化。例如定義成粒子類形、hp、屬性等等。

;

現在再回到StgeTest1.stge,我們再修改一下原來的程式,如下所示。
script StgeTest1
  speed(120)
  repeat(-1)
    direction(30, add)
    fire(sub1, rand(2,4), 1)
    sleep(0.1)
  end
end

script sub1
  sleep($1)
  speed(120)
  userdata($2)
  repeat(10)
    direction(36, add)
    fire()
  end
  clear()
end


在這個範例裡面,我們把sub1要暫停的時間,和要設定的userdata參數化了,由在StgeTest1的fire指令參數傳遞過來給sub1(fire指令總共可以傳遞最多4個參數)。在sub1裡,就以$1及$2來取用由上一層傳遞過來的參數。透過這個方法,增加了不少彈性,可以作出更多的變化來。

;

範例程式下載

2009年12月24日 星期四

任意類型物件混色的簡單應用

新增了可以疊加顏色層到任意類型的物件後,就可以作更多應用變化。


如圖,我可以只畫一種子彈,但利用色層就能變化出多種顏色的子彈來。簡單,又省圖。

2009年12月19日 星期六

關於C/C++的指標

我想應該還有不少人在使用指標上有些地方觀念不大清楚,比如說下面二個函式,那個是正確的?為什麼?像這樣的問題如果弄不清楚,寫出來的程式一定非常危險。
// 為簡化忽略檢查
void alloc_mem(char* p) // 版本1
{
  p = new char[100];
}

void alloc_mem(char** p) // 版本2
{
  *p = new char[100];
}
如上,這個函式要配置大小是100個字元的記憶體並從傳入的參數p回傳,這二個版本除了輸入參數不一樣外大致上是一樣的;從第一個版本來看,參數是一個字元指標,記憶體配置出之後直接傳給p,如果觀念正確的人一定可以馬上指出這樣的寫法是錯誤的,第二個版本才是正確能work的。

現在就來說明為什麼,在這之前先要了解在C/C++中,函式的參數是如何傳遞的,在C/C++中函式的呼叫所傳入的參數是透過堆疊(Stack) 來傳入函式的,不懂什麼是堆疊也沒關係,就把它看成是另外一塊記憶體也行,當在程式中呼叫某個函式時,傳入的參數會先被複製到這塊記憶體中,當在函式中要使用這些參數時再從堆疊中去取出來。

以版本1的例子來說明,如下在程式中大概會這樣呼叫。
char* pp = NULL;
alloc_mem(pp);
pp一開始的初值是NULL,當呼叫alloc_mem時,pp的值會被複製到堆疊中(傳址),這種情況和以下的code事實上是對等的,只不過 p的值一開始被初始化成和pp的值一樣,p就好像一個區域變數一樣,一離開函式後這個變數就無效了,所以在外面的pp的值永遠都不會改變,同時在涵式中 new出來的記憶體也lost掉了。
void alloc_mem()
{
  char* p = new char[100];
}
再來看版本2,它的參數是一個指標的指標,這是什麼意思,我們先從實際使用上來看,如下。
char* pp = NULL;
alloc_mem(&pp);
這次我們把pp這個變數的位址傳入涵式,所以在涵式中所得到的是pp這個變數的位址,在函式中p所含的內容是pp的位址,pp是一個char*形態的變數,p是一個指標它的內容是char*的形態,現在p已經指向pp了,所以對p的內容作改變,相對的pp的值也會跟著改變。

現在來看另外一個類似例子,會比較清楚些。
void change_val(int* i)
{
  *i = 5;
}

int ii = 3;
change_val(&ii);
這個例子和上面是一樣的,只不過變數的形態從char*改成了int,仔細去對照比較一下,回頭再去看alloc_mem相信能更容易明白。

2009年12月6日 星期日

分散式的線上遊戲伺服器

smallworld是smallworld2網路架構的第三層(應用層)。

smallworld2網路架構分成四個階層,最底層是串流層(Stream),負責提供最基本的TCP/IP串流封裝及連線管理。第二層為封包層(Network),負責提供格式化的封包支援以及完整的斷線處理機制。第三層為應用層(Smallworld),提供動態可擴展的分散式網路架構。第四層為遊戲應用層,提供和線上遊戲一般應用邏輯相關支援。

設計smallworld應用層最大的困難在於,必須讓使用者也就是應用程式的開發者,能夠以開發單一伺服器應用的單純方式,來開發一個分散式架構的多人連線應用程式,所有複雜的細節都需由底層處理掉。smallworld建立了二個概念來達成這個目標,分別是Scope及VirtualConnection。

對於伺服器S而言,所有訊息都是透過一條Connection傳送出去的。Connection的另一個端點可能是一個Client,也可能是另一個Server。而這個端點可能是與伺服器S有實際建立連線,也可能是間接和伺服器S建立連線。假如這個Connection與伺服器S間有實體連線,則伺服器S就能直接把訊息傳送給對方,否則就以間接的方式轉送過去。無論這個Connection是直接或間接的連線,對於伺服器S來說,是不必關心的事情,底層自動會想辨法把訊息傳送給這條Connection的對應的端點上去。所以對伺服器S而言,Connection是虛擬的。

Connection的取得一律透過定義Scope作為Filter來獲得。以線上遊戲為例。當一個玩家登入遊戲後,就會有許多個Scope和他建立關係,例如這個玩家的可見視野、玩家加入的組隊、公會、聊天室、P2P交易、商店等等。這些全都是Scope,概念一樣,只是定義不同。透過定義好的Scope,再拿這個定義作為Filter由可以到達的在線上的伺服器收集符合的Connection,之後就可以對這些Connection作操作。

以上是構成smallworld的二個重要Concept。

------------------

套用smalllworld的框架就可以很容易建立可以動態擴展的線上遊戲架構,不過在實際應用上還是會有其它問題。舉個例子說明:假如我以單一伺服器的方式實作了一支Server程式,這支程式可以處理完整的虛擬世界。伺服器執行起來後如果發現登入玩家太多,伺服器負載太大時,我可以不必關機,只需要動態的再啟動新的伺服器加入服務就行了。

但如果我想改變配置,不要讓每一個Server程式都載入並處理完整的世界,我要把整個世界切分為幾塊,讓不同伺服器各別負責其中一塊,這些區塊可以完全獨立,或者也可以有重疊的區域。要如何作到?

為了實現這個功能,需要再引入一個新的概念,這個概念由smallworld2的網路第四層所提供...

2009年12月5日 星期六

很讚的遊戲編輯器論壇

終於要作第一個版本的relase了,
所以用PHPBB新建了一個簡單的論壇
開了一個主題作為編輯器第一個版本的發佈

2009年11月15日 星期日

C++ Const

作個小實驗。

void func_a(const int& a) {
  int& b = const_cast<int&>(a);
  b = 200;
  cout << a << ", " << &a << endl;
}

int main() {
  const int a = 100;
  cout << a << ", " << &a << endl;
  func_a(a);
  cout << a << ", " << &a << endl;
}

-------------------------------------------------
100, 0012FED4
200, 0012FED4
100, 0012FED4
-------------------------------------------------

可以看到,在func_a內,明明a的值已經被改成200了(可以由變數位址確認)。為了確認這一點,加上了第四個cout,如下。

cout << *(int*)&a << ", " << &a << endl;

-------------------------------------------------
100, 0012FED4
200, 0012FED4
100, 0012FED4
200, 0012FED4
-------------------------------------------------

可以看到第四個cout果然印出a的內容已改變為200。但是第三個cout卻是印出原來的值100,這是為什麼?答案在將程式反組譯,從組合語言的層面來看就很明顯了。

cout << a << ", " << &a << endl;
0041E6B2 push offset std::endl (41C528h)
0041E6B7 lea eax,[a]
0041E6BA push eax
0041E6BB push offset string ", " (4570C8h)
0041E6C0 push 64h
0041E6C2 mov ecx,offset std::cout (460888h)
0041E6C7 call std::basic_ostream<char,std::char_traits<char> >::operator<< (41C663h)
0041E6CC push eax
0041E6CD call std::operator<<<std::char_traits<char> > (41CB8Bh)
0041E6D2 add esp,8
0041E6D5 mov ecx,eax
0041E6D7 call std::basic_ostream
0041E6DC mov ecx,eax
0041E6DE call std::basic_ostream<char,std::char_traits<char> >::operator<< (41CBB8h)

注意紅色那行指令。編譯器直接把100(64h)這個常數值push進堆疊,而不再從常數變數a裡取出值來。這就是為什麼在func_a內,a的值已被改變,最後卻還印出原來的值的原因。

2009年11月10日 星期二

背景重覆模式

新增了一個類似CSS的background repeat功能,可以在關卡裡指定貼圖背景及地圖背景水平及垂直方向的重覆模式。有了這個功能之後,背景就可以使用簡單的磚塊圖片重覆鋪滿,或者也能很容易的實作出多重背景捲動。底下的抓圖是使用repeat-x功能作出多重背景捲動。


總共使用了三張底圖,全景底圖、白雲及太陽寶寶,全部設定成水平重覆貼圖。程式執行後,三張圖各以不同的速度去改變自己的水平座標,結果就在畫面上呈現出三層的背景捲動。

2009年11月7日 星期六

Script Editor

結果又把Script Editor的Syntax Color功能拿掉了,因為我把Editor本身用到的Rich Edit換回一般的Edit,所以也就沒辨法支援顏色。原因是加入了完整的Edit Hotkey支援,例如Ctrl+C/Ctrl+X等等。為了省麻煩,減少複雜度所以才換回一般的Edit元件。

除此之外直接在Script Edit內作的改變終於也能夠存檔了。因為原本存檔的定義是針對專案,現在再加上也可對程式原始碼檔作存檔,所以在操作的定義上需要再多作一點判定,同時在專案切換或程式關閉時也需要多加對變動檔案需存檔的提醒。

目前還缺Find/Replace及GotoLine的功能需要補充。

2009年11月5日 星期四

stge粒子系統介面修正

stge粒子系統介面部份作了重新設計,現在能夠更有彈性的將stge產生的粒子bind到任意的good物件。

主要的改良點在於新增了二個event。

1. 當有新粒子產生時可以收到一個通知,此時可以獲得這個新粒子的id,透過這個id可以取得粒子的user定義參數,利用把這個參數應用為粒子型別,我們就能夠根據型別產生不同類型的物件,再把這個物件和這個粒子關聯起來。

2. 當粒子即將被刪除時也能得到一個通知,此時我們就可以解除粒子和物件的關聯,或者連同物件一起刪除。

2009年10月26日 星期一

自動解壓縮程式原理

許多朋友來信詢問關於編輯器CreateExecutable的運作原理,所以這篇文章就來講講背後的技術原理。

底下提供一個簡單的方法,也是編輯器使用的方法,而這個方法只能適用在Windows作業系統,因為它使用了Windows系統的資源機制。Windows提供了一系列的API可以讓你對PE執行檔內的程式資源作存取。利用這點,只要我們作點變化稍加利用就能夠達到我們的需求。

+ + +

先說明good編輯器產生獨立的單一可執行檔的操作過程。

1,開啟舊專案
2,執行選單Project->Create Executable
3,選擇輸出目標
4,按下儲存

整個動作完成後,會產生一個不必編輯器就可以獨立執行遊戲執行檔。其實這個動作就相當於,編輯器會幫你產生一個內建了遊戲資源的播放器(Player)。

那麼這個播放器是從那來的?答案是它是事先建立好,然後被藏在編輯器裡。當使用者執行了Create Executable操作時,編輯器會從自己身上取出這個Player,然後再把專案所有相關資源,當成資料又全塞到Player裡,全部掺在一起打包成一個獨立的執行檔。當執行這個執行檔時,Player會先從自己身上找到遊戲資源,解開,載入遊戲,然後開始執行。

要完成上述的功能,主要有二件事情。第一就是如何把資料藏到執行檔內,第二就是載入藏在執行檔內的資料,以下分別說明。

+ + +

在VS.NET裡執行Add Resource功能時可以選擇要加入的資源類型,這些類型是Windows預設的,例如Bitmap/Cursor/Dialog/Menu/String Table等等。假如要加入的不是內建的資源類型,你也能在AddResource對話盒點選Custom鈕加入自訂資源。自訂資源用一個自訂的字串來作區別,例如"GameData"。如下圖所示,加入了一個"TEST"類型的資源,ID是IDR_TEST1,對應到外部檔案res\test1.bin。

以上是用工具的方式來加入自訂資源,只要是能夠編輯程式資源的工具都能拿來應用,而不限只能用VS.NET。接著我們來看怎麼用程式來加入資源。

Windows提供了BeginUpdateResource, UpdateResource EndUpdateResource三個API來讓我們修改資源的內容。
HANDLE hExe = BeginUpdateResource(exename, FALSE);

UpdateResource(hExe, _T("TEST"), (LPCTSTR)IDR_TEST1,
MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), (VOID*)lpData, szData);

EndUpdateResource(hExe, FALSE);
首先呼叫BeginUpdateResource開啟準備要更新內部資源的執行檔,接著呼叫UpdateResource來更新或加入指定類型及ID的資源項目,最後結束後再呼叫EndUpdataResource關閉程式。

+ + +

自動解壓縮程式執行起來後,主要要作的事情就是在自己內部搜尋並載入資源。

首先以FindResourceEx來搜尋指定類型和ID的資源,找到後再以LoadResource來載入資源,最後再以LockResource取得指向載入的資源資料的記憶體指標,並以SizeofResource取得這塊資源的大小,然後就能作讀取的操作,剩下來的部份就是程式怎麼去處理這塊記憶體的工作了。底下是個簡單的範例。
HRSRC hTest = FindResourceEx(hInst, _T("TEST"), (LPCTSTR)IDR_TEST1,
MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US));

int len = SizeofResource(hInst, hTest);
HGLOBAL lpres = LoadResource(hInst, hTest);

char const* pDat = (char const*)LockResource(lpres);


+ + +

如此,知道怎麼存取程式資源後,就能應用來製作出像zip那樣的自動解壓縮程式了。

2009年10月25日 星期日

打磚塊

沒有使用任何資源,使用純色塊編輯了一個陽春版的打磚塊小遊戲。

2009年9月2日 星期三

C++的靜態多型

class Foo : public Base<Foo>
{
};
在上例中,類別Foo繼承自類別Base,類別Base有一個Template參數,而在此例中是以類別Foo作為參數傳入。以上這個宣告是合法的,根據C++的標準規格,當類別Foo宣告之後,Foo這個名字就算是已被定義了,所以能夠立即被拿來餵給類別Base作為參數使用。
template<class T>
class B
{
public:
void sayHello()
{
T* pThis = static_cast<T*>(this);
pThis->hello();
}

// 可被子類別覆寫(overridable)
void hello()
{
cout << "B::hello" << endl;
}
};

class D1 : public B<D1>
{
};

class D2 : public B<D2>
{
public:
void hello()
{
cout << "D2::hello" << endl;
}
};

int main()
{
D1 d1;
D2 d2;

d1.sayHello(); // 印出 "B::hello"
d2.sayHello(); // 印出 "D2::hello"

return 0;
}
在上面的範例中我們宣告了一個類別B,還有二個繼承類別B的類別D1和類別D2。類別D1和類別D2以上面所提到的方式,以自己為參數繼承template類別B。類別B中有個sayHello公開函式,它會呼叫一個叫作hello的函式。hello函式在類別B中有一個預設的實作,會在畫面上列印出"B::hello"的文字訊息,而這個函式hello可以被子類別所覆寫。如果子類別有覆寫hello函式實作自己的版本,則在呼叫sayHello時子類別的實作就會被呼叫到,否則就會呼叫到預設的類別B的版本。

這裡使用到的關鍵技巧就在於static_cast<T*>(this)這一行的地方。

它把this指標轉型成參數T所指的型別。在這個例子中,參數T不是類別D1就是類別D2,而類別D1或類別D2都是類別B的子類別,所以這樣的轉型是合法且安全的。透過pThis指標來呼叫到的hello實作品會根據參數T的型別而定,如果參數T有一個自己的hello實作,則這個hello實作就會被呼叫,若沒有則會呼叫到類別B自己的hello實作。因為這裡面使用到泛型程式設計,所以這個動作是在編譯時期決定的。而這樣的行為和執行時期的多型非常類似,因為是在編譯時期決定,是靜態的,所以我們才會稱它作靜態多型。

但是如果參數T是有問題的參數呢?比如說在此例中我們不是傳入類別D1或類別D2呢?假如是這樣子的話,會有底下二種可能的情況發生。

1.傳入的參數T類別正好有個叫作hello函式,所以沒問題編譯可以過,只不過類別的行為是不是會變得異常就不得而知了。

2.傳入的參數T類別本身找不到一個叫作hello的函式(包含類別T的基礎類別),這時候編譯器就會直接告訴你一個找不到hello的定義的錯誤因而編譯失敗。所以這不構成什麼問題。

繼續回到原來的範例。類別D1只單純繼承類別B而沒有覆寫hello,所以物件d1在呼叫sayHello時會呼叫到類別B的hello,因此畫面上打印出"B::hello"文字訊息。而類別D2有自己的hello實作,所以物件d2在呼叫sayHello時就會呼叫到自己的hello函式,因此畫面上打印出"D2::hello"文字訊息。

使用靜態多型的技術(或把它叫作技巧)有以下幾個好處。

1.類別就不再需要額外的vtbl了,因為沒有任何虛擬函式。而因為不是執行期多型,所以呼叫更快,類別體積也較小。

2.在執行時期不可能透過一個null指標去呼叫靜態"虛擬函式",因為指標都是有效的this指標轉型而來的。

3.所有的呼叫都是在編譯時期決定,所以編譯器有更多機會作最佳化。

2009年8月29日 星期六

Windows程式使用argc,argv參數

Console程式的進入點main函數的其中一個原型如下:
int main(int argc, char *argv[]);

透過argc與argv二個參數,可以用來取得程式執行時執行檔案的名稱及其執行參數。例如你下了指令:C:\>dir /b /l,argc參數會等於3表示包含檔名本身再加上二個參數,可以由argv參數取出分別為"dir", "/b"及"/l"這三個字串。

+ + +

Windows程式進入點WinMain原型為:
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE, LPTSTR lpstrCmdLine, int nCmdShow);

假如想要像Console程式一樣能夠取得argc及argv參數,底下提供二個方法。

1,
直接使用__argc及__argv這二個全域變數即可。(如果你的程式是Unicode程式,則使用__wargv)

2,
使用Win32 API GetCommandLineWCommandLineToArgvW。(這個方法僅支援Unicode格式,同時第一個取出的參數不是執行檔名)

2009年8月19日 星期三

PNG秀圖的處理

首先看看底下這張PNG格式圖檔,正確顯示結果應該如下原圖所示。


載入後以API AlphaBlend作Per-Pixel Alpha Blend如下所示,結果是不正確。


檢查AlphaBlend規格發現,以AlphaBlend作Per-Pixel Alpha Blend時,會以底下公式計算每一個像素的值。

Dst.Red = Src.Red + (1 - Src.Alpha) * Dst.Red;
Dst.Green = Src.Green + (1 - Src.Alpha) * Dst.Green;
Dst.Blue = Src.Blue + (1 - Src.Alpha) * Dst.Blue;

要作正確PNG秀圖(或者說有帶Alpha Channel的圖),底下列出虛擬碼。

if (255 == Src.Alpha)
{
Dst.Red = Src.Red;
Dst.Green = Src.Green;
Dst.Blue = Src.Blue;
}
else if (0 != Src.Alpha)
{
Dst.Red = Src.Red * Src.Alpha + (1 - Src.Alpha) * Dst.Red;
Dst.Green = Src.Green * Src.Alpha + (1 - Src.Alpha) * Dst.Green;
Dst.Blue = Src.Blue * Src.Alpha + (1 - Src.Alpha) * Dst.Blue;
}

;

AlphaBlend測試代碼:
BLENDFUNCTION bf;
bf.BlendOp = AC_SRC_OVER; // 目前僅支援此操作.
bf.BlendFlags = 0; // 必須為0
bf.SourceConstantAlpha = 255; // 使用Per-Pixel Alpha
bf.AlphaFormat = AC_SRC_ALPHA; // 使用Per-Pixel Alpha

dc.AlphaBlend(0, 0, sz.cx, sz.cy, memdc, 0, 0, sz.cx, sz.cy, bf);

2009年8月16日 星期日

實作組合語言除錯器

這幾天寫了個簡易版的組合語言除錯器。原因是當我在整理並重新組譯之前寫的一個組合語言小程式時,找到了一個小Bug。Debug的過程花了點時間,因為手上還沒有Debugger可用。所以就動手把之前就已計劃有需要製作的除錯器實作了一個可用的簡單版本出來,目前俱備基本的Memmory Dump, Unassemble, Breakpoints 等除錯功能。

這張圖裡面可以看到的指令好像跟x86指令集裡的有點不大一樣,暫存器看起來更不同。沒錯,因為這是我設計的虛擬機器指令集,我把它命名為 jaja XD

2009年8月13日 星期四

實驗中國象棋 for Windows Mobile Port

花了一點時間把以前作的實驗象棋植移上去,純粹當作睡前消磨時間的小玩意。這次還新增了簡單的開局資料,至少能避免每次都走出一模一樣的棋局。


PS:WM PC 模擬器在有Focus的情況下PrintScr鍵似乎無效。

2009年8月11日 星期二

聰明過頭的編譯器

作個小實驗來看看我們所使用的編譯器有多聰明,看了實驗結果後也該小心在意,別因為編譯器太過聰明,聰明反被聰明誤,壞了事。

工具:MS VS.NET C++ 2003
平台:MS Windows XP SP2
void Foo()
{
int a = 0;
if (10 == a)
a = 1;
}

void Bar()
{
int a = 0;
if (10 == a)
a = 1;
}

int main()
{
cout << "&Foo = " << &Foo << "\n";
cout << "&Bar = " << &Bar << "\n";
}
以上很簡單的一小段程式碼。

Debug版本的執行結果如下。

&Foo = 0041B5A0
&Bar = 0041BFA0

而Release版本的執行結果如下。

&Foo = 00401130
&Bar = 00401130

顯然Release版時,Foo及Bar這二個函式被當作同一個函式,因為二者實作內容完全相同(或最佳化後相同),只有名稱不同。雖然這個情況很少見,但看到這樣的結果,還是要小心謹慎,以免不小心寫出莫名其妙不知問題在那裡的程式來。

;

同樣的程式在VS.NET 2005及2008上測試結果也是會發生同樣的問題。

2009年7月25日 星期六

good 最新iPhone移植

目前已把最新版本的good移植上iPhone。因為繪圖的部份已更換為OpenGL版本的gx,所以效能上比較沒什麼問題了。除了color這個範例因為一次產生2000個物件,每一個物件都以scirpt作控制,在效能上不很理想外,其它都OK。

下圖畫執行的程式就是上次介紹過的範例,可以選擇8個範例中的其中一個以PlayPackage方式個別執行。

上面的抓圖裡,可以看到畫面最下方黃色區塊畫了一個虛擬的搖桿,這是作了一個簡單的輸入裝置模擬,不是很精確,會再另外找時間加強。

2009年7月20日 星期一

Complied Lua code support.

在載入Lua script的部份稍微作了點修改,以luaL_loadstring取代luaL_loadbuffer,現在除了可以支援原來的文字格式程式碼外,也能夠支援使用luac事先編譯好的Lua程式碼。

C/C++呼叫ObjectiveC程式

前面的一篇文章裡提到如何在ObjectiveC程式裡呼叫C/C++程式的方法,現在反過來要在C/C++程式裡呼叫ObjectiveC的程式,要如何作?方法也同樣也很簡單,關鍵點就在於二者之間的程式介面必需是C的介面,也就是說在ObjectiveC這邊可以寫個C函式讓C/C++的程式直接呼叫。

舉一個簡單的例子,現在要用ObjectiveC寫個函式,這個函式可以讓C/C++碼呼叫取得目前app的完整路徑字串。
// 取得app路徑字串,複製字串至一buffer
// pBuff buffer
// szBuff buffer的大小,字元數
// 回傳實際複製的字元數
int objcGetAppPath(char* pBuf, int szBuff)
{
char* ppath = [[[NSBundle mainBundle] bundlePath] UTF8String];
...
}
在C/C++裡面就可以直接呼叫使用這個函式了。

這裡有一點要特別提出來,在C++中使用objcGetAppPath時有個地方要注意一下,就是宣告時需要使用extern "C"修飾,這樣才不會被當作是C++的函式而最後發生連結錯誤。
extern "C" {
int objcGetAppPath(char* pBuf, int szBuff);
}

2009年7月16日 星期四

OpenGL gx 取代 SDL gx

將編輯器內建的Player用的gx換成OpenGL版本取代原來SDL的版本,執行起來比SDL的版本順暢的多,按鍵的處理也更單純一點,等有空時再弄到iPhone上看看效能如何。



這裡面發現個新的小問題。從抓圖裡可以看到原本有使用KeyColor的物件不正常了,沒有正確鏤空把顏色去掉。

其實這個問題可以忽略,因為原本使用2D SDL render,鏤空的方法是用傳統的作法,現在改成OpenGL的作法,只要載入的貼圖本身帶alpha,再把alpha test打開,就能作到自動鏤空,所以不是問題。所以以後只需要稍微注意一下,用的是那一個版本的gx就行了,當然最理想的處理是使用者完全不必管這種問題,底下自動處理掉。

2009年7月13日 星期一

good sample list

在一邊開發good編輯器及核心功能的時候,也同時會製作使用到對應功能的小專案來作測試。目前總共有8個範例,有的是很小的測試程式,也有的是具體而微的小遊戲,下面作個整理及簡單介紹。

weeder 鋤草機

這是最早作的範例,因為鋤草機這個小遊戲感覺起來最單純,所以就拿它作為一個標的,以把它用good實作出來為目標來開發good相關功能。一開始只編輯了一個關卡,後來花了點時間,將原作裡12個關卡全部編輯出來,作成完整版。

zelda 薩爾達

這是最早開發good的起因,為了重製薩爾達作為練習。一開始重點放在地圖編輯器上,也花了不少時間編輯世界地圖。整合了Lua後,再加上一小段程式碼,讓林克可以在地圖上走動。不過和最終的目標還相差太遠,只能在地圖上走走路,作些簡單的碰撞,只好等有機會再繼續未完成的工作。

mmc 魔法寶石方塊

為了驗證good也能製作出這種類型的遊戲,花了幾天時間編輯實作這個小遊戲的雛型出來,效果還不錯。

texture 貼圖物件

加上這個功能後,才能以更簡單的方法製作拼圖遊戲。沒有這個功能,也能作出拼圖這樣的遊戲,只不過是需要使用較曲折麻煩的方法作到。

color 色塊物件

有了色塊物件後,就不必以填貼圖的方式來填色,省圖又方便。

stge 粒子系統

很早就規劃打算要整合stge模組。雖然stge本來是專門設計來製作彈幕射擊遊戲,但因為它本身的設計就是一個以粒子為核心的系統,所以不一定只能被用來製作射擊遊戲,也能夠作為一般用途的粒子系統。目前還不算100%整合完成,但已經可以拿來作應用。等到更完整的整合完成後,就能用good來重製25940p

puzzle 拼圖

在貼圖物件功能加入後,就能很簡單的製作出這類型的應用。

mario 瑪莉歐

這是拿來作gameplay練習的小品,實際上寫的不是很好,細節部份還有許多加強的空間。

2009年7月5日 星期日

iPhone程式的多點觸控(Multiple Touch)

自己開發的程式中,預設情況下多點觸控的檢查是Disable的,也就是說無論在畫面上同時按下幾隻手指頭,都只能得到一個點觸控的通知。要把多點觸控打開有二種方法。

1.
透過UIView類別的setMultipleTouchEnabled打開多點判定。
[[Director sharedDirector] window] setMultipleTouchEnabled:YES];

2.
覆寫(override)isMultipleTouchEnable方法,並回傳YES。
-(BOOL) isMultipleTouchEnabled {return YES; }

2009年6月27日 星期六

Win32 拖曳Tree-View項目

處理Win32的Tree-View Item主要分為三個步驟。
  • 開始拖曳通知。
  • 拖曳中處理。
  • 拖曳結束。
底下對拖曳程序的每個階段作個簡介,並提供實作範例。


開始拖曳通知

當使用者開始拖曳(Drag)Tree-View Item時,Tree-View元件會經由WM_NOTIFY訊息發送一個TVN_BEGINDRAG通知給父視窗,這時應用便可以以這個通知得知使用者從什麼位置開始拖曳那一個Item,並開始進入拖曳程序。
case WM_NOTIFY:
switch (((LPNMHDR)lParam)->code)
{
case TVN_BEGINDRAG:
// ...由此進入拖曳程序
Main_OnBeginDrag();
break;
}
break;
Main_OnBeginDrag是應用程式定義的一個函式,主要的任務是設定好相關狀態表示進入拖曳程序,並由Tree-View取得一個暫時的ImageList元件,可以用來畫出拖曳中的Item。
void Main_OnBeginDrag(HWND hwndTV, LPNMTREEVIEW lpnmtv)
{
// 取得暫時的ImageList.
HIMAGELIST himl = TreeView_CreateDragImage(hwndTV, lpnmtv->itemNew.hItem);

// 開始進入拖曳.
ImageList_BeginDrag(himl, 0, 0, 0);
ImageList_DragEnter(hwndTV, lpnmtv->pt.x, lpnmtv->pt.x);

SetCapture(GetParent(hwndTV));
g_fDragging = TRUE;
}
拖曳中處理

當使用者以滑鼠左鍵開以拖曳物件時,Tree-View送出一個TVN_BEGINDRAG,接著應用程式需要處理WM_MOUSEMOVE訊息來反應使用者拖曳的動作,直到使用者將滑鼠按鍵放開產生一個WM_LBUTTONUP訊息時,才結束拖曳動作。附帶一提,如果使用者是以滑鼠右鍵開始拖曳物件,則產生的通知是TVB_BEGINRDRAG,並以對應的WM_RBUTTONUP訊息作結束訊號。
void Main_OnMouseMove(HWND hwndParent, HWND hwndTV, LONG xCur, LONG yCur)
{
if (!g_fDragging)
return;

// 更新滑鼠拖曳的座標.
POINT point;
point.x = xCur;
point.y = yCur;
ClientToScreen(hwndParent, &point);
ScreenToClient(hwndTV, &point);
ImageList_DragMove(point.x, point.y);

// 暫時關閉不畫拖曳物件,讓Tree-View可以重畫它的內容.
ImageList_DragShowNoLock(FALSE);

// 取得滑鼠指標底下的Item,並將它設定成DropTarget Item.
TVHITTESTINFO tvht;
tvht.pt.x = point.x;
tvht.pt.y = point.y;

HTREEITEM htiTarget = TreeView_HitTest(hwndTV, &tvht);
if (NULL != htiTarget)
TreeView_SelectDropTarget(hwndTV, htiTarget);

// 繼續繪製拖曳的物件.
ImageList_DragShowNoLock(TRUE);
}
拖曳結束

Tree-View的父視窗接收到一個WM_LBUTTONUP訊息時,表示拖曳程序終止。應用程式執行相關狀態回覆並且根據自己的定義執行拖放動作的操作。
void Main_OnLButtonUp(HWND hwndTV)
{
if (!g_fDragging)
return;

// 取得拖放目標.
HTREEITEM htiDest = TreeView_GetDropHilight(hwndTV);
if (NULL != htiDest)
{
// To do: 執行實際的移動或相關操作.
}

// 釋放掉暫時的ImageList物件.
ImageList_EndDrag();

// 狀態回覆.
TreeView_SelectDropTarget(hwndTV, NULL, TRUE);
ReleaseCapture();
g_fDragging = FALSE;
}


參考連結

2009年6月17日 星期三

good spec

程式語言

  • 程式語言:C/C++
編輯器

  • 系統平台:Windows
  • 視窗框架:WTL
執行時期

  • 系統平台:Windows,MacOS/iPhone
  • 程式庫:smallworld2,stge
  • 第三方程式庫:Lua/SDL/zlib/Boost.Spirit

繪圖(gx)

  • GDI
  • SDL
  • OpenGL

編輯器使用WTL作視窗框架實作,所以主要是以C++ Template實作。而Runtime部份則整個使用C++ Template實作,主要區分成三大模組:good、ed和rt。另外還有個gx模組,簡單定義了圖形介面讓其它模組使用來作圖形繪製。

good模組提供了資源管理的功能,ed模組繼承good模組再擴充編輯功能,而rt模組繼承good模組實作Runtime及提供Script擴充介面。其實正確來說,rt模組並非繼承good模組,而是包含good模組,透過包含及使用good模組作為資源管理。

good模組是個狀態管理器,只能載入資源和讀取內容。ed模組則繼承good模組提供編輯資源內容的功能,可以修改資源並存檔。而rt模組透過good模組載入資源,再經由RunTime及Script將資源和物件關聯起來,並付予物件動作邏輯,最後再透過gx模組將結果呈現在畫面上。

2009年6月10日 星期三

good整合stge粒子系統

初步完成stge和good的整合。


stge本來是專門設計來編輯彈幕射擊遊戲的,但因為它本身是個粒子系統,所以並不侷限於只能應用在彈幕射擊遊戲。整合進good後,就能以stge製作粒子特效,再以good來製作圖形及邏輯,讓good可以作出更豐富的應用。

在使用上變成會需要使用到二種Script,一個是以Lua來編輯Gameplay,另外再使用stge語言來編輯粒子。編輯器對應新增的粒子資源作了對應的修改,讓使用者能新增Particle資源,如下圖所示。同時Script也需要增加幾條對應的API。


目前還只是初步整合,還有許多地方需要作細部調整和加強。

整合stge後,有個較麻煩的問題。因為stge使用Boost.Spirit來實作Parser,所以編譯時間大大的增加了不少,這對Debug的影響很大,要想辨法解決。

2009年6月9日 星期二

全銀幕(fullscreen)的iPhone應用程式

使用Xcode新建一個iPhone的專案,編譯好執行起來的應用程式預設情況下在畫面的最上方會有個狀態列,顯示電信業者、時間和電量等資訊。

要把這個狀態列隠藏起來讓應用程式變成full-screen很簡單,不需要修改到原來的程式碼,只需要修改一下設定即可。

打開Resources/Info.plist檔案,在上面按右鍵,點選Add Row新增一個新欄位,輸入名稱UIStatusBarHidden。接著在這個新欄位上點右鍵,在Value Type子選單中選擇Boolean將這個欄位的屬性改變為Boolean,然後勾選設成True。

完成後存檔,重編程式,再執行後就可以看到應用程式以full-screen方式執行。

2009年6月3日 星期三

Windows Mobile透過ActiveSync上網

這個方法可以讓Windows Mobile手機透過USB和電腦連接經由ActiveSync連線上網。

步驟如下:

  1. 在PC開啟Regedit。(開始->執行->Regedit)
  2. 點選進入HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows CE Services。
  3. 在"Windows CE Services"點選右鍵選新增DWORD值,名稱為"AllowLSP",值為"0"。
重新連接USB,完成。

現在可以在手機上經由USB透過PC開啟IE上網。

2009年5月27日 星期三

good::gx介面

good::gx是負責繪圖的介面。它很簡單,至少目前來說還很簡單。只有二個介面:Image及Graphics。

Image介面對貼圖作一層薄薄的包裝,good執行的時候,所有使用到的貼圖的地方全都透過這一層介面作存取。下面是Image介面定義。
template<class ImgT>
class Image
{
public:

bool isValid() const;

int getWidth() const;
int getHeight() const;
bool hasKeyColor() const;
int getKeyColor() const;
void setKeyColor(int keycolor);
};
Graphics介面提供了很少的功能,目前只有貼圖和畫色塊二個功能,因為目前good的成像目前只需要這二個功能就足夠畫出所有東西。下面是Graphics介面定義。
template<class ImgT>
class Graphics
{
public:

bool drawImage(int x, int y, ImgT const& img, int srcx, int srcy, int srcw, int srch);

bool drawImage(int x, int y, ImgT const& img)
{
return drawImage(x, y, img, 0, 0, img.getWidth(), img.getHeight());
}

bool fillSolidColor(int left, int top, int width, int height, int color);
};
+ + +

實際應用的時候,只需要針對不同繪圖平台作不同實作品的提供,對於good內部而言,幾乎不必作任何修改。目前good::gx有四種實作品,GDI、SDL、imgp及OpenGL。

2009年5月22日 星期五

OpenGL 畫色塊

前幾天用OpenGL glDrawArrays方法來畫色塊時遇到問題。色塊是畫出來了,不過另外一個不使用顏色的貼圖方塊也被疊上了和色塊一樣的顏色,真是莫名其妙。


原來,畫色塊的時候使用glColor,而且當畫貼圖方塊時,glColor指定的顏色也會被使用到,所以要再以glColor指定一個白色的顏色值進去(0xffffffff),這樣畫出來的結果才會正確。


看起來好像對了,不過還是不對勁,因為我畫的是一個RGB(0x88ffff00)的黃色色塊,可是這顏色怪怪的。經過幾次測試發現這顏色和畫色塊之前所畫的一個貼圖方塊的顏色有關係。

經過研究後,發現問題還是出在狀態上,畫色塊的時候除了要以glDisableClientState關掉GL_TEXTURE_COORD_ARRAY狀態外,也還要再以glDisable把GL_TEXTURE_2D狀態關閉,這樣子就得到正確的結果了。


+ + +

換說回來,雖然作過幾個3D應用的專案,甚至還參與過3D引擎的實作,我還是不懂3D。往好處想,這表示我還有很大的成長空間~

2009年5月17日 星期日

編輯魔法寶石遊戲資源

底下是J2ME版小香咪咪方塊的寶石方塊圖形資源,直接可以拿來使用,有這張圖就足夠來作個魔石寶石小遊戲了。


全部共有六種不同顏色的寶石方塊,再加上一個威力方塊P,不過這次我們只會用到那六種不同顏色的方塊。

+ + +

首先開個新專案,視窗解析度設成和J2ME版小香咪咪方塊的大小一樣,176x208,其它保留預設值,然後專案存檔。

接著把圖形資源加入,每個Tile大小是16x16。一一把六種不同顏色的方塊和它對應的同色爆炸方塊加入製成精靈資源。在加入方塊精靈資源時,有個小地方特別注意的是,先加入一個顏色的方塊後,接著再加入這個顏色方塊的爆炸版本。依照這個規則把所有方塊加入,共12個精靈。


如上圖,接著再加入一個地圖資源。大小是11x13,每個Tile是16x16。上面缺的一塊是故意保留,而不是畫到一半就抓圖下來。缺的這一塊到時候會在關卡編輯器裡增加一個色塊來填,為了作出方塊是由正上方落下來的效果,我們會拿一個色塊來作遮擋。


接下來如上圖所示新增一個關卡資源,大小設定成和視窗一樣,指定黑色作為背景清除顏色。加入事先編輯好的地圖資源,一個遮擋用的色塊,還有三個作了提示下一回合要出現的方塊。以上完成所有遊戲資源編輯,剩下的就是用Lua編寫遊戲邏輯,下回再繼續。

2009年5月15日 星期五

在iPhone上的效能問題

前次花了點時間將good植移到iPhone上去,因為只是試port上去,所以並沒有特別考慮效能問題。這裡面最大的瓶頸就是成像的部份,一開始的作法是將成像的結果全輸出到一個記憶體中的影像Buffer,最後再拿這個Buffer動態的產生貼圖,然後再以這張貼圖貼到剛好蓋滿畫面的二個三角形上。

這在電腦上看不出有什麼效能上的問題,不過當真的把它放到iPhone上執行的時候就可以很明顯的感受到效能的不足。當然這個作法本來就只是為了速成,想要快速的看到結果,所以效能會有問題也是預料到的事情。

解決辨法很簡單,只要把Graphics抽換掉就可以了。目前計劃的作法也很簡單,提供一個OpenGL實作的Graphics就行了。不過現在還沒空來作這件事,等有空再說。

2009年5月13日 星期三

快速複製STL Stream內容

有時候我們需要把一個stream的內容複製到另一個stream上,底下的方法最簡單也最快速。
ifstream ins("in.txt");
ofstream outs("out.txt");
outs << ins.rdbuf();
以上。

2009年5月10日 星期日

魔法寶石消除演算法

現在要來稍微研究一下魔法寶石類遊戲的核心Gameplay,消除演算法。有了基本知識之後,再來用good實作一個魔法寶石遊戲。進入正題之前,先簡單介紹一下玩法。如圖中所示,這是一個典型的魔法寶石遊戲,當然還有其它不同的變形,不過這邊只提核心玩法,這是所有魔法寶石遊戲都相同的。


在一個魔法寶石的遊戲區裡可以看到一堆不同顏色的方塊。遊戲的目的很單純,想辨法不斷的消除方塊,直到無法再繼續為止(GameOver)。這邊消除的方法主要有二種,一種我稱為米字形規則,另一種我稱它為十字形規則。

十字形規則

十字形規則是以一個方塊為中心,如果包含自身在這個方塊的上下左右的相隣方塊中有四個以上相同顏色的方塊存在,則這個方塊就可以被消除。


如上圖中,可以看到要判定1號方塊是否可以消除的範圍比米字形規則大的多,依據十字規則的檢驗,最後可以找出1到4號的四個黃色方塊可以被消除。因為這次我們只要探討米字形消除規則,所以略過十字規則的研究。

米字形規則

米字形規則是以一個方塊為中心,假如在這個方塊的上下,左右,或斜向的方向上包含自己本身有超過三個以上相同顏色的方塊存在,則這個方塊就可以被消除。


上圖中間的區塊部份,中間1號方塊周圍用紅色線框框起來的四個方向,垂直(A)、水平(B)及二條斜向(C及D)。以此規則,上圖右側區塊部份中標示1到5的五個橘色方塊可以被消除。

檢查的時候,每一條方向是各自獨立的,也就是說上述超過三個以上相同顏色的方塊這個限制必須是在同一方向上才能成立。只要其中一條方向消除條件成立,剩下的方向就可以略過不作檢驗。同理在同一個方向上只要檢查出有三個以上相同方塊存在,剩下的方塊就不必一一檢查。

以垂直方向為例。

首先檢查1號方塊上方的方塊是否和1號方塊是相同顏色,假如不同的話再往上一個方塊也不需要再作檢查。假如是一樣的話就再檢查最上面那個A方塊,假如也是一樣的話,那剩下的下面二個A方塊就沒必要再檢驗了,因為已經有三個一樣的方塊存在了。

假如最上面的A方塊和1號方塊不同的話,則再檢查1號方塊下方的A方塊。假如這個方塊和1號方塊不同的話,那最下面的A方塊也不需要再多作檢查了。反過來說,假如這個方塊和1號方塊是一樣顏色的話,再加上1號方塊上面那塊就有三個一樣顏色的方塊。

同理可以推出反向及其它方向的檢驗規則,就不列虛擬碼了。

2009年5月6日 星期三

good的資料格式

自從使用Ini之後我就不再碰XML了,在任何情況下,只要情況允許我都會儘可能的使用Ini作為資料格式或設定檔案。雖然Ini不像XML那樣天生就能表達結構化的資料,但也不是完全不行,至少應用在good上就沒什麼問題。

good專案資料格式使用Ini,再透過smallworld2的Ini模組就能很簡單的操作讀寫。

+ + +

專案檔頭

每個good專案檔裡,至少會有個名稱叫作good的Section,這相當於檔案的Header。這個Section包含了整個專案的內容資訊,是個總表。底下是個典型的範例。
[good]
version=0.3
name=mmc
window=176 208
texs=1
maps=2 3
sprites=4 5 6 7 8 9 10 11 12 13 14 15
levels=18
如上。格式版本是0.3;專案名稱是mmc;視窗的大小或解析度是176X208;有一個ID為1的貼圖資源;有二個ID分別是2和3的地圖資源;有12個精靈資源;以及一個ID為18的關卡資源。

多麼簡單一目了然,這就是我喜歡使用Ini的原因。

Script資源

接著有個叫scripts的Section,這個Section的內容是Script資源列表,和其它類型的資源比較起來是比較特別的,因為它不是在Section good裡面的一個項目,而是獨立出來一個Section。底下是個範例。
[scripts]
16=./mmc.lua
在scripts Section裡,每一個項目表示一個Script資源,項目的Key值表示為這個Script資源的ID,而項目的Value值表示為Script資源的檔案路徑。在good裡面的所有牽涉到檔案路徑的欄位,內容一律都是相對於專案檔本身的相對路徑。

貼圖資源

在專案檔頭裡列出的貼圖資源,每一個項目都對應到一個貼圖資源Section,底下是個簡單的範例。
[tex5]
name=tileset
fileName=./weeder.bmp
hasKeyColor=1
keyColor=253 0 255
貼圖資源Section的名稱固定以tex開始,後面的數字表示這個資源的ID。其它不同類型的資源也是以同樣的規則定義,所以你大概可以猜到Section map2就是ID為2的地圖資源。

上面的例子可以清楚的看到,這個貼圖的ID為5;名稱叫作tileset;圖形資源檔案路徑名稱為./weeder.bmp;使用KeyColor,顏色值為RGB(253,0,255)。

在目前的設定,good裡除了Script外所有的資源都有一個可有可無的name欄位。以上面的例子來說,假如tex5不指定名稱的話,那整個name項目都可以拿掉,沒必要再多寫個name=或name=""放在那裡表示空內容。

hasKeyColor和keyColor欄位也是同樣可有可無,一但hasKeyColor內容不是0(一般使用1)就表示要用使keyColor欄位的顏色,否則hasKeyColor和keyColor二個欄位都可以省略。

地圖資源

地圖資源的資料格式是所有資源裡最複雜的。底下是個簡單的例子。
[map26]
width=6
height=6
texture=5
tileWidth=32
tileHeight=32
cxTile=8
cyTile=8
data=k2VgYJBEw1JALA/ElmhYgQhxayLVy2CxVxqIAQ==
vgrid=1 128 128 128
hgrid=1 128 128 128
先看width和height欄位,這二個欄位定義的是這張地圖的大小是由多少個Tile組成;而每一個Tile的大小由tileWidth和tileHeight定義,單位是Pixel。

而Tileset是由texture欄位所指定,數字5表示這張地圖使用tex5作為Tileset,再去參考tex5就能得到相關資訊。cxTile和cyTile是一個輔助訊息欄位,它們記錄的值是這張地圖新建時,根據相關設定對Tileset作切割所得到的Tile個數。

而vgrid和hgrid分別是垂直及水平輔助線的設定,每一條輔助線有4的數字設定值,第一個數字是格線的Tile間隔數,後面三個數字是輔助線的顏色RGB值。

data欄位看起來是個亂它八糟的字串,這是編碼過的地圖資料。每張地圖是width乘height個Tile所組成,假如地圖比較大的話那要把所有資料都列出來會得到一個很大的字串,所以需要對它作壓縮,使用zip壓縮過後的資料還要經過base64作編碼才能以文字格式儲存下來。

精靈資源

精靈資源因為和地圖資源都是Tile Base的資源,所以格式和地圖資源的格式有點類似。底下是個簡單的範例。
[sprite32]
name=head_right
texture=5
tileWidth=32
tileHeight=32
cxTile=8
cyTile=8
data=8 60 9 60
loop=1
這裡只說明和地圖資源不同的地方,其它都和地圖資源相同。loop欄位簡單的指定這個精靈資源的用來作動畫播放時是否會自動的作循環播放,0表示只播一次,1表示作無限循環。

data欄位定義和地圖資源有所不同。精靈是由一個個的Frame所組成,每一個Frame有二個資料,Tile(指向Tileset)值及播放時間(Frame)。data欄位是由2的倍數的個數組成,每二個數字表示一個Frame,第一個數字表示Tile值,第二個數字表示的為播放時間。

關卡資源

關卡資源是的格式有點類似專案檔頭,因為關卡內含了物件。底下是個例子。
[level18]
width=176
height=208
script=Level
hasClearColor=1
clearColor=0 0 0
objects=19 20 21 22 23
width和height是關卡的大小,單位是Pixel。script欄位指定一個定義在Script資源裡的類別物件,這樣在執行時間就能透過它來控制這個關卡物件。

hasClearColor和clearColor類似貼圖的hasKeyColor和keyColor欄位,是用來指定一個作為清除畫面背景的顏色,同樣也是可有可無。

objects表示這個關卡總放擺放了5個物件在上面,ID分別是19到23。每一個物件類似像其它資源一樣,又關聯到一個物件資源Section。底下提供幾個範例。
[object31]
texture=1

[object30]
map=26
x=224
y=144

[object21]
sprite=4
x=144
y=32
script=BaseBlock

[object44]
dim=0 0 48 48
bgColor=255 0 0
以上分別表示了四種不同類型的物件,可以分別對應到在關卡編輯器裡可以擺放的四種物件類別。

object31是個貼圖物件,所以它有個texture欄位用來指明它是參考到tex1的貼圖資源。和底下的二個範例作比較,它少了x和y欄位,因為它的位置就在(0,0),而剛好是預設值,所以也省略不寫了。

object30是個地圖物件,參考的地圖資源是由map欄位所指定,這個地圖物件的位置是擺放在座標(224,144)的位置。

object21是個精靈物件,參考的精靈資源是由sprite欄位所指定。script的功能和關卡資源的script欄位功用一模一樣,在關卡裡面(包含本身)所有東西都可以指定一個Script物件。

object44是個純色塊物件,由bgColor指定色塊顏色RGB值,而dim的後二個數字用來指定色塊的大小。

+ + +

以上是good的資源檔格式的定義,非常簡單。也因為簡單再加上是文字格式,所以可以很容易也很方便的用文字編輯器開啟作編輯。不過為了避免發生錯誤,除非真的知道自己在改什麼,否則最好還是透過編輯器來編輯資料。

2009年5月3日 星期日

實作迷你薩爾達傳說

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

先以編輯器編輯好遊戲資源。使用地圖TileSet編輯好地圖資源,並開一個新關卡資源加地圖加入。然後再加入幾個簡單的精靈資源,主要是林克的4個行走方向,還有幾朵不同顏色的花。接著就可以開始用Lua來實作Gameplay。

+ + +

因為這是個簡單的Demo,主要要實作的功能是要可以在地圖上行走,也要可以作簡單的碰撞,當走到地圖邊緣的時候要能捲動到下一張地圖。

為了簡單起見,我把它劃分為二個類別:Game及Link。Game類別只負責作地圖捲動的工作。而Link類別則負責處理使用方向鍵輸入在地圖上行走,行走的時候作碰撞簡單,以及走到地圖邊緣時觸發事件讓Game類別作地圖捲動。

另外在也圖上常常可以見到一些會動的小花,假如要在關卡編輯器裡面,一個一個用精靈物件的方式種上去太費時費力了。所以需要使用動態生成的方式,在每次地圖捲動後把可見範圍內的小花動態產生出來,同時釋放捲出畫面範圍外的小花物件。

所以最後整個關卡裡只需放二個物件,一個是地圖,一個是林克(在靠畫面中央小屋前的小傢伙)。然後在關卡的Script欄位填上Game,林克的Script欄位填上Link。而視窗的大小設定為一張小地圖的大小,160x128。

+ + +

在關卡上的每一個物件都有個位置,可以透過Good.GetPos和Good.SetPos來作存取。而關卡本身也有位置屬性,一樣可以透過Good.GetPos和Good.SetPos來作存取。

改變關卡的位置,可以製作出地圖捲動的效果。當然也可以去改變地圖物件的位置來作出捲動效果,但是使用這個方法的話,所有相對於地圖的物件,像是林克或地圖上的小花也都要一起移動。雖然可以把這件物件的父親都設成地圖,這樣只要移動地圖就好,但還是比不上移動關卡來的直覺容易,畢盡關卡是所有物件的最上層父物件,只要移動它所有底下的物件都可以全跟著動起來。
Game.OnCreate = function(param)
local idLvl = param._id

scrolling = false
Good.SetPos(idLvl, 320, 1280)
KillAndDynaGenObj(idLvl)
end
Game初始化的時候,我們先把關卡的位置捲到小屋前林克所在的小地圖位置,同時初始化用來控制是否作地圖捲動的全域變數scrolling。接著再呼叫函式KillAndDynaGenObj來動態產生小花物件。

KillAndDynaGenObj函式首先會把除了林克以外的所有精靈物件刪除。接著檢查目前地圖可見範圍內的每一塊Tile,假如是小花Tile的話,則在那個地置動態的產生一個小花物件出來(全部共有4種不同顏色的小花)。最後一步則是再把林克物件加入關卡裡,這個動作是重新調整物件的次序,確保林克會畫在其它物件上面。
function KillAndDynaGenObj(idLvl)
local nc = Good.GetChildCount(idLvl)
for i = nc,0,-1 do
local idChild = Good.GetChild(idLvl, i)
if (Good.TYPES_OBJ == Good.GetType(idChild) and 14 ~= idChild) then
Good.KillObj(idChild)
end
end

local x,y = Good.GetPos(idLvl)
for i = 0,9 do
for j = 0,7 do
local lx,ly = x + 16 * i, y + 16 * j
local tile = Resource.GetTileByPos(1, lx, ly)
...(略)
end
end

Good.AddChild(idLvl, 14)
end
OnStep則檢查scrolling全域變數是否設立來執行捲動,捲動的方向則檢查另一全域變數dir。捲動的時候依據水平或垂直方向決定速度,再配合offset全域變數來控制捲動量。當結束捲動時,再執行一次KillAndDynaGenObj函式重新產生小花物件。

Game.OnStep = function(param)
local idLvl = param._id

if (not scrolling) then
return
end

local x,y = Good.GetPos(idLvl)
local spd1, spd2 = 5, 4
if (Input.KEYS_LEFT == dir) then
x = x - spd1
offset = offset + spd1
elseif ...
....(略)
end
Good.SetPos(idLvl, x, y)

if (Input.KEYS_LEFT == dir or Input.KEYS_RIGHT == dir) then
if (160 <= offset) then
scrolling = false
end
elseif (Input.KEYS_DOWN == dir or Input.KEYS_UP == dir) then
if (128 <= offset) then
scrolling = false
end
end
if (not scrolling) then
KillAndDynaGenObj(idLvl)
end
end
稍微注意一下,dir的值沒有另外再定義而是直接套用方向鍵的定義。

+ + +

林克的處理主要分成二個部份。一個是當地圖捲動時林克也要隨著地圖捲動移動位置,如下。
Link.OnStep = function(param)
local id = param._id
local x,y = Good.GetPos(id)

if (scrolling) then
local spd1, spd2 = 0.5, 0.5
if (Input.KEYS_LEFT == dir) then
x = x - spd1
elseif ...
....(略)
end
Good.SetPos(id, x, y)
return
end
end
當地圖捲動時林克也需要跟著修正位置。


如上圖所示,當林克走到地圖左側邊緣,地圖往左捲動。假如完全不對林克作位置修正的話,地圖捲過去後,林克一樣會站在地圖左側邊緣。這不合理,因為以這張地圖的角度來說,林克是從右側進入的,應讓要站在右側才對。
Link.OnStep = function(param)
....(略)
local ox,oy = x,y
local spr
local spd = 0.8

if (Input.IsKeyDown(Input.KEYS_LEFT)) then
x = x - spd
spr = 10
elseif (Input.IsKeyDown(Input.KEYS_RIGHT)) then
x = x + spd
spr = 13
end
if (Input.IsKeyDown(Input.KEYS_DOWN)) then
y = y + spd
spr = 11
elseif (Input.IsKeyDown(Input.KEYS_UP)) then
y = y - spd
spr = 12
end

if (ox == x and oy == y) then
return
end

Good.SetSpriteId(id, spr)
end
接著檢查按下的方向鍵來計算移動後的位置,假如沒有任何位移(包含沒有按下方向鍵),則不作任何事,否則依據按下的方向鍵改變林克的面向。
Link.OnStep = function(param)
....(略)
local cx,cy = x + 8, y + 10
if (Input.IsKeyDown(Input.KEYS_LEFT)) then
cx = x;
elseif (Input.IsKeyDown(Input.KEYS_RIGHT)) then
cx = x + 16
end
if (Input.IsKeyDown(Input.KEYS_DOWN)) then
cy = y + 16
end
if (not IsMoveable(cx, cy)) then
return
end
end
剛才是依據移動下方計算下一個位置,現在再把下一個位置加上一個Tile的大小同時再一次依據移動方向作一次修正(因為原點在物件左上角),然後再以函式IsMoveable判定是否可走來作最後是否要移動林克的決定。
MoveableTile = {121,122,....(略)}

function IsMoveable(x, y)
local tile = Resource.GetTileByPos(1, x, y)
for i,v in ipairs(MoveableTile) do
if (tile == v) then
return true
end
end
return false
end
IsMoveable原理很簡單,根據指定位置取出地圖Tile,並且和MoveableTile裡所列出的所有可走的Tile作比較作是否可走判定。
Link.OnStep = function(param)
....(略)
local idLvl = Good.GetParent(id)
local px,py = Good.GetPos(idLvl)

dir = -1
if (px > x) then
dir = Input.KEYS_LEFT
x = x - 0.5
elseif (px + 160 < x + 16) then
dir = Input.KEYS_RIGHT
x = x + 0.5
end
if (py > y) then
dir = Input.KEYS_UP
y = y - 0.5
elseif (py + 128 < y + 16) then
dir = Input.KEYS_DOWN
y = y + 0.5
end

if (-1 ~= dir) then
offset = 0
scrolling = true
end

Good.SetPos(id, x, y)
end
最後的步驟先檢查林克是否走到地圖邊緣,假如是的話設定scrolling全域變數從下一個step開始作地圖捲動,最後一行則是把林克移動到下一個位置。

....(完)

2009年5月1日 星期五

分解字串成Token

偶爾我們會需要自己將字串分解成一個個的Token,對於簡單的需求我們通常都自己來,而不特別使用Tokenizer。使用C語言的話,我們會用strtok這個函式來完成我們的需求,不過我比較偏好C++的作法。
string s("this is a string");
vector<string> v;

v.assign(
  istream_iterator<string>(stringstream(s)),
  istream_iterator<string>()
);
如上所示,執行後v的內容會包含4個Token:this, is, a, string。

有一點要提醒的是,因為stringstream的Ctor會對s作copy的動作而不是直接使用s作為來源,所以當s的是一個很大的字串的話,在效能上會受到影響。

上面的範例在分解字串時是以空白字元作分隔,那假如要使用其它不同的字元作分隔符號該怎麼作呢?

getline可以提供這基本的需求。
stringstream ss(s);
string token;

for (;;)
{
  getline(ss, token, ',');
  if (ss.fail())
    break;

  cout << token << endl;
}
上面這個範例示範使用getline以','字元作分隔符號將字串分解。

+ + +

反向的操作有很多作法,底下舉一個STL的作法。
stringstream ss;

copy(
  v.begin(), v.end(),
  ostream_iterator<string>(ss, " ")
);

string s(ss.str());
stringstream的str方法也會產生一份新的string,所以當字串很大時效能也會受到影響。

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日 星期日

Firefox Plugins入門

底下的內容是研究如何製作Firefox Plugins的學習筆記,記錄在Windows平台上實作外掛的一個簡單方法,這是為了將來要把good嵌到Browser上去所作的準備。在https://developer.mozilla.org/en/Plugins可以得到Firefox Plugins的最新消息。

參考連結

1 :下載SDK

我是在Windows環境開發,所以下載Gecko 1.9 (Firefox 3.0) for Windows版本的SDK。下載完成後,將xulrunner解開至電腦資料夾裡。

下載SDK

2:下載範例程式

下載範例程式

範例要自己手動一個一個檔案下載,將資料夾裡面的檔案除了Makefile.in之外全都下載到你的電腦裡。下載的方式是點擊檔案連結後,在右上角有個Raw File連結,用右鍵點擊這個連結另存新檔。

3:新建VS.NET專案

底下一步步使用VS.NET 2003建立新專案。
  1. 開一個新的VS.NET專案,名稱取作nprt,Application Type選擇DLL,把Empty Project打勾。
  2. 將剛才下載下來的Sample Code全部複製到專案資料夾裡。
  3. 將剛才的檔案都加入到專案裡。
  4. 假設SDK是解到D:\xulrunner-sdk\,則將專案Additional Include Directories設定成:"D:\xulrunner-sdk\include";"D:\xulrunner-sdk\include\plugin";"D:\xulrunner-sdk\include\nspr";"D:\open\xulrunner-sdk\include\java"加入到搜尋路徑上裡。
  5. 將Preprocessor Definitions改成如下內容:WIN32; _WINDOWS; XP_WIN32; MOZILLA_STRICT_API; XPCOM_GLUE; XP_WIN; _X86_; NPSIMPLE_EXPORTS。
  6. 關閉Precomplied Headers功能。
  7. 在Linker/Input/Module Definition File設定為nprt.def。
4:修改程式碼
  1. 將npp_gate.cpp裡的NPP_GetJavaClass函式整個註解掉。
  2. 在Plugin.cpp裡找到函式ScriptablePluginObject::Invoke,將
    if (name == sFoo_id){
    ...
    }
    內容改為
    if (name == sFoo_id){
    MessageBox(NULL, "foo called", "nprt sample", MB_OK);
    }
    (因為原來的Code會讓Firefox當掉)
  3. 編譯產生nprt.dll。
5:測試
  1. 將nprt.dll複製到Firefox的plugins資料夾底下。
  2. 開啟test.html作測試。
+ + +

如果所有步驟都沒出錯的話,應該可以在test.html中間看到一個黑色矩形框,裡面寫了Mozilla/5.0....的一個字串。

這個test.html裡面有些東西似乎是不太對的,其它請自己修修改改玩玩。

如何在 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。


2009年3月31日 星期二

很讚的遊戲編輯器

簡介

good是一個輕量的2D遊戲編輯器,中文名稱叫作很讚的遊戲編輯器!

剛開始的時候其實也沒有想的太多,只想到要作到可以用來編輯鋤草機這個遊戲的一個關卡就夠了。而現在也的確達到了這個目標。廢話不多說,我們直接開始,透過編輯一個簡單的遊戲關卡來看看good能作到什麼,也等於對good作一個很簡單的介紹。


開新專案

首先把good執行起來,可以看到如下圖所示的畫面。


點擊一下左側上半部的資源樹上的Project項目。可以看到資源樹下方的屬性檢視器跟著產生變化。


在Name欄位填上專案的名稱:鋤草機,其它欄位保留預設值不動。你可以發現到編輯器視窗的標題列產生了變化,標題變成了good - (鋤草機),同時工具列上的存檔鈕也致能了。點擊存檔鈕,給一個檔名把專案存到一個你想要的地方。你可以發現,編輯器的標題又改變了,這次加上了專案檔的路徑及檔案名稱。



新增圖形資源


接著我們要新增鋤草機需要使用到的圖形資源,一共是二張圖。點擊工具列上的新增圖形資源鈕叫出視窗介面。


Name欄位填不填無所謂,重點是File欄位。點擊File欄位最右側的鈕叫出開啟舊檔對話盒視窗,挑選我們想要加入的圖形檔案加入到專案裡。


點擊資源樹上的tex2,再點擊一下屬性檢視器上的KeyColor欄位。點擊KeyColor欄位後,在欄位的右側會出現一個小鈕,再點擊這個小鈕叫出顏色選擇介面,接著選擇RGB值為R:253 G:0 B:255的顏色值後按下確定。


以上的動作是因為我們的tex2需要使用到鏤空色的功能而作的鏤空色設定。而tex1(如下圖)只作為靜態背景圖,就不需要作鏤空色的設定。


這裡補充說明一下,每一個資源項目無論是那種類別都會有幾個共同的屬性。從屬性檢視器裡面可以看到最上面的Id以及Name屬性就是共用的屬性。Id是用來辨別不同資源的,每一個Id都是唯一的,對於Id這個屬性以後我們還會再提到。再來是Name屬性,這是個可有可無的欄位,也因此這個欄位也不需保證是唯一的。


編輯地圖

接下來點擊工具列上的新增地圖鈕叫出介面視窗。


我們現在要新增一個6X6個Tile的地圖,所以在Width及Height各填上6。每一個Tile的大小是32X32,這剛好是預設值。最後在Tileset的Texture欄位最右側點擊選擇圖形鈕叫出介面視窗來,選取剛剛加入的tex2圖形資源作為我們用來編輯地圖的Tileset。


點擊下一步,可以看到輔助線設定介面,這邊我們暫時跳過,直接點擊完成。如下圖所示,點擊完成後成功加入一個新的空白地圖。


接著使用地圖編輯器完成如下圖的地圖編輯。


新增精靈

如圖所示,點擊工具列上的新增精靈鈕叫出介面視窗。


Tile Width及Tile Height指定的是精靈的大小,預設值是32X32正好也是我們想要的大小。和新增地圖時一樣,在Tileset的Texture欄位最右側點擊選擇圖形鈕叫出介面視窗來,選取tex2作為我們用來編輯精靈的Tileset。最後點擊完成加入一個新的空白精靈資源。


一個精靈是由一個以上的Frame所組成,每一個Frame包含了顯示Frame所需要的對應到Tileset的Tile編號,以及一個顯示延遲時間。Tile的選擇方法和在編輯地圖一樣,從編輯器上的Tileset檢視區上作選取。可以單選,也可以一次選取一個矩形區域範圍。而顯示延遲時間的單位則是畫格數(Frame)。

我們屬性檢視器裡看過Project項目的屬性,其中有一個叫作Frame Rate的欄位。這個欄位所指定的是每一秒鐘裡面,畫面更新的次數,60表示每秒60個畫面的更新速率。而Frame的顯示延遲時間單位也是以同樣的方法定義,所以一個精靈的Frame顯示延遲時間是30的話,在每秒60個畫面的更新速率設定下,就是顯示0.5秒的意思。相對的如果是在每秒30個畫面的更新速率設定下,就是顯示1秒的意思。

接著加入並編輯以下12個精靈,每個精靈的播放速度都設定為60個Frame。加入的同時,順便指定如圖中的名稱。

完成後如下圖所示。


編輯關卡

終於要進入正題編輯關卡了。編輯關卡需要的操作和編輯地圖並不會有太大差異,一個是在種物件另一個是在塗地磚。

首先點擊工具列上的新增關卡鈕,和前面介紹過的像是地圖資源或精靈資源等的新增操作不同的時,新增關卡時不需要作什麼參數設定,當你一點擊新增關卡鈕立刻就可以新增一個空白的關卡。


完成後如下圖所示,你可以得到一個空白的關卡。關卡的大小預設和Project所設定的視窗大小相同,你可以再關據你的需求作調整。ClearColor屬性是用在當關卡要成像之前使用來作背景清除的一個顏色值,你可以指定也可以不指定。而Script和ScriptParam欄位我們暫時先不去管它,以後會再作說明。


編輯關卡時我們可以在空白關卡上面種植三種類型的物件,使用如下圖中所示的三個鈕,可以分別加入貼圖物件、地圖物件和精靈物件。顧名思義,當我們要種植一個地圖物件到關卡上面時,首先就要從我們已編輯好的地圖裡面選取一個出來,然後種到關卡上面指定的位置。

首先加入一個貼圖物件,完成後如下圖所示。


關卡上的物件常常需要重新定位,需要移動到我們預期的位置。要移動物件是一件很簡單的事,只需使用滑鼠拖曳著物件到喜歡的位置再放開就可以了。或者也可以在點選物件後,在屬性檢視器上改變物件的PosX和PosY欄位值也能達到改變坐標的目的。

和關卡相同,每個物件都擁有Script和ScriptParam欄位,這邊我們也暫時先忽略不看,以後再提。除此之外還可以看到一個叫Visible的屬性,這個屬性是用來指定這個物件在執行時是否是可見的。有一點要注意的是,即使你把Visible屬性設為False不可見,但在編輯器裡仍然是可見的。

如下圖所示,完成我們的第一個關卡。


最後點擊工具列上的紅色驚嘆號鈕,或者由選單Project裡點選Play...,又或者按下快速鍵F5執行我們的關卡看看。你可以發現到不像是編輯器那樣都是靜態的圖形,關卡上的精靈物件都會動了。

可是這遊戲還不能玩啊!這是怎麼回事,從頭到尾都只是在編輯資料,沒見到什麼地方可以編輯GamePlay,這是黑心遊戲編輯器嗎!?的確,其實這個編輯器的確只能編輯資料不能編輯GamePlay,所以把它叫作遊戲編輯器是比較誇大的說法,而且還叫作很讚的遊戲編輯器,真夠黑心。

不過別急,我們還是有辨法可以作GamePlay,只不過你需要寫點Script。我們選擇了Lua來作為good的Script語言。可以在編輯器裡作遊戲邏輯的編輯的話,雖然可以讓編輯器變的更強大,但相對的編輯器就會變複雜了,至少在開發上會變的複雜。所以為了讓事情簡單才作出這樣的決定。也許以為會再加強也說不定。

下回我們再來說明如何使用Script來為我們的關卡加上GamePlay完成這個超小品遊戲!
Related Posts Plugin for WordPress, Blogger...