-
Notifications
You must be signed in to change notification settings - Fork 3
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 | [0,1,2,3,5,7,9,11,15] | 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 0, 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.
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. We need to modify src/field_camera.c
.
The first function we tackle is DrawMetatile
which is responsible for rendering the metatiles to VRAM. We overwrite the function with the following:
static void DrawMetatile(s32 metatileLayerType, 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 3 layers per metatile our data also grow, we need the game to handle 12 tile instances per metatile instead of 8 in the vanilla game. To do so we make the following change in DrawMetatileAt
:
- DrawMetatile(MapGridGetMetatileLayerTypeAt(x, y), metatiles + metatileId * 8, offset);
+ DrawMetatile(MapGridGetMetatileLayerTypeAt(x, y), metatiles + metatileId * 12, offset);
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 = 0;
else
metatileLayerType = 1;
if (metatile < NUM_METATILES_IN_PRIMARY)
{
- BuyMenuDrawMapMetatile(i, j, (u16*)mapLayout->primaryTileset->metatiles + metatile * 8, metatileLayerType);
+ BuyMenuDrawMapMetatile(i, j, (u16*)mapLayout->primaryTileset->metatiles + metatile * 12, metatileLayerType);
}
else
{
- BuyMenuDrawMapMetatile(i, j, (u16*)mapLayout->secondaryTileset->metatiles + ((metatile - NUM_METATILES_IN_PRIMARY) * 8), metatileLayerType);
+ BuyMenuDrawMapMetatile(i, j, (u16*)mapLayout->secondaryTileset->metatiles + ((metatile - NUM_METATILES_IN_PRIMARY) * 12), metatileLayerType);
}
}
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 == 0)
{
- 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.
This one is a bit easier. All those changes go to src/decoration.c
In PlaceDecorationGraphicsDataBuffer
:
- data->tiles[sDecorTilemaps[shape].tiles[i]] = GetMetatile(data->decoration->tiles[sDecorTilemaps[shape].y[i]] * 8 + sDecorTilemaps[shape].x[i]);
+ data->tiles[sDecorTilemaps[shape].tiles[i]] = GetMetatile(data->decoration->tiles[sDecorTilemaps[shape].y[i]] * 12 + sDecorTilemaps[shape].x[i]);
In PlaceDecorationGraphics
:
- CopyPalette(data->palette, ((u16 *)gTilesetPointer_SecretBaseRedCave->metatiles)[(data->decoration->tiles[0] * 8) + 7] >> 12);
+ CopyPalette(data->palette, ((u16 *)gTilesetPointer_SecretBaseRedCave->metatiles)[(data->decoration->tiles[0] * 12) + 7] >> 12);
In AddDecorationIconObjectFromObjectEvent
:
- CopyPalette(sPlaceDecorationGraphicsDataBuffer.palette, ((u16 *)gTilesetPointer_SecretBaseRedCave->metatiles)[(sPlaceDecorationGraphicsDataBuffer.decoration->tiles[0] * 8) + 7] >> 12);
+ CopyPalette(sPlaceDecorationGraphicsDataBuffer.palette, ((u16 *)gTilesetPointer_SecretBaseRedCave->metatiles)[(sPlaceDecorationGraphicsDataBuffer.decoration->tiles[0] * 12) + 7] >> 12);
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. If you are using porymap you have to a flag in the porymap.project.cfg
file which is located in your pokeemerald
directory if you have used porymap before. Set the following flag:
enable_triple_layer_metatiles=1
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)