瓦片和瓦片地图概述
瓦片地图是 2D 游戏开发中一种非常流行的技术,它由称为瓦片的小型、规则形状的图像构建游戏世界或关卡地图。这带来了性能和内存使用方面的优势——不需要包含整个关卡地图的大图像文件,因为它们是通过多次使用小型图像或图像片段构建的。本系列文章将介绍使用 JavaScript 和 Canvas 创建瓦片地图的基础知识(尽管相同的高级技术也可以用于任何编程语言)。
除了性能优势之外,瓦片地图还可以映射到一个逻辑网格,该网格可以在游戏逻辑的其他方面使用(例如创建寻路图,或处理碰撞),或者用于创建关卡编辑器。
一些使用这种技术的流行游戏包括《超级马里奥兄弟》、《吃豆人》、《塞尔达传说:众神的三角力量》、《星际争霸》和《模拟城市 2000》。想想任何使用规则重复的背景方块的游戏,你很可能就会发现它使用了瓦片地图。
瓦片图集
存储瓦片图像的最有效方法是将其放在图集或精灵表中。这是所有必需的瓦片组合在一个图像文件中。在绘制瓦片时,只渲染这个大图像的一小部分到游戏画布上。下面的图片展示了一个 8x4 瓦片的图集。
使用图集还有一个优点,就是可以自然地为每个瓦片分配一个索引。这个索引非常适合用作创建瓦片地图对象时的瓦片标识符。
瓦片地图数据结构
通常会将处理瓦片地图所需的所有信息分组到相同的数据结构或对象中。这些数据对象(地图对象示例)应包含:
- 瓦片大小:每个瓦片在像素宽度 / 像素高度上的尺寸。
- 图像图集:将要使用的图像图集(一个或多个)。
- 地图尺寸:地图的尺寸,以瓦片宽度 / 瓦片高度,或像素宽度 / 像素高度表示。
- 视觉网格:包含索引,指示应将哪种类型的瓦片放置在网格的每个位置。
- 逻辑网格:这可以是一个碰撞网格、一个寻路网格等,具体取决于游戏的类型。
注意:对于视觉网格,需要一个特殊值(通常是负数、0
或 null
)来表示空瓦片。
方形瓦片
基于方形的瓦片地图是最简单的实现。更通用的情况是基于矩形的瓦片地图——而不是方形——但它们远不如方形常见。方形瓦片允许两种视角:
- 俯视(例如许多 RPG 或策略游戏,如《魔兽争霸 2》或《最终幻想》的世界视角)。
- 侧视(例如平台游戏,如《超级马里奥兄弟》)。
静态瓦片地图
瓦片地图可以适配可见屏幕区域,也可以比屏幕大。在第一种情况下,瓦片地图是静态的——无需滚动即可完全显示。这种情况在街机游戏中很常见,例如《吃豆人》、《打砖块》或《箱男》。
渲染静态瓦片地图很容易,可以通过嵌套循环遍历列和行来完成。一个高层算法可以是:
for (let column = 0; column < map.columns; column++) {
for (let row = 0; row < map.rows; row++) {
const tile = map.getTile(column, row);
const x = column * map.tileSize;
const y = row * map.tileSize;
drawTile(tile, x, y);
}
}
您可以阅读更多关于此内容,并查看实现示例,请参阅 方形瓦片地图实现:静态地图。
滚动瓦片地图
滚动瓦片地图一次只显示世界的一小部分。它们可以跟随角色——就像在平台游戏或 RPG 中一样——或者允许玩家控制摄像机——就像在策略或模拟游戏中一样。
定位和摄像机
在所有滚动游戏中,我们需要在世界坐标(精灵或其他元素在关卡或游戏世界中的位置)和屏幕坐标(这些元素在屏幕上的实际渲染位置)之间进行转换。世界坐标可以根据游戏的不同,以瓦片位置(地图的行和列)或地图的像素宽度来表示。为了能够将世界坐标转换为屏幕坐标,我们需要摄像机的坐标,因为它们决定了显示的是世界的哪个部分。
以下示例展示了如何从世界坐标转换为屏幕坐标以及反向转换:
// these functions assume that the camera points to the top left corner
function worldToScreen(x, y) {
return { x: x - camera.x, y: y - camera.y };
}
function screenToWorld(x, y) {
return { x: x + camera.x, y: y + camera.y };
}
渲染
一种简单的渲染方法只是遍历所有瓦片(就像在静态瓦片地图中一样),减去摄像机坐标(如上面 `worldToScreen()` 示例所示),并让落在视图窗口外的部分留在那里,隐藏起来。然而,绘制所有不可见的瓦片是浪费的,并且会影响性能。理想情况下,应该只渲染可见的瓦片——有关提高渲染性能的更多想法,请参阅 性能 部分。
您可以阅读更多关于实现滚动瓦片地图的内容,并查看一些实现示例,请参阅 方形瓦片地图实现:滚动地图。
图层
视觉网格通常由多个图层组成。这允许我们使用更少的瓦片创建更丰富的游戏世界,因为相同的图像可以与不同的背景一起使用。例如,一个可以出现在多种地形类型(如草地、沙地或砖块)上的岩石,可以包含在其自己的单独瓦片中,然后渲染在新图层上,而不是多个带有不同背景地形的岩石瓦片。
如果角色或其他游戏精灵绘制在图层堆栈的中间,这可以产生有趣的效果,例如让角色穿过树木或建筑物。
以下截图展示了这两个方面的示例:一个角色出现在瓦片后面(骑士出现在树顶后面),以及一个瓦片(灌木丛)被渲染在不同地形类型之上。
逻辑网格
由于瓦片地图是实际的视觉瓦片网格,因此通常在此视觉网格和逻辑网格之间创建映射。最常见的情况是使用此逻辑网格来处理碰撞,但也有其他用途:角色生成点、检测某些元素是否以正确的方式组合在一起以触发特定操作(如《俄罗斯方块》或《宝石迷阵》),寻路算法等。
注意:您可以查看我们的演示,其中展示了 如何使用逻辑网格来处理碰撞。
等距瓦片地图
等距瓦片地图创造了 3D 环境的错觉,在 2D 模拟、策略或 RPG 游戏中非常流行。其中一些游戏包括《模拟城市 2000》、《法老》或《最终幻想战略版》。下图展示了一个等距瓦片集图集的示例。
性能
绘制滚动瓦片地图可能会影响性能。通常,需要实现一些技术来使滚动顺畅。第一种方法,如上所述,是仅绘制可见的瓦片。但有时这还不够。
一种简单的技术是预先将地图渲染到一个单独的 Canvas 上(使用 Canvas API 时),或渲染到一个纹理上(使用 WebGL 时),这样就不需要在每一帧重新绘制瓦片,并且渲染可以一次完成blit操作。当然,如果地图很大,这并不能真正解决问题——而且有些系统对纹理大小的限制并不太宽松。
一种方法是 在 Canvas 外绘制可见的部分(而不是整个地图)。这意味着只要没有滚动,就不需要渲染地图。
这种方法的缺点是,当有滚动时,该技术效率不高。更好的方法是创建一个比可见区域大 2x2 瓦片的 Canvas,这样在边缘周围有一层“溢出”。这意味着地图只需要在滚动进展一整瓦片时才在 Canvas 上重新绘制——而不是在每一帧——在滚动期间。
对于快速游戏来说,这可能仍然不够。另一种方法是将瓦片地图分成大的部分(例如,将整个地图分成 10x10 的瓦片块),预先在 Canvas 外渲染每个部分,然后将每个渲染的部分视为一个“大瓦片”,并结合前面讨论的一种算法。
另见
-
MDN 上的相关文章
-
外部资源
- 演示和源代码
- 网格部件和关系,作者:Amit Patel(2021 年 5 月)
- 视频游戏中的等距图形(维基百科)