[GBA教程]pokeemerald三层地图块详解

本文转自https://github.com/pret/pokeemerald/wiki/Triple-layer-metatiles

三层元图块

原版游戏使用了一种相当奇怪的方式来利用游戏的3个地图图层。为了完全控制这3个背景层,我们可以对游戏进行一些修改。

前提条件

  • Python >= 3.6

  • Porymap >= 4.3.0

原版行为

在原版游戏中,我们有以下几种在游戏主世界中使用背景层(BG)的模式:

  • 普通模式(使用 BG1 和 BG2)

  • 覆盖模式(使用 BG2 和 BG3)

  • 分割模式(使用 BG1 和 BG3)

当玩家位于主世界时,下表显示了每个图层的一些信息:

BG 图层BG 优先级对象事件高度(Z坐标)内容
00[13,14]用户界面
11[4,6,8,10,12]顶部地图层
22[1,2,3,5,7,9,11]中部地图层
33[]底部地图层

如果NPC精灵对应的“高度”(也称为Z坐标)大于特定图层的优先级,则该精灵将渲染在该图层之上。这可能听起来有点令人困惑,这里举个例子:

玩家的Z坐标起始值为3(默认值),这意味着它将被顶部地图层以及用户界面所遮挡。一旦玩家切换到高度为4,它将被渲染在所有地图图层之上,但仍在用户界面之下。当玩家切换到高度为13时,它甚至会被渲染在用户界面之上。

  • 注意:高度0和15是特殊的。如果玩家踩到高度为0或15的图块上,他们将保持在之前离开时的高度(或者游戏在传送时设置的高度,该情况下是高度3)。玩家可以从高度为0的图块走到另一个高度的图块,因此高度0被用来从一个高度过渡到另一个高度。玩家不能从高度为15的图块走到一个与他们离开时不同的高度。高度15最常用于桥梁。

当你能够实际使用所有3个图层时,这个表格可能会派上用场。

编辑游戏代码

我们需要对游戏代码做的更改相当简单。首先,我们更改 include/fieldmap.h 中的 NUM_TILES_PER_METATILE 值:

-#define NUM_TILES_PER_METATILE 8
+#define NUM_TILES_PER_METATILE 12

如果你的项目版本较旧,没有这个常量,你需要将你的项目与最新的代码库进行比较,找到使用这个常量的地方,然后手动将你项目中出现的所有8改为12

基础功能

我们修改的第一个函数是 src/field_camera.c 中的 DrawMetatile,它负责将元图块渲染到VRAM。我们用以下代码覆盖该函数:

static void DrawMetatile(s32 metatileLayerType, const u16 *tiles, u16 offset)

{

if (metatileLayerType == 0xFF)
{
    // 需要绘制一个门元图块,我们使用覆盖行为
    // 将元图块的底层绘制到底部背景层(BG3)。
    gOverworldTilemapBuffer_Bg3[offset] = tiles[0];
    gOverworldTilemapBuffer_Bg3[offset + 1] = tiles[1];
    gOverworldTilemapBuffer_Bg3[offset + 0x20] = tiles[2];
    gOverworldTilemapBuffer_Bg3[offset + 0x21] = tiles[3];

    // 将透明图块绘制到顶部背景层(BG2)。
    gOverworldTilemapBuffer_Bg2[offset] = 0;
    gOverworldTilemapBuffer_Bg2[offset + 1] = 0;
    gOverworldTilemapBuffer_Bg2[offset + 0x20] = 0;
    gOverworldTilemapBuffer_Bg2[offset + 0x21] = 0;

    // 将元图块的顶层绘制到中部背景层(BG1)。
    gOverworldTilemapBuffer_Bg1[offset] = tiles[4];
    gOverworldTilemapBuffer_Bg1[offset + 1] = tiles[5];
    gOverworldTilemapBuffer_Bg1[offset + 0x20] = tiles[6];
    gOverworldTilemapBuffer_Bg1[offset + 0x21] = tiles[7];

}
else
{
    // 将元图块的底层绘制到底部背景层(BG3)。
    gOverworldTilemapBuffer_Bg3[offset] = tiles[0];
    gOverworldTilemapBuffer_Bg3[offset + 1] = tiles[1];
    gOverworldTilemapBuffer_Bg3[offset + 0x20] = tiles[2];
    gOverworldTilemapBuffer_Bg3[offset + 0x21] = tiles[3];

    // 将元图块的中层绘制到中部背景层(BG2)。
    gOverworldTilemapBuffer_Bg2[offset] = tiles[4];
    gOverworldTilemapBuffer_Bg2[offset + 1] = tiles[5];
    gOverworldTilemapBuffer_Bg2[offset + 0x20] = tiles[6];
    gOverworldTilemapBuffer_Bg2[offset + 0x21] = tiles[7];

    // 将元图块的顶层绘制到顶部背景层(BG1),该层会覆盖对象事件精灵。
    gOverworldTilemapBuffer_Bg1[offset] = tiles[8];
    gOverworldTilemapBuffer_Bg1[offset + 1] = tiles[9];
    gOverworldTilemapBuffer_Bg1[offset + 0x20] = tiles[10];
    gOverworldTilemapBuffer_Bg1[offset + 0x21] = tiles[11];

}

ScheduleBgCopyTilemapToVram(1);
ScheduleBgCopyTilemapToVram(2);
ScheduleBgCopyTilemapToVram(3);

}

修复门的问题

以目前的状态,门会出问题。绘制门也会调用 DrawMetatile,但提供的包含门动画图块的数组对于我们的新三层系统来说太小了。为了解决这个问题,我们已经在 DrawMetatile 中做了一个例外处理(见上文),并且需要相应地更改 DrawDoorMetatileAt

- DrawMetatile(METATILE_LAYER_TYPE_COVERED, tiles, offset);

  • DrawMetatile(0xFF, tiles, offset);

这使得游戏在处理门动画时使用正常的渲染行为。

修复友好商店

商店在原版中很奇怪。它们试图将图块从BG1移动到其他2个BG,以便为“pokemart”用户界面腾出空间。它们还会重新绘制大部分地图,这需要更新。所有这些更改都在 src/shop.c 文件中。

在 BuyMenuDrawMapBg 函数中:

for (i = 0; i 

这将使元图块的大小正确,并且还会更新 metatileLayerType,我们稍后将用它来进行一些图块重新排序。接下来,看一下 BuyMenuDrawMapMetatile 函数:

static void BuyMenuDrawMapMetatile(s16 x, s16 y, const u16 *src, u8 metatileLayerType)
{
u16 offset1 = x * 2;
u16 offset2 = y * 64;
-
  • switch (metatileLayerType)
  • if (metatileLayerType == METATILE_LAYER_TYPE_NORMAL)
    {
  • case METATILE_LAYER_TYPE_NORMAL:
  • BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[3], offset1, offset2, src);
  • BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[1], offset1, offset2, src + 4);
  • break;
  • case METATILE_LAYER_TYPE_COVERED:
  • BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[2], offset1, offset2, src);
  • BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[2], offset1, offset2, src + 0);

       BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[3], offset1, offset2, src + 4);
  • break;
  • case METATILE_LAYER_TYPE_SPLIT:
  • BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[2], offset1, offset2, src);
  • BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[1], offset1, offset2, src + 4);
  • break;
  • BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[1], offset1, offset2, src + 8);
  • }
  • else
  • {
  • if (IsMetatileLayerEmpty(src))
  • {
  • BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[2], offset1, offset2, src + 4);
  • BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[3], offset1, offset2, src + 8);
  • }
  • else if (IsMetatileLayerEmpty(src + 4))
  • {
  • BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[2], offset1, offset2, src);
  • BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[3], offset1, offset2, src + 8);
  • }
  • else if (IsMetatileLayerEmpty(src + 8))
  • {
  • BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[2], offset1, offset2, src);
  • BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[3], offset1, offset2, src + 4);
  • }
    }
    }

  • 或者,直接复制并粘贴这个函数:

    static void BuyMenuDrawMapMetatile(s16 x, s16 y, const u16 *src, u8 metatileLayerType)
    {
    u16 offset1 = x * 2;
    u16 offset2 = y * 64;
    if (metatileLayerType == METATILE_LAYER_TYPE_NORMAL)
    {

      BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[2], offset1, offset2, src + 0);
       BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[3], offset1, offset2, src + 4);
      BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[1], offset1, offset2, src + 8);

    }
    else
    {

      if (IsMetatileLayerEmpty(src))
      {
          BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[2], offset1, offset2, src + 4);
          BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[3], offset1, offset2, src + 8);
      }
      else if (IsMetatileLayerEmpty(src + 4))
      {
          BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[2], offset1, offset2, src);
          BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[3], offset1, offset2, src + 8);
      }
      else if (IsMetatileLayerEmpty(src + 8))
      {
          BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[2], offset1, offset2, src);
          BuyMenuDrawMapMetatileLayer(sShopData->tilemapBuffers[3], offset1, offset2, src + 4);
      }

    }
    }

    这将处理三层图块的绘制,除非地图网格上的元素会与用户界面元素重叠。在这种情况下,它会尝试找到一个空层并相应地移动其他图块。

    你还需要在 BuyMenuDrawMapMetatile 函数上方某处添加这个函数:

    static bool8 IsMetatileLayerEmpty(const u16 *src)
    {
    u32 i = 0;
    for (i = 0; i

    请注意,在使用友好商店时,当商店打开时,你必须确保商店用户界面元素周围没有三层图块。商店本身占用了一个BG层,这是我们在这里需要考虑的。

    更新现有的图块集

    如前所述,此方法要求每个元图块有4个额外的图块映射条目。普通的图块集数据不包含这些数据,在此阶段你的游戏看起来会是损坏的。幸运的是,我们可以运行一个简单的Python脚本来迁移旧的图块集。可以在这里找到:https://gist.github.com/SBird1337/ccfa47b5ef41c454b637735d4574592a

    下载后,使用 python3 运行它。它期望你的 data/tilesets 目录路径作为 tsroot 参数。你可以像这样运行:

    python3 triple_layer_converter.py --tsroot 

    例如,如果我的 pokeemerald 实例在 /home/hacker/pokeemerald 目录下,我会运行:

    python3 triple_layer_converter.py --tsroot /home/hacker/pokeemerald/data/tilesets

    脚本将为每个成功转换的图块集输出 [OK]

    使用 Porymap

    幸运的是,porymap 在视觉和功能上都支持这个新系统。

    • 如果你使用的 porymap 版本 >= 6.0.0,无需任何额外操作!Porymap 会自动为你启用必要的设置。如果 porymap 已经打开,请确保重新加载你的项目。

    • 如果你使用的 porymap 版本 >= 5.2.0,请转到 Options -> Project Settings...,然后在 Tilesets 标签下勾选 Enable Triple Layer Metatiles 选项。然后选择 OK 并重新加载你的项目。

    • 如果你使用的是旧版本的 porymap (,你必须在你的 pokeemerald 目录下的 porymap.project.cfg 文件中手动将 enable_triple_layer_metatiles 设置为 1

    差不多就是这样了,你现在可以在 porymap 中使用三层支持。请注意,在图块集编辑器中会出现第三层,并且 Layer Type 属性消失了(不再需要它了)。

     

    支持这篇文章点赞、收藏或分享给朋友
    DISCUSSION

    发表评论

    留下你的想法

    输入 @用户名 可以提及站内用户0 / 5000