Skip to content

Triple layer metatiles

HunarPG edited this page Sep 1, 2022 · 15 revisions

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.

Prerequisites

  • Python >= 3.6
  • Porymap >= 4.3.0

Vanilla Behavior

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.

Editing the Game Code

The changes we need to make to the game's code are fairly simple. We need to modify src/field_camera.c.

Basic functionality

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);
}

Handle the extra Data

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);

Fixing Doors

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.

Fixing the pokemart

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.

Fixing Secret Bases

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);

Updating existing tilesets

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.

Using Porymap

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)

Clone this wiki locally