瓦片和瓦片地图概述

瓦片地图是二维游戏开发中非常流行的技术,它通过构建由称为**瓦片**的小型、规则形状的图像组成的游戏世界或关卡地图。这样可以提高性能并节省内存使用量——不再需要包含整个关卡地图的大型图像文件,因为它们是由多次重复的小型图像或图像片段构建的。本系列文章介绍了使用JavaScriptCanvas创建瓦片地图的基础知识(虽然相同的技术也能应用于任何编程语言)。

除了性能提升,瓦片地图还可以映射到逻辑网格,这可以在游戏逻辑中以其他方式使用(例如创建寻路图或处理碰撞),或者用来创建关卡编辑器。

一些使用这种技术的游戏包括*超级马里奥兄弟*、*吃豆人*、*塞尔达传说:梦见岛*、*星际争霸*和*模拟城市2000*。只要想想任何使用规律重复的背景方块的游戏,你可能就会发现它使用了瓦片地图。

瓦片图集

存储瓦片图像最有效的方式是使用图集或精灵图。这将所有必需的瓦片组合在一个图像文件中。当需要绘制瓦片时,只渲染此大型图像的一小部分到游戏画布上。下面的图像显示了 8 x 4 个瓦片的瓦片图集

Tile atlas image

使用图集还有一个好处,那就是自然地为每个瓦片分配一个**索引**。此索引非常适合在创建瓦片地图对象时用作瓦片标识符。

瓦片地图数据结构

通常将处理瓦片地图所需的所有信息都组合到相同的数据结构或对象中。这些数据对象(地图对象示例)应包含以下内容:

  • 瓦片大小:每个瓦片的尺寸,以像素横向 / 像素纵向表示。
  • 图像图集:将使用的图像图集(一个或多个)。
  • 地图尺寸:地图的尺寸,可以是瓦片横向 / 瓦片纵向,也可以是像素横向 / 像素纵向。
  • 视觉网格:包含索引,显示网格中每个位置应放置哪种瓦片。
  • 逻辑网格:这可以是碰撞网格、寻路网格等,具体取决于游戏类型。

注意:对于视觉网格,需要一个特殊的值(通常为负数、0null)来表示空瓦片。

方形瓦片

基于正方形的瓦片地图是最简单的实现。更通用的情况是基于矩形的瓦片地图——而不是正方形——但它们并不常见。正方形瓦片允许两种**视角**

  • 俯视(类似于许多 RPG 或策略游戏,例如*魔兽争霸 2* 或*最终幻想*的世界视图)。
  • 侧面(类似于平台游戏,例如*超级马里奥兄弟*)。

静态瓦片地图

瓦片地图可以适合可视屏幕区域或更大。在第一种情况下,瓦片地图是**静态**的——它不需要滚动就能完全显示出来。这种情况下在街机游戏(例如*吃豆人*、*打砖块*或*推箱子*)中很常见。

渲染静态瓦片地图很容易,可以使用嵌套循环遍历列和行。一个高级算法可能是

js
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——或者让玩家控制摄像机——例如策略游戏或模拟游戏。

定位和摄像机

在所有滚动游戏中,我们需要在**世界坐标**(精灵或其他元素在关卡或游戏世界中的位置)和**屏幕坐标**(这些元素在屏幕上渲染的实际位置)之间进行转换。世界坐标可以用瓦片位置(地图的行和列)或地图上的像素表示,具体取决于游戏。为了能够将世界坐标转换为屏幕坐标,我们需要摄像机的坐标,因为它们决定了要显示世界中的哪一部分。

以下示例展示了如何将世界坐标转换为屏幕坐标,反之亦然

js
// 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()示例中),并让落在视窗外的部分保持隐藏。但是,绘制所有不可见的瓦片是一种浪费,会影响性能。理想情况下,**只应该渲染可见的瓦片**——有关改进渲染性能的更多想法,请参阅性能部分。

你可以在方形瓦片地图实现:滚动地图中详细了解滚动瓦片地图的实现,并查看一些示例实现。

图层

视觉网格通常由多个图层组成。这样一来,我们就可以使用更少的瓦片创建更丰富多彩的游戏世界,因为相同的图像可以与不同的背景一起使用。例如,一块石头可以在几种地形类型(如草地、沙地或砖块)上出现,可以将其放在单独的瓦片上,然后渲染到新的图层上,而不是创建多个石头瓦片,每个瓦片都有不同的背景地形。

如果角色或其他游戏精灵绘制在图层堆栈的中间,就可以实现一些有趣的视觉效果,例如让角色在树木或建筑物后面行走。

以下屏幕截图展示了这两个要点示例:角色出现在瓦片后面(骑士出现在树顶后面)以及瓦片(灌木)渲染在不同地形类型之上。

A grid of layered background terrains. A bush tile is rendered at the top, over a large grass terrain, and again over a layered rectangular terrain with brown sand at the bottom. A tree tile is rendered over the grass terrain at the bottom left and again at the bottom right. A knight tile appears behind the tree tile that is rendered at the bottom left.

逻辑网格

由于瓦片地图实际上是由视觉瓦片组成的网格,因此通常会在视觉网格和逻辑网格之间创建映射。最常见的情况是使用此逻辑网格来处理碰撞,但也可以用于其他目的:角色生成点、检测某些元素是否以正确的方式放置在一起以触发某个操作(例如*俄罗斯方块*或*宝石迷阵*)、寻路算法等。

注意:你可以查看我们的演示,它展示了如何使用逻辑网格处理碰撞

等距瓦片地图

等距瓦片地图创建了三维环境的错觉,在二维模拟、策略或 RPG 游戏中非常流行。其中一些游戏包括*模拟城市 2000*、*法老王*或*最终幻想战略版*。下面的图像展示了等距瓦片集的图集示例。

A 3x4 map of variously colored tiles in isometric projection

性能

绘制滚动瓦片地图可能会影响性能。通常,需要实现一些技术才能使滚动流畅。如上所述,第一种方法是**只绘制可见的瓦片**。但有时这还不够。

一种简单的技术是在单独的画布上(使用 Canvas API 时)或纹理上(使用 WebGL 时)预渲染地图,这样就不需要在每一帧都重新绘制瓦片,并且渲染可以在一个 blitting 操作中完成。当然,如果地图很大,这并不能真正解决问题——而且有些系统对纹理的大小限制非常严格。

一种方法是在画布之外绘制将要显示的部分(而不是整个地图)。这意味着只要没有滚动,地图就不需要渲染。

这种方法的一个缺点是,当存在滚动时,该技术效率不高。更好的方法是创建一个比可见区域大 2x2 个瓦片的画布,这样在边缘周围就会有一个瓦片的“溢出”。这意味着地图只需要在滚动前进一个完整瓦片时重新绘制到画布上——而不是每帧都重新绘制——在滚动过程中。

在快速游戏中,这可能还不够。另一种方法是将瓦片地图分成大块(比如将完整的图分成 10 x 10 块瓦片),在画布外预渲染每一块,然后将每个渲染的块作为“大瓦片”与上面讨论的算法之一结合使用。

另请参阅