-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Triple layer metatiles
The vanilla game uses a rather weird way to make use of the 3 map layers of the game. In order to have full control over the 3 BG layers we can modify the game a bit.
- Python >= 3.6
- Porymap >= 4.3.0
In the vanilla game we have the following possibilities for using the BG layers on the overworld:
- Normal Mode (BGs 1 and 2)
- Covered Mode (BGs 2 and 3)
- Split Mode (BGs 1 and 3)
The following table shows some information about each layer when the player is on the overworld:
BG Layer | BG Priority | Object Event Elevation | Content |
---|---|---|---|
0 | 0 | [13,14] | User Interface |
1 | 1 | [4,6,8,10,12] | Top Map Layer |
2 | 2 | [1,2,3,5,7,9,11] | Middle Map Layer |
3 | 3 | [] | Bottom Map Layer |
An NPC sprite will be rendered on top of a layer if its corresponding elevation (Also called Z Coordinate) is greater than the priority of the respective layer. This may sound confusing, so here's an example:
The player starts with a Z Coordinate of 3 (the default), meaning it will be covered by the Top Map Layer as well as the User Interface. Once the player transitions to an elevation of 4 it will be rendered above all the map layers but still below the User interface. Once the player transitions to an elevation of 13 it will be rendered even above the User interface.
- Note: Elevations 0 and 15 are special. If the player steps onto a tile that's elevation 0 or 15, they will stay at the previous elevation that they left from (or were set at by the game on a warp, which is elevation 3 in that case). The player can step from an elevation 0 tile to another elevation tile, so elevation 0 is used to transition from one elevation to another. The player cannot step from an elevation 15 tile to a different elevation than they left from. Elevation 15 is used most often for bridges.
This table may come in handy once you can actually work with all 3 layers.
The changes we need to make to the game's code are fairly simple. First, we will change the value of NUM_TILES_PER_METATILE
in include/fieldmap.h
:
-#define NUM_TILES_PER_METATILE 8
+#define NUM_TILES_PER_METATILE 12
If your project is old enough that it doesn't have this constant you will need to compare your project to an up-to-date repo, see where the constant is used, and manually change those instances of 8 in your own project to 12.
The first function we tackle is DrawMetatile
in src/field_camera.c
, which is responsible for rendering the metatiles to VRAM. We overwrite the function with the following:
static void DrawMetatile(s32 metatileLayerType, const u16 *tiles, u16 offset)
{
if (metatileLayerType == 0xFF)
{
// A door metatile shall be drawn, we use covered behavior
// Draw metatile's bottom layer to the bottom background layer.
gOverworldTilemapBuffer_Bg3[offset] = tiles[0];
gOverworldTilemapBuffer_Bg3[offset + 1] = tiles[1];
gOverworldTilemapBuffer_Bg3[offset + 0x20] = tiles[2];
gOverworldTilemapBuffer_Bg3[offset + 0x21] = tiles[3];
// Draw transparent tiles to the top background layer.
gOverworldTilemapBuffer_Bg2[offset] = 0;
gOverworldTilemapBuffer_Bg2[offset + 1] = 0;
gOverworldTilemapBuffer_Bg2[offset + 0x20] = 0;
gOverworldTilemapBuffer_Bg2[offset + 0x21] = 0;
// Draw metatile's top layer to the middle background layer.
gOverworldTilemapBuffer_Bg1[offset] = tiles[4];
gOverworldTilemapBuffer_Bg1[offset + 1] = tiles[5];
gOverworldTilemapBuffer_Bg1[offset + 0x20] = tiles[6];
gOverworldTilemapBuffer_Bg1[offset + 0x21] = tiles[7];
}
else
{
// Draw metatile's bottom layer to the bottom background layer.
gOverworldTilemapBuffer_Bg3[offset] = tiles[0];
gOverworldTilemapBuffer_Bg3[offset + 1] = tiles[1];
gOverworldTilemapBuffer_Bg3[offset + 0x20] = tiles[2];
gOverworldTilemapBuffer_Bg3[offset + 0x21] = tiles[3];
// Draw metatile's middle layer to the middle background layer.
gOverworldTilemapBuffer_Bg2[offset] = tiles[4];
gOverworldTilemapBuffer_Bg2[offset + 1] = tiles[5];
gOverworldTilemapBuffer_Bg2[offset + 0x20] = tiles[6];
gOverworldTilemapBuffer_Bg2[offset + 0x21] = tiles[7];
// Draw metatile's top layer to the top background layer, which covers object event sprites.
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);
}
With the state as is doors will break. Drawing doors also causes a call to DrawMetatile
but the supplied array that contains the door animation tiles is too small for our new triple layer system. To mitigate this we already made an exception in DrawMetatile
(See above) and need to change DrawDoorMetatileAt
accordingly:
- DrawMetatile(METATILE_LAYER_TYPE_COVERED, tiles, offset);
+ DrawMetatile(0xFF, tiles, offset);
This causes the game to use the normal rendering behavior when using handling door animations.
Marts are weird in vanilla. They try to move tiles from BG1 to the other 2 BGs in order to make some space for the pokemart
UI. They also redraw a big portion of the map which needs to be updated. All those changes go to src/shop.c
In 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;
This will get the size of the metatiles correct and will also update the metatileLayerType
which we will use to do some tile reordering later. Next have a look at 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);
+ }
}
}
This will handle drawing triple layers, except when the element on the mapgrid would overlap with an UI element. It will then try to find and empty layer and move the other tiles accordingly. You also have to add this function somewhere above 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;
}
Note that when using the pokemart
you have to absolutely make sure that no triple layer tiles are around the UI elements when the mart is open. The mart uses one BG layer for itself, which we need to take into account here.
As mentioned previously this method requires us 4 additional tilemap entries for each metatile. The normal tileset data does not contain that data and at this stage your game will just look corrupted. Luckily we can just run a simple python script to migrate old tilesets. It can be found here: https://gist.github.com/SBird1337/ccfa47b5ef41c454b637735d4574592a
Once downloaded you run it using python3
. It expects the path to your data/tilesets
directory as tsroot
. You can run it like this:
python3 triple_layer_converter.py --tsroot <path/to/pokeemerald/data/tilesets>
So for example if my instance of pokeemerald
is in /home/hacker/pokeemerald
I would run
python3 triple_layer_converter.py --tsroot /home/hacker/pokeemerald/data/tilesets
The script will yield [OK]
for each successfully converted tileset.
Luckily porymap
supports this new system both visually and functionally. Under the Tilesets
tab in Options -> Project Settings...
check the Enable Triple Layer Metatiles
option. Then select OK
and reload your project.
If you are using an older version of porymap (<= 5.1.1) you must instead manually set enable_triple_layer_metatiles
to 1
in the porymap.project.cfg
file located in your pokeemerald
directory.
That's about it, you can now use porymap with Triple Layer support. Note that in the tileset editor a third layer appears and the Layer Type property disappears (It is not needed anymore)