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,所以當字串很大時效能也會受到影響。
Related Posts Plugin for WordPress, Blogger...