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新建了一個簡單的論壇
開了一個主題作為編輯器第一個版本的發佈
Related Posts Plugin for WordPress, Blogger...