手機平台上開發斜45度地圖系統的遊戲,相信做慣了正面俯視的開發者剛接觸總很不習慣。所謂斜45度遊戲,也就是常說的2.5D遊戲,用斜方向俯視的角度來增強立體感的一種技術。這種技術在PC平台上早就流行了,手機平台由于螢幕表現力的限制,大部分使用正面視角。但随着手機螢幕分辨率不斷增大,斜45度視角的遊戲出現得越來越多。
斜45度地圖系統分Staggered、Slide、Diamond等幾種,除了起始位置的差別,與正視地圖系統的主要差別在于使用菱形的圖塊。關于什麼是45度地圖系統以及其原理,我不想再多說,網上有很多的資料,下面主要講一講坐标系的轉換。

圖1
圖2(該圖來自雲風的部落格http://www.codingnow.com/)
如圖1,雖然圖塊是菱形拼接,但圖檔還是矩形,繪制地圖就是要矩形的圖檔映射到正确的螢幕坐标系上,使圖檔無縫地拼接起來。如圖2,以Diamond地圖系統為例,為了簡化,掠過滾屏等問題,假設始終從螢幕左上角開始繪制0,0的資料,圖中所标的數字是指圖塊資料下标(data[j][i]中的(j,i))。通過觀察你會發現:圖檔的X位置為資料j,i之差乘以圖檔寬度的一半,Y位置為資料j,i之和乘以高度的一半,即公式1:
px = (CHIP_W >> 1) * (i - j);
py = (CHIP_H >> 1) * (i + j);
通過公式1,我們就能根據資料數組的下标值獲得對應圖塊在螢幕坐标系中的位置。逆推,得到公式2:
i = 0.5f * (y / (CHIP_H >> 1) + x / (CHIP_W >> 1));
j = 0.5f * (y / (CHIP_H >> 1) - x / (CHIP_W >> 1));
通過公式2,可以把圖塊位置映射到對應的資料坐标。
下面開始繪制地圖内容。傳統的俯視角度遊戲地圖繪制,都是記錄螢幕(或錄影機)所在的資料j,i位置,計算螢幕能容納多少個圖塊,用一個嵌套循環完成地圖的繪制。如下所示:
for(int j=0;j<SCREEN_HEIGHT/CHIP_H;j++)
{
for(int i=0;i<SCREEN_WIDTH/CHIP_W;i++)
{
......//do draw
}
}
圖3
但是如圖3所示,斜45度地圖是斜的,無法用數組的循環周遊來繪制。不過既然有了轉換公式,我們可以把螢幕分割成CHIP_W/2*CHIP_H/2的若幹個區域,通過“周遊”這些區域的坐标,可以用公式知道,要在這個坐标上畫哪張圖檔。算法如下:
//當paintY為CHIP_H / 2的奇數倍時,paintX需要偏移CHIP_W / 2
int offset = 0;
for (int paintY = 0; paintY <= SCREEN_H + CHIP_H; paintY += CHIP_H / 2)
{
for (int paintX = 0; paintX <= SCREEN_W + CHIP_W; paintX += CHIP_W)
{
int gx = getGx(paintX + offset, paintY) + startCol;
int gy = getGy(paintX + offset, paintY) + startRow;
if (gy < 0 || gx < 0 || gy > 10 || gx > 10)
{
continue;
}
drawTile(g, data[gy][gx], paintX + offset, paintY);
}
offset = offset == 0 ? CHIP_W / 2 : 0;
}
//螢幕坐标轉換成遊戲坐标
int getGx(int x, int y)
{
return (int) (0.5f * (y / (CHIP_H >> 1) + x / (CHIP_W >> 1)));
}
int getGy(int x, int y)
{
return (int) (0.5f * (y / (CHIP_H >> 1) - x / (CHIP_W >> 1)));
}
關于地圖上的碰撞與拾取。當你需要判斷精靈所處地圖資料位置的時候用公式2,會發現判斷誤差很嚴重,仔細想一下不難解釋:逆推過來的公式是根據圖檔之間的坐标系來定義的,而菱形切片之間透明的部分都是重疊的,是以當你判斷重疊部分的碰撞時就無法預計判斷是那一塊資料了,是以用公式2進行地圖資料的碰撞判斷不可行。
圖4
圖5 圖6
介紹一下我的碰撞方法。如圖4,要判斷螢幕中任意一點X,Y于資料中的位置。首先按圖塊的圖檔尺寸将螢幕分割,計算X,Y位于圖中綠色矩形框選的哪個圖檔中,初步得到j,i。知道了位于哪張圖塊圖檔中,如圖5,再判斷X,Y位于圖5中四個角落哪一個區域。結合圖6,如果都不位于這四個角落,那X,Y就屬于j,i。位于左上角的紅色區域,對應i-1,其它三個角落同理。這裡的難點在于如何判斷紅色區域,建議用三角形與點碰撞的算法。這種碰撞拾取算法的優點是精确無誤差,而且無論菱形圖塊比例是32:15 、2:1還是其他比例都可以檢測。
資料碰撞檢測的主要代碼:
int[] checkInDataIJ(int x, int y)
{
final int I = 0;
final int J = 1;
int[] data = new int[] { 0, 0 };
Log.e("", "click:" + x + "," + y);
int xd = x / CHIP_W;
int yd = y / CHIP_H;
if (x < 0)
{
xd -= 1;
}
if (y < 0)
{
yd -= 1;
}
Log.e("", "xd:" + xd + " yd:" + yd);
data[I] = yd + xd;
data[J] = yd - xd;
//計算觸摸點位于矩形中,與菱形的位置
int cx = x % CHIP_W;
if (cx < 0)
{
cx += CHIP_W;
}
int cy = y % CHIP_H;
if (cy < 0)
{
cy += CHIP_H;
} //是否位于左上角的三角形
if (MyMath.isInsideTriangle(cx,
cy,
new int[] { 0, CHIP_W / 2, 0 },
new int[] { 0, 0, CHIP_H / 2 }))
{
data[I] -= 1;
}
//是否位于右上角的三角形
else if (MyMath.isInsideTriangle(cx, cy, new int[] { CHIP_W / 2,
CHIP_W, CHIP_W }, new int[] { 0, 0, CHIP_H / 2 }))
{
data[J] -= 1;
}
//是否位于右下角的三角形
else if (MyMath.isInsideTriangle(cx, cy, new int[] { CHIP_W, CHIP_W,
CHIP_W / 2 }, new int[] { CHIP_H / 2, CHIP_H, CHIP_H }))
{
data[I] += 1;
}
//是否位于左下角的三角形
else if (MyMath.isInsideTriangle(cx,
cy,
new int[] { 0, CHIP_W / 2, 0 },
new int[] { CHIP_H / 2, CHIP_H, CHIP_H }))
{
data[J] += 1;
}
Log.e("debug", "get(:" + data[J] + "," + data[I] + ")");
return data;
}
三角形檢測算法:
public static boolean isInsideTriangle(int cx, int cy, int[] x, int[] y)
{
float vx2 = cx - x[0];
float vy2 = cy - y[0];
float vx1 = x[1] - x[0];
float vy1 = y[1] - y[0];
float vx0 = x[2] - x[0];
float vy0 = y[2] - y[0];
float dot00 = vx0 * vx0 + vy0 * vy0;
float dot01 = vx0 * vx1 + vy0 * vy1;
float dot02 = vx0 * vx2 + vy0 * vy2;
float dot11 = vx1 * vx1 + vy1 * vy1;
float dot12 = vx1 * vx2 + vy1 * vy2;
float invDenom = 1.0f / (dot00 * dot11 - dot01 * dot01);
float u = (dot11 * dot02 - dot01 * dot12) * invDenom;
float v = (dot00 * dot12 - dot01 * dot02) * invDenom;
return ((u > 0) && (v > 0) && (u + v < 1));
}