在 2D 游戏中绘制等距图块的正确方法是什么?
我已阅读参考资料(例如this one),这些参考资料建议以一种将地图二维数组表示中的每一列锯齿形呈现的方式呈现图块。我想它们应该更多地以菱形方式绘制,在屏幕上绘制的内容与 2D 阵列的外观更密切相关,只需旋转一点。
两种方法都有优点还是缺点?
更新:更正了地图渲染算法,添加了更多插图,更改了格式。
或许“之字形”技术用于将图块映射到屏幕的优势可以说是图块x
和y
坐标位于垂直轴和水平轴上。
“绘制钻石”方法:
通过使用“绘制菱形”绘制等距地图,我认为这指的是通过for
在二维数组上使用嵌套循环来渲染地图,例如这个例子:
tile_map[][] = [[...],...]
for (cellY = 0; cellY < tile_map.size; cellY++):
for (cellX = 0; cellX < tile_map[cellY].size cellX++):
draw(
tile_map[cellX][cellY],
screenX = (cellX * tile_width / 2) + (cellY * tile_width / 2)
screenY = (cellY * tile_height / 2) - (cellX * tile_height / 2)
)
优势:
该方法的优点是它是一个简单的嵌套for
循环,具有相当直截了当的逻辑,可以在所有图块中始终如一地工作。
坏处:
这种方法的一个缺点是地图上瓦片的坐标x
和y
坐标会在对角线上增加,这可能会使将屏幕上的位置直观地映射到表示为数组的地图上变得更加困难:
然而,实现上面的示例代码会有一个陷阱——渲染顺序将导致应该在某些图块后面的图块被绘制在前面的图块之上:
为了修正这个问题,内部for
循环的顺序必须颠倒——从最高值开始,向着较低的值渲染:
tile_map[][] = [[...],...]
for (i = 0; i < tile_map.size; i++):
for (j = tile_map[i].size; j >= 0; j--): // Changed loop condition here.
draw(
tile_map[i][j],
x = (j * tile_width / 2) + (i * tile_width / 2)
y = (i * tile_height / 2) - (j * tile_height / 2)
)
通过上述修复,应该更正地图的渲染:
“之字形”方法:
优势:
也许“之字形”方法的优点是渲染的地图可能看起来比“菱形”方法更垂直紧凑:
坏处:
从尝试实现之字形技术来看,缺点可能是编写渲染代码有点困难,因为它不能像for
在数组中的每个元素上嵌套循环一样简单:
tile_map[][] = [[...],...]
for (i = 0; i < tile_map.size; i++):
if i is odd:
offset_x = tile_width / 2
else:
offset_x = 0
for (j = 0; j < tile_map[i].size; j++):
draw(
tile_map[i][j],
x = (j * tile_width) + offset_x,
y = i * tile_height / 2
)
此外,由于渲染顺序的交错性质,尝试找出图块的坐标可能有点困难:
注意:此答案中包含的插图是使用所提供的瓷砖渲染代码的 Java 实现创建的,使用以下int
数组作为地图:
tileMap = new int[][] {
{0, 1, 2, 3},
{3, 2, 1, 0},
{0, 0, 1, 1},
{2, 2, 3, 3}
};
平铺图像是:
tileImage[0] ->
一个盒子,里面有一个盒子。tileImage[1] ->
一个黑匣子。tileImage[2] ->
一个白色的盒子。tileImage[3] ->
一个盒子,里面有一个高大的灰色物体。关于瓷砖宽度和高度的说明
上述代码示例中使用的变量tile_width
和tile_height
是指表示地砖的图像中地砖的宽度和高度:
只要图像尺寸和平铺尺寸匹配,使用图像的尺寸就可以了。否则,瓷砖地图可能会被渲染为瓷砖之间的间隙。
无论哪种方式都能完成工作。我假设之字形的意思是这样的:(数字是渲染的顺序)
.. .. 01 .. ..
.. 06 02 ..
.. 11 07 03 ..
16 12 08 04
21 17 13 09 05
22 18 14 10
.. 23 19 15 ..
.. 24 20 ..
.. .. 25 .. ..
你说的钻石是指:
.. .. .. .. ..
01 02 03 04
.. 05 06 07 ..
08 09 10 11
.. 12 13 14 ..
15 16 17 18
.. 19 20 21 ..
22 23 24 25
.. .. .. .. ..
第一种方法需要渲染更多图块以便绘制全屏,但您可以轻松地进行边界检查并跳过任何图块完全离开屏幕。这两种方法都需要进行一些数字运算才能找出 01 块的位置。最后,两种方法在一定水平的效率所需的数学方面大致相等。
如果您有一些超出钻石边界的图块,我建议您按深度顺序绘制:
...1...
..234..
.56789.
..abc..
...d...
Coobird 的答案是正确的、完整的。但是,我将他的提示与其他站点的提示结合起来,创建了适用于我的应用程序(iOS/Objective-C)的代码,我想与任何来这里寻找此类东西的人分享。请,如果您喜欢/投票赞成这个答案,请对原件做同样的事情;我所做的只是“站在巨人的肩膀上”。
至于排序顺序,我的技术是修改后的画家算法:每个对象都有(a)底部的高度(我称之为“水平”)和(b)“底部”或“脚”的 X/Y图像(例如:化身的底部在他的脚下;树的底部在它的根部;飞机的底部是中心图像等)然后我只是从最低到最高排序,然后从最低(屏幕上最高)到最高基- Y,然后是最低(最左边)到最高的 base-X。这以人们期望的方式呈现瓷砖。
将屏幕(点)转换为平铺(单元格)并返回的代码:
typedef struct ASIntCell { // like CGPoint, but with int-s vice float-s
int x;
int y;
} ASIntCell;
// Cell-math helper here:
// http://gamedevelopment.tutsplus.com/tutorials/creating-isometric-worlds-a-primer-for-game-developers--gamedev-6511
// Although we had to rotate the coordinates because...
// X increases NE (not SE)
// Y increases SE (not SW)
+ (ASIntCell) cellForPoint: (CGPoint) point
{
const float halfHeight = rfcRowHeight / 2.;
ASIntCell cell;
cell.x = ((point.x / rfcColWidth) - ((point.y - halfHeight) / rfcRowHeight));
cell.y = ((point.x / rfcColWidth) + ((point.y + halfHeight) / rfcRowHeight));
return cell;
}
// Cell-math helper here:
// http://stackoverflow.com/questions/892811/drawing-isometric-game-worlds/893063
// X increases NE,
// Y increases SE
+ (CGPoint) centerForCell: (ASIntCell) cell
{
CGPoint result;
result.x = (cell.x * rfcColWidth / 2) + (cell.y * rfcColWidth / 2);
result.y = (cell.y * rfcRowHeight / 2) - (cell.x * rfcRowHeight / 2);
return result;
}
您可以使用距观察者最高点和最近点的欧几里得距离,但这并不完全正确。它导致球形排序顺序。你可以通过从更远的地方看来解决这个问题。离得越远,曲率就会变平。因此,只需将 x、y 和 z 分量中的每一个添加 1000 即可得到 x'、y' 和 z'。x'*x'+y'*y'+z'*z' 上的排序。
真正的问题是当您需要绘制一些相交/跨越两个或多个其他图块的图块/精灵时。
经过 2 个月(困难)的个人问题分析后,我终于为我的新 cocos2d-js 游戏找到并实现了“正确的渲染绘图”。解决方案包括为每个图块(易受影响的)映射哪些精灵是“前、后、顶部和后面”。一旦这样做,您就可以按照“递归逻辑”绘制它们。