手机平台上开发斜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的若干个区域,通过“遍历”这些区域的坐标,可以用公式知道,要在这个坐标上画哪张图片。算法如下:
-
- 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));
- }