20 Triple layer metatiles
GriffinR edited this page 2024-02-07 16:14:33 -05:00

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 [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.

Editing the Game Code

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.

Basic functionality

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

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 = 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.

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