本文转自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坐标) | 内容 |
|---|---|---|---|
| 0 | 0 | [13,14] | 用户界面 |
| 1 | 1 | [4,6,8,10,12] | 顶部地图层 |
| 2 | 2 | [1,2,3,5,7,9,11] | 中部地图层 |
| 3 | 3 | [] | 底部地图层 |
如果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 < 15; i++)
{
metatile = MapGridGetMetatileIdAt(x + i, y + j);
if (BuyMenuCheckForOverlapWithMenuBg(i, j) == TRUE)
- metatileLayerType = MapGridGetMetatileLayerTypeAt(x + i, y + j);
+ metatileLayerType = METATILE_LAYER_TYPE_NORMAL;
else
metatileLayerType = METATILE_LAYER_TYPE_COVERED;
这将使元图块的大小正确,并且还会更新 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 < 4; ++i)
{
if ((src[i] & 0x3FF) != 0)
return FALSE;
}
return TRUE;
}
请注意,在使用友好商店时,当商店打开时,你必须确保商店用户界面元素周围没有三层图块。商店本身占用了一个BG层,这是我们在这里需要考虑的。
更新现有的图块集
如前所述,此方法要求每个元图块有4个额外的图块映射条目。普通的图块集数据不包含这些数据,在此阶段你的游戏看起来会是损坏的。幸运的是,我们可以运行一个简单的Python脚本来迁移旧的图块集。可以在这里找到:https://gist.github.com/SBird1337/ccfa47b5ef41c454b637735d4574592a
下载后,使用 python3 运行它。它期望你的 data/tilesets 目录路径作为 tsroot 参数。你可以像这样运行:
python3 triple_layer_converter.py --tsroot <path/to/pokeemerald/data/tilesets>
例如,如果我的 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 (< 5.2.0),你必须在你的
pokeemerald目录下的porymap.project.cfg文件中手动将enable_triple_layer_metatiles设置为1。
差不多就是这样了,你现在可以在 porymap 中使用三层支持。请注意,在图块集编辑器中会出现第三层,并且 Layer Type 属性消失了(不再需要它了)。
![[GBA教程]pokeemerald三层地图块详解-宝可梦营地](https://pokedream.cn/wp-content/uploads/2026/01/QQ20260115-154539.png)

![[GBA教程]pokeemerald新增NPC头像显示,并支持镜像翻转-宝可梦营地](https://pokedream.cn/wp-content/uploads/2026/01/QQ20260114-144714.png)
![[GBA]宝可梦 彼岸花绽放之夜 v1.0正式版-宝可梦营地](https://pokedream.cn/wp-content/uploads/2026/01/1.png)
![[GBA教程]pokeemerald新增自定义多项选择框-宝可梦营地](https://pokedream.cn/wp-content/uploads/2026/01/1F90AAF66BB1765251BED3AB10ACDD15.png)
![[GBA教程]pokeemerald动态地图块实操记录-宝可梦营地](https://pokedream.cn/wp-content/uploads/2026/01/QQ20260123-171516.png)

![[NDS中文]宝可梦白2加强v1.0.1测试版-宝可梦营地](https://pokedream.cn/wp-content/uploads/2025/12/pokew2.png)

![[NDS]心金魂银自用debug内置修改器-宝可梦营地](https://pokedream.cn/wp-content/uploads/2026/01/game_patched__13041.png)
![[GBA教程]pokeemerald实现动态地图色板功能-宝可梦营地](https://pokedream.cn/wp-content/uploads/2025/12/LittlerootTown.png)
![[记录]GBA反编译上踩过的一些坑-宝可梦营地](https://pokedream.cn/wp-content/uploads/2025/12/water.jpg)
暂无评论内容