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
| 一個地圖建構模是要是沒有座標的運算法,那跟本就沒法使用,而這大概就是為什麼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;
對一個以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
這是在實做上一定會遇到的問題,因為地圖上的滑鼠指標傳回程式的座標是「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
}