Introduction of using Isometric Tile

 

1. 什麼是Isometric Tile?

    Isometric Tile以字面上的解釋,是「菱形的磚塊」,而他是什麼時候被用到Game Development上的,我也不太知道。但是我知道,它是目前最受歡迎的地圖元素,也是最難「搞」的地圖元素。大家應該都知道,在Game Development上的地圖建造,都是以一堆小塊的Tile(磚塊)拼湊而成的整個大地圖場景,而Tile的格式就分為很多種了,而Tile的格式也會影響到整個Game Development的Sprite移動機制。我的們可以簡單的分為以下幾種Tile格式:

Tile格式 圖例 特點 代表作
Rectangular Tile 最為傳統也是最簡單的地圖建構方式,而且其座標的表示法和螢幕的一模一樣,非常的Easy,但是用這個建構出來的場景相當單調而且平面化,所才現代2D遊戲中已不常見。(有的遊戲只使用其中四的方向) 炎龍騎士團(RPG)、Final Fantasy太空戰士I~VI(RPG)勇者鬥惡龍(RPG)
Hexagonal Tile 不知道是誰想出來的怪模式,和Rectangular Tile一樣,算是早期的應用,而因為是六邊形(Hexagonal)的關係,所以只有六個方向。發明者可能是因為Rectangular Tile的四邊形Grid太單調,而想出來的吧!它的座標的表示這麼難,又沒什麼視覺上的特殊效果,是個不實用的模式。 精靈幻境(RPG)、Dragon Knight IV龍騎士四戰鬥場景(RPG)
Isometric Tile 目前最為popular的地圖建構模式。因為它有擬似3D的視覺效果,所以被廣為利用在新式的Action Role Playing Game(ARPG)。雖然它的座標表示法很難運算,但是因為他的視覺效果很出色,所以相當的受歡迎哦!(有的遊戲只使用其中四的方向) 仙劍奇俠傳(RPG)、風色幻想(RPG)、Diablo暗黑破壞神(ARPG)、StarCraft星海爭霸(SLG)...

    由上面的表格你是不是已經看出來了?前兩種Tile模式的遊戲都是相當古老的,而Isometric則都是比較新的遊戲所使用的。因為地圖場景的視覺效果在現代遊戲中也是相當重要的一環,而且自從Diablo以這種模式的地圖場景開創Real-time Action-RPG以後,這種模式已能將RPG搬上網路的平台,也成為目前很多網路遊戲的一貫模式,包括Lineage the Blood Pledge、Dragon Raja、Redmoon、以及Fantasy for You,都是使用這樣的模式來達成Multi-player On-line Real-time RPG的遊戲進行方式。

    而其中,Isometric Tile又分為兩種,一種是等角的Isometric Tile,如Dragon Raja(龍族)。另一種則是非等角的Isometric Tile,如上表中的圖就是非等角的Isometric Tile,它的特性是為了表現出進似45度的3D視角而設計的,也是一般大多數遊戲所採納的設計。至於為什麼Dragon Raja(龍族)會使用等角的設計,讓視角拉高,使場景看起來怪怪的呢?這可能要問問作者才知道哩:P

 

2. Isometric Tile地圖的座標

    一個地圖建構模是要是沒有座標的運算法,那跟本就沒法使用,而這大概就是為什麼Isometric Tile是在Hexagonal Tile出現後才發展出來的原因吧。其座標表示法是繼承了Hexagonal的方式,而不是把X-Y軸放斜的。而是,像Rectangular Tile一樣,用的是水平X軸和垂直Y軸,而座標的表示,則是誰Y軸方面有變化,看出來了嗎?為了方便看出來,所以旁邊的圖用顏色來區分一下。而這樣的座標關係,顯然比Rectangular Tile難得多了,因為會用到座標的時候大多是在做Path-finding,而要做Path-finding,則須要找出在地圖中的某一點和它鄰近的點的關係,而Isometric Tile在這方面就比較麻煩一點了,要分為兩種情況來討論。

以(1,4)做為中間點去得到鄰近的8個座標,則:

North: (x, y-2) = (1, 4-2) = (1,2)
Northern East: (x, y-1) = (1, 4-1) = (1,3)
East: (x+1, y) = (1+1, 4) = (2,4)
Southern East: (x, y+1) = (1, 4+1) = (1,5)
South: (x, y+2) = (1, 4+2) = (1,6)
Southern West: (x-1, y+1) = (1-1, 4+1) = (0,5)
West: (x-1, y) = (1-1, 4) = (0,4)
Northern West: (x-1, y-1) = (1-1, 4-1) = (0,3)

以(1,5)做為中間點去得到鄰近的8個座標,則:

North: (x, y-2) = (1, 5-2) = (1,3)
Northern East: (x+1, y-1) = (1+1, 5-1) = (2,4)
East: (x+1, y) = (1+1, 5) = (2,5)
Southern East: (x+1, y+1) = (1+1, 5+1) = (2,6)
South: (x, y+2) = (1, 5+2) = (1,7)
Southern West: (x, y+1) = (1, 5+1) = (1,6)
West: (x-1, y) = (1-1, 5) = (0,5)
Northern West: (x, y-1) = (1, 5-1) = (1,4)

 

以上,是使用Isometric Tile的座標關係式。我們可以很容易的發現,「綠色字體」的座標關係式有變化,而這個變化來自於中心點的Y座標,所以我們在整合歸納後,可以利用判斷中心點的Y座標是奇數或是偶數來決定鄰近點的關係式,假設center(x,y)為中心點,則各方面的關係式為:

// north
north.x = center.x;
north.y = center.y-2;
// northern-east
northern_east.x = (center.y%2==0)?center.x:center.x-1;
northern_east.y = center.y-1;
// east
east.x = center.x+1;
east.y = center.y;
// southern-east
southern_east.x = (center.y%2==0)?center.x:center.x+1;
southern_east.y = center.y+1;
// south
south.x = center.x;
south.y = center.y+2;
// southern-west
southern_west.x = (center.y%2==0)?center.x-1:center.x;
southern_west.y = center.y+1;
// west
west.x = center.x-1;
west.y = center.y;
// northern-west
northern_west.x = (center.y%2==0)?center.x-1:center.x;
northern_west.y = center.y-1;

 

3. Isometric Tile格式

對一個以Isometric Tile構成的地圖而言,地圖上的每一個Tile就好像是細胞一樣,所以地圖細胞Map Cell也就代表著一個Tile。現在就讓我們來看看一個Map cell的pixel格式,當然,這並不是硬性規定一定要以4:3的螢幕比來建構一個Map cell,但是之前說過了,這樣的比例是黃金比例,是整體感覺上Viewport最適當的角度,才不會像Dragon Raja(龍族)一樣,仰角過高,看起來很怪。

所以,我們就可以用這樣的比例,來做一個40x30的圖,如圖:

接下來我們用比較精確的表示,也就是在程式中,為了產生一個Map Cell所設定的MaskBits:

#define TILEWIDTH 40
#define TILEHEIGHT 30

char BaseTileMask[TILEHEIGHT][TILEWIDTH]={
    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, // 0
    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, // 1
    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, // 2
    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, // 3
    {0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0}, // 4
    {0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0}, // 5
    {0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0}, // 6
    {0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0}, // 7
    {0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0}, // 8
    {0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0}, // 9
    {0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0}, // 10
    {0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0}, // 11
    {0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0}, // 12
    {0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0}, // 13
    {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}, // 14
    {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}, // 15
    {0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0}, // 16
    {0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0}, // 17
    {0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0}, // 18
    {0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0}, // 19
    {0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0}, // 20
    {0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0}, // 21
    {0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0}, // 22
    {0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0}, // 23
    {0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0}, // 24
    {0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0}, // 25
    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, // 26
    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, // 27
    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, // 28
    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}};// 30

4. 地圖pixel座標找MapCell座標小技巧

這是在實做上一定會遇到的問題,因為地圖上的滑鼠指標傳回程式的座標是「pixel座標」,而我們所關心的是:被滑鼠點到的「pixel 座標」落在哪一個「MapCell座標」。如圖:

先解釋一下名詞:

MapSize_by_Tile是以MapCell座標來看的地圖大小:

    MapSize_by_Tile.Width則為其寬度,由上圖可知MapSize_by_Tile.Width=3

    MapSize_by_Tile.Height則為其高度,由上圖可知MapSize_by_Tile.Height=9

MapSize_by_Pixel是以Pixel座標來看的地圖大小。而它的大小必須利用MapSize_by_Tile運算而得:

    MapSize_by_Pixel.Width= MapSize_by_Tile.Width*TILEWIDTH+(TILEWIDTH/2) 

                                            = 3 * 40 + (40/2) = 140 pixels

    MapSize_by_Pixel.Height= (MapSize_by_Tile.Height%2==0)?

                                                (MapSize_by_Tile.Height/2)*TILEHEIGHT+(TILEHEIGHT/2) :

                                                (MapSize_by_Tile.Height/2+1)*TILEHEIGHT

                                            = (9/2 + 1)*30 = 150 pixels

而在上圖中,箭頭所指的地方表示滑鼠指到的地方,現在我們就做一個函數,參數為滑鼠所指的(x,y)pixel座標,而傳回來MapCell座標。

在做之前,我在這裡先解釋一定設計的觀念。要達成目的的方法有兩種,一種是循序的詢問每個MapCell座標,但是這個方法有個致命的缺點:「在上圖的小地圖中,此方法最多要重覆運算3x9=27次;如果遇到特大號地圖1000x1000,則最多要重覆運算1000,000次。」這是很笨的方法,所以筆者我就想到了一個比較快的方法,如上圖中滑鼠所在的區域,以下列方法運算可得到Prediction_MapCell(也是是預測出來的,不是一定等於結果):

checkpt[0].x=pt.x/TILEWIDTH;
checkpt[0].y=pt.y/(TILEHEIGHT/2);

上式中的checkpt[0]是一個POINT的型態,將代表著預測出來的MapCell座標,而本著「雖不中,亦不遠矣」的中心思想,我們必須找出上圖中所標示的1~8的MapCell座標,而且分別以checkpt[1]~checkpt[8]來表示。上圖中的虛線所及的範圍,就是滑鼠座標在預測後,所含概到的MapCell範圍,一共有九個(即0~8),所以以這樣的方法來運算,就算是碰上特大號地圖,最多也只運算9次就可以找到滑鼠所指的那塊MapCell。我們把這個觀念寫成程式,就成了以下這個函數:

POINT GetMapCell(POINT mouse)
{
    POINT checkpt[9];

    // center
    checkpt[0].x=mouse.x/TILEWIDTH;
    checkpt[0].y=mouse.y/(TILEHEIGHT/2);

    // north
    checkpt[1].x=checkpt[0].x;
    checkpt[1].y=checkpt[0].y-2;

    // northern-east
    checkpt[2].x=(checkpt[0].y%2==0)?checkpt[0].x:checkpt[0].x-1;
    checkpt[2].y=checkpt[0].y-1;

    // east
    checkpt[3].x=checkpt[0].x+1;
    checkpt[3].y=checkpt[0].y;

    // southern-east
    checkpt[4].x=(checkpt[0].y%2==0)?checkpt[0].x:checkpt[0].x+1;
    checkpt[4].y=checkpt[0].y+1;

    // south
    checkpt[5].x=checkpt[0].x;
    checkpt[5].y=checkpt[0].y+2;

    // southern-west
    checkpt[6].x=(checkpt[0].y%2==0)?checkpt[0].x-1:checkpt[0].x;
    checkpt[6].y=checkpt[0].y+1;

    // west
    checkpt[7].x=checkpt[0].x-1;
    checkpt[7].y=checkpt[0].y;

    // northern-west
    checkpt[8].x=(checkpt[0].y%2==0)?checkpt[0].x-1:checkpt[0].x;
    checkpt[8].y=checkpt[0].y-1;

    // starting check.....
    for(int i=0;i<9;i++)
    {
        if(checkpt[i].x>=0 && checkpt[i].y>=0 
            && checkpt[i].x<iMapWidth && checkpt[i].y<iMapHeight)
        {
            if( PtInRegion( (*(map+checkpt[i].y)+checkpt[i].x)->region, mouse.x, mouse.y )!=0 )
            {
              return checkpt[i];
            }
        }
    }
    return (POINT)-1; // not found
}