斜視角遊戲地圖解析 |
|
axsoft
版主 發表:681 回覆:1056 積分:969 註冊:2002-03-13 發送簡訊給我 |
Isometric Game Map作者:劉小軍 資料來源: http://www.cpp3d.com 關于斜視角方面的文章很多,之所以寫這組文章,一方面是對本站點的內容作一些補充,另一方面我們一直努力朝游戲這一方向靠近,能在同大家交流的同時,學到更多的東西才是最重要的,所以大家如有什麼高見,千萬千萬不要吝嗇批評指教。 圖塊的組織 大家都知道,游戲中的場景(map)可通過有限數量的一些圖塊(tile)拼接形成,就象磁磚畫一樣。游戲中採用的圖塊形狀有兩種,一種是矩形的,另一種是菱形的,由此就有了兩種不同的引擎,兩種引擎誰優誰劣這里就不做討論,但是有一點可以肯定,兩種方法暫時誰也無法絕對地否定誰。用少量的圖塊來構造一個較大的場景,這樣做的好處是顯然的,比如減少內存(顯存)的消耗、方便地計算從一處走到另一處所要消耗的時間或體力(通過率)、物體間的遮掩、動態場景的實現等等,你可以通過定義圖塊的屬性來方便地完成一些現實模擬計算。 斜視角所採用的圖塊是一些上、下、左、右都對稱的菱形,橫向寬度與縱向寬度之比為2:1(如圖一),實際中常用的是寬為32個象素點高為16個象素點的矩形圖塊,右圖中四周黑色區域是透明的,大家如果細心一點一定可以看到,底部一行象素點也是透明的,也就是說實際高度是15個象素點,為什麼呢? 圖一 圖二 我們再來看看圖二,從當前塊位置右移16點,再向下移8個象素點就是右下方圖塊的位置,16和8分別為橫向寬和縱向高的一半!16和8分別為2的4次方和2的3次方!這對于圖塊的拼接和優化算法是多麼可愛的兩個數字。 接下來就是如何來給每一個圖塊定位,這也有很多方法,最早的RPG採用的方法是以左上角到右下角為X軸方向,Y軸方向為右上角到左下角方向。這樣編號簡單是簡單,不過場景的四個角落就成了黑暗區域,這多少有些美中不足。 另一種方法是直接用屏幕坐標方向來進行編號,由于圖塊是交錯排列的,這給編號帶來一定困難。目前常用的也有兩種,一種是分奇偶行排列,另一種是將奇偶行並成一行(如圖三)。 圖三 現在把所有橫坐標為奇數的圖塊拿走(如圖四),這樣是不是與我們的直視角有些類似,這幅圖對我們又有些什麼樣的啟發呢?未完待續…… 約定:以后文章中提到的圖塊將都是32X16大小,圖塊的排列也採用圖三的編號方式 坐標換算 IsometricView演示 (470K) IsometricView演示源碼 (359K) 坐標換算是斜視角引擎中首要解決的問題之一,它是場景渲染(地圖拼接)的前提。這也就是為什麼斜視角要比直視角計算開銷要大的原因之一。前面我們把奇數列的圖塊拿走后,看到的是一個類似直視角這樣一個場景地圖(如左圖)。可能大家早就想到了可以用類似直視角的方法來進行坐標換算。比如地圖中的某一點,它的絕對象素坐標為(x, y),我們要算出它的絕對圖塊坐標(tx, ty)。首先該點一定是在如左上圖所示的某個矩形區域中,假設是塊(2,1)所在的矩形里,這很好計算。其次該點在該矩形里可能落在五個區域內:菱形內和菱形外其它四個角落,這也很好確定。最后我們根據菱形的前后左右關系,就可以很方便地確定出該點所在圖塊的編號了。具體算法如下:const CHAR CDXIsoMap::m_CellFigue[16][32] = { {1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,2}, {1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,2,2,2,2,2,2,2,2,2,2,2,2}, {1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,2,2,2,2,2,2,2}, {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,2,2,2,2,2,2,2}, {1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,2,2,2}, {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,2,2,2,2}, {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,2,2}, {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}, {3,3,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,4,4}, {3,3,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,4,4,4}, {3,3,3,3,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,4,4,4,4,4}, {3,3,3,3,3,3,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,4,4,4,4,4,4,4}, {3,3,3,3,3,3,3,3,3,3,0,0,0,0,0,0,0,0,0,0,0,0,4,4,4,4,4,4,4,4,4,4}, {3,3,3,3,3,3,3,3,3,3,3,3,0,0,0,0,0,0,0,0,4,4,4,4,4,4,4,4,4,4,4,4}, {3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,0,0,0,4,4,4,4,4,4,4,4,4,4,4,4,4,4}, {3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4} }; const int CDXIsoMap::m_nCellWidth = 32; const int CDXIsoMap::m_nCellHeight = 16; const int CDXIsoMap::m_nCellWidthShift = 5; const int CDXIsoMap::m_nCellHeightShift = 4; // 根據象素坐標計算出絕對圖塊坐標 // int nPixelX 橫向象素坐標 // int nPixelY 縱向象素坐標 // int nCellX 絕對橫向圖塊坐標 // int nCellY 絕對縱向圖塊坐標 // BOOL bAbsolutePixel 傳入的象素坐標是否是絕對坐標 void CDXIsoMap::AbsoluteCell(int nPixelX, int nPixelY, int &nCellX, int &nCellY, BOOL bAbsolutePixel/*=TRUE*/) { // 先計算出傳入象素點落在那個(偶數列)圖塊所在的矩形區域內 if (bAbsolutePixel) // 是絕對坐標 { nCellX = (nPixelX >> m_nCellWidthShift) << 1; nCellY = nPixelY >> m_nCellHeightShift; } else // 是屏幕坐標,則要先換算成絕對坐標后再計算 { nCellX = (((nPixelX-m_rcDest.left) m_nPosX) >> m_nCellWidthShift) << 1; nCellY = ((nPixelY-m_rcDest.top) m_nPosY) >> m_nCellHeightShift; } // 計算傳入點在小矩形區域內的相對位置 int nPx, nPy; if (bAbsolutePixel) // 絕對坐標 { nPx = nPixelX & (m_nCellWidth-1); nPy = nPixelY & (m_nCellHeight-1); } else // 相對坐標 { nPx = ((nPixelX-m_rcDest.left) m_nPosX) & (m_nCellWidth-1); nPy = ((nPixelY-m_rcDest.top) m_nPosY) & (m_nCellHeight-1); } switch( m_CellFigue[nPy][nPx] ) { case 1: nCellX--; nCellY--; break; // 左上角 case 2: nCellX ; nCellY--; break; // 右上角 case 3: nCellX--; break; // 左下角 case 4: nCellX ; break; // 右下角 } }注:上面的m_rcDest是相對于屏幕的一個視窗(view port),也就 是說地圖只在屏幕中的某個矩形區域內滾動。另外m_nPosX和 m_nPosY是該矩形視窗左上角的絕對象素坐標。 前半段我們解決了如何計算給定點所在圖塊的編號。這在接下來的圖塊拼接中起著非常重要的作用。在此之前還有一個小問題,就是如果給定一個圖塊編號,如何計算它的絕對象素坐標或相對象素坐標呢?又如何計算其它相鄰圖塊的編號呢?這在以后AI中路徑搜索等其它一些場合要用到,雖然暫時還不會用到,但因屬坐標計算範疇,所以提前討論了。 注:因為文章中很多還只是處于構思階段,大部分我自己也沒有來得及實現,因此如果前后文有所衝突,還請多多包涵。這里我沒有任何想誤人子弟的意思,如何取舍,請自行定奪。當然隨時不要忘記指出錯誤!! 先看看如何將絕對圖塊坐標換算成絕對象素坐標,偷一下懶,還是用前一個圖吧,我們已經知道了偶數列的圖塊有左圖這樣一個類似直視角的性質。那麼同前面討論的一樣,如果給定的圖塊在偶數列,直接計算計算即可,如果是奇數列圖塊,則還需在相應偶數列圖塊的坐標上加上半個圖塊的偏移,具體看下面一段代碼: // 根據絕對圖塊坐標計算出象素坐標 // int nCellX 絕對橫向圖塊坐標 // int nCellY 絕對縱向圖塊坐標 // int nPixelX 橫向象素坐標 // int nPixelY 縱向象素坐標 // BOOL bAbsolute 計算絕對象素坐標還是相對(屏幕)象素坐標 void CDXIsoMap::Cell2Pixel(int nCellX, int nCellY, int &nPixelX, int &nPixelY, BOOL bAbsolute/*=TRUE*/) { // 先計算偶數列圖塊的絕對象素坐標 nPixelX = (nCellX >> 1) << m_nCellWidthShift; nPixelY = nCellY << m_nCellHeightShift; // 計算圖塊的絕對象素坐標 if (nCellX & 1) { nPixelX = (m_nCellWidth >> 1); nPixelY = (m_nCellHeight >> 1); } if (!bAbsolute) { // 計算相對(屏幕)坐標 nPixelX -= m_nPosX; nPixelY -= m_nPosY; } }然后再來看看如何計算相鄰圖塊的編號,圖塊的編號有兩種情況, 一種是位于奇數列,另一種是位于偶數列(如左、右圖)。那麼我們計算時只要區分兩種情況就行了,參照圖示,代碼實現很簡單: // 計算相鄰圖塊編號 // 方向: // 3 4 5 // 2 6 // 1 0 7 void CDXIsoMap::NextCell(int &x, int &y, int nDirection) { if( x & 1) // 奇數列(如左上圖) { switch( nDirection ) { case 0 : y ; break; // 下 case 1 : x--; y ; break; // 左下 case 2 : x-=2; break; // 左 case 3 : x--; break; // 左上 case 4 : y--; break; // 上 case 5 : x ; break; // 右上 case 6 : x =2; break; // 右 case 7 : x ; y ; break; // 右下 } } else // 偶數列(如右上圖) { switch( nDirection ) { case 0 : y ; break; // 下 case 1 : x--; break; // 左下 case 2 : x-=2; break; // 左 case 3 : x--; y--; break; // 左上 case 4 : y--; break; // 上 case 5 : x ; y--; break; // 右上 case 6 : x =2; break; // 右 case 7 : x ; break; // 右下 } } }地圖數據的組織 現在我們要來討論一下如何組織圖塊單元的屬性。斜視角與直視角還有一個本質的區別就是用于拼接地圖的圖塊都是有高度的。通過這一屬性可以方便地實現遮掩算法。 這里我們先來看看一些傳統RPG(如仙劍)中地圖格式,一般它們的地圖都分成兩層或三層,第一層我們稱為基本圖塊層(base layer),如草地、路等,這一層是由一些完整的菱形圖塊組成。第二層為邊緣圖塊層(fringe layer),這一層中的圖塊主要都是一些不完整的菱形圖塊,如樹木、花草、石子等,透過這些圖塊縫隙可以看到第一層(base bayer)的圖形,這也就是實現遮掩效果的一大關鍵。如右圖,房子的牆角有些圖塊只有半個菱形,這就是所謂的邊緣圖塊,這些邊緣圖塊(牆角)覆蓋在其它基本圖塊(草地)之上,在當人物走到牆角時,你看到的就只有沒被遮住的半個人(人物遮掩的具體方法放在后面介紹),再舉一個例子就是長在草地上的小花,當人走在這里時,花草會將人的腳遮住。有些游戲還有第三層,用于存放一些可以拾起的道具,如鑰匙、藥品等,稱之為(object layer)。 “仙劍”的地圖單元每個占一個字長,第一個字節是圖塊單元的編號,第二個字節是些圖塊的標志屬性,其中低4位是此單元的高度,第5位至第8位依次為圖塊單元的最高位(與第一個字節組成圖塊單元編號)、障礙標志(人物是否可通過)、人物標志(此圖塊上是否有人站在上面)、地圖鏈接標志(當人走到這里可切換到其它地圖)。各位含義見下表: 地圖單元格式 ============================ bit 15 地圖鏈接標志 bit 14 人物標志 bit 13 障礙標志 bit 12 編號高位 bit 8~11 單元高度 bit 0~7 圖塊單元編號低8位 ====================================== 從上表我們可以看出圖塊編號占9位,也就是說構成地圖的圖塊最多不超過511塊,對于早期的game來說,內存資源相當緊張,現在就不一樣了,你可以用更豐富的圖塊來構造你的地圖,對上面的格式作適當的擴張不僅可提高游戲的速度,更主要的是可以使游戲場景更加豐富多彩。方便起見,我暫時仍沿用上表所示格式。 struct TILE { BYTE index; BYTE flag; }; struct CELL { TILE base; TILE fringe; }; class CDXIsoMap { . . . . . . Static const BYTE m_CellIdxFlg;// 圖塊索引高位 static const BYTE m_CellRmrFlg;// 障礙標志 static const BYTE m_CellNpcFlg;// 人物標志 static const BYTE m_CellLnkFlg;// 地圖鏈接標志 CELL* m_pMapData; // 地圖數據圖塊拼接 前面各文章其實都可以說是斜視角地圖實現的基礎,有了前面一些基礎知識之后,下面我們就可以開始做場景的渲染了。我們已經知道,各圖塊都是交錯排列的,所以畫圖塊時要適當考慮周邊的情形,否則四周就會出現鋸齒效果。如圖中,如果從圖塊(2,1)開始畫起,則圖塊(3,0)、(5,0)……以及(1,1)、(1,2)……等就會被漏畫。其實一開始我就已經提到過,所有斜線方向相鄰的圖塊其位置都相差半個圖塊的寬高,我們這里分別為16和8,如果你還沒概念的話再看看本組文章的第一篇。 圖塊的blit次序是與圖塊編號一致的,為了不產生鋸齒效果,每次都要多畫一些圖塊,這里只需將渲染區域適當加大一些就可以了。為了方便計算,我把起始圖塊向左上方向移動一些,舉個例子來說,如果起始圖塊為(3, 4),則從圖塊(2, 4)blit起,如果起始圖塊為(2, 4),則從圖塊(0, 4)blit起,也就是說始終從偶數列開始blit。 大家可能已經看得云里霧里了,我寫這一節是非常賣力的,希望大家能體諒我的辛苦,本來我就不善言詞,誰讓我經不住OC的威逼利誘呢。建議大家還是看代碼可能會更清楚一些: // 畫地圖(“畫地圖”?) // IN: // pDestSurface: Destination surface // lprcDest: View port void CDXIsoMap::Draw(CDDSurface* pDestSurface, LPRECT lprcDest) { if (lprcDest != NULL) { m_rcDest.top = lprcDest->top; m_rcDest.left = lprcDest->left; m_rcDest.bottom = lprcDest->bottom; m_rcDest.right = lprcDest->right; } // 圖塊半寬、半高 int nHalfW = m_nCellWidth >> 1; int nHalfH = m_nCellHeight >> 1; int nOffsetX; int nOffsetY; // 計算相應偶數例圖塊與視窗的距離 // 視窗左上角處為奇數列 if (m_nPosTX & 1) { nOffsetX = (m_nPosX-nHalfW) & (m_nCellWidth-1); nOffsetY = (m_nPosY-nHalfH) & (m_nCellHeight-1); } else { nOffsetX = m_nPosX & (m_nCellWidth-1); nOffsetY = m_nPosY & (m_nCellHeight-1); } nOffsetX = -nOffsetX; nOffsetY = -nOffsetY; int nOffTx, nOffTy; // 左上方多畫一些圖塊 // 如果視窗左上角處是奇數列,則從相應偶數列畫起 if (m_nPosTX & 1) { nOffTx = m_nPosTX - 1; nOffTy = m_nPosTY; nOffsetX = nOffsetX - nHalfW; nOffsetY = nOffsetY - nHalfH; } // 否則起如列左移兩列,起始行上移一行 else { nOffTx = m_nPosTX - 2; nOffTy = m_nPosTY - 1; nOffsetX = nOffsetX - m_nCellWidth; nOffsetY = nOffsetY - m_nCellHeight; } int dy = nOffsetY m_rcDest.top; // 行循環 for (int I = nOffTy; ; I ) { if ((I>=0) && (I < m_nRows)) { int dx = nOffsetX m_rcDest.left; // 列循環 for (int j= nOffTx; ;) { // 畫偶列 if ((j>=0) && (j < m_nColumns)) BltTile(pDestSurface, dx, dy, &(m_pMapData[I*m_nColumns j])); j ; // 在右下方半個圖塊位置畫奇列 if ((j>=0) && (j < m_nColumns)) BltTile(pDestSurface, dx nHalfW, dy nHalfH, &(m_pMapData[I*m_nColumns j])); j ; dx = m_nCellWidth; if (dx >= m_rcDest.right) break; } } dy = m_nCellHeight; if (dy >= m_rcDest.bottom) break; } }人物遮掩 人物的遮掩分兩種情況, 一種是人物與人物之間的遮掩, 還有一種是人物與地圖中的建築、樹木等之間的遮掩。注:方便起見,所有精靈類這里統稱人物。第一種情況的解決辦法是通過構造一個具有位置屬性的基類,再在此基礎上引申出其它精靈類,這樣我們就可以在視覺方向對人物的位置進行排序,從遠到近畫出各人物就實現了人物的遮掩。當然排序算法看你自己喜歡了。 至于第二種情況,大家一定還記得我們前面說過圖塊是有高度的,而圖塊的高度又是如何來定義的呢?我們來看看右邊這幅圖,一般建築物圖塊的高度的是與現實是一致的,圖中的房子從牆角往上高度依次為1、2、3……,這在我們編輯場景時就必須定義好的。人物也是有高度的,它的高度是從下往上依次遞增(如圖中的右半部分),這與地圖中圖塊的編號有些類似,只不過是上下倒過來罷了。不知大家現在有沒有看出什麼名堂來,對了,這些高度就是為了確定圖塊遮掩,我們渲染場景時一般先畫地圖,接著畫人物,因為可能人物被建築物遮住了,這時就要重畫人物位置的部分地圖場景,前面講的高度現在可派上用場了,從下往上依次比較人物與圖塊的高度,如圖塊的高度大于人物的高度則圖塊遮住人物,此圖塊要重畫,否則是人物遮住圖塊,不須重畫圖塊。大家可仔細對照圖中屋前屋后兩個人的遮掩效果。 寫到這里我對斜視角與直視角之間的優劣有許多新的想法,比如現實的表現力度方面是否直視角天生就不如斜視角呢?如果大家有興趣的話,歡迎來信談談你的看法,還是那句話,千萬千萬不要吝嗇批評指教。 全文完。 網路志工聯盟----Visita網站http://www.vista.org.tw ---[ 發問前請先找找舊文章 ]--- 發表人 - axsoft 於 2002/08/21 14:44:08 |
本站聲明 |
1. 本論壇為無營利行為之開放平台,所有文章都是由網友自行張貼,如牽涉到法律糾紛一切與本站無關。 2. 假如網友發表之內容涉及侵權,而損及您的利益,請立即通知版主刪除。 3. 請勿批評中華民國元首及政府或批評各政黨,是藍是綠本站無權干涉,但這裡不是政治性論壇! |