Skip to content

Adding Animations

grunt-lucas edited this page Jul 12, 2024 · 47 revisions

Note: this tutorial assumes that the porytiles executable exists on your system path. It also assumes your pokeemerald project is stored at the $HOME location. If your executable or project lives elsewhere, you'll need to change those paths in the commands below.

Table Of Contents

Getting Started

This tutorial continues the series we started in Compiling A Primary Tileset and Compiling A Secondary Tileset.

A Note On How Animations Work

One thing we should clarify at the outset: in the vanilla Gen III games, animations are handled on a per-tile basis, not a per-metatile basis. That is, there is no concept of an animated metatile. Rather, the animation system periodically copies a new sequence of tiles directly into a hardcoded, contiguous region of the tile section of VRAM. Those tiles will show up on-screen only insofar as they are referenced from a metatile entry, and they will display using whichever palette that metatile entry specifies. This is a key concept. Once you truly understand this, you'll understand the animation system. The rest is just an implementation detail.

Later in this tutorial, we will walk through the C code modifications you'll need to make this all work. If you find any of the above confusing, that's OK. Please be patient and read on.

Formatting The Assets

OK! With that out of the way, let's go ahead and add some animated tiles to our primary tileset. Specifically, we'll set up an animation for the middle layer of metatile 4. You can see it below, highlighted in cyan:

The flower metatile

The first thing we'll need to do is create some animation frames for the flower. By frame, we just mean a snapshot of the animation at a particular state. You'll need to create a separate PNG image for each frame, and the PNGs must be named numerically, in increasing order. So if you have three frames, you should have three PNG files called 00.png, 01.png, and 02.png. See below for the three frames I came up with:

If you want to use these examples to follow along, you can find the assets here.

Recall earlier, I said that "animations are handled on a per-tile basis". The game engine copies your animation frame into VRAM tilewise left-to-right, top-to-bottom. This means that you could have formatted your flower frames like I did above. Alternatively, you could also have formatted them in one of the following two ways:

It doesn't matter. As long as reading them tilewise left-to-right, top-to-bottom yields the same tile order, the engine's VRAM copy will behave identically.

One other thing you may be wondering: where are the palettes for the animated tiles stored? Recall what I said earlier in the note on how animations work. That in fact, the palette for an animated tile is just the palette indicated by the metatile entry that references the animated tile. This means that a given animated subtile across all your animation frames will have to use the same palette. Keep that in mind when creating animated tiles.

The Key Frame

Ok, now that we have some animation frames, we need to talk about a special frame called the key frame. This is a bit tricky to explain, so if you don't understand on the first read-through, don't despair. Just keep following the tutorial and re-read it later.

As its name suggests, the key frame is a special frame that Porytiles uses as a key to identify your animation on the three input layer PNGs. Without the key frame, Porytiles has no way to know which tile art on the layer PNGs is supposed to be animated. Like the key in a map data structure, each tile in the key frame must be unique across all animation key frame tiles for a given tileset, including the paired primary if you are compiling a secondary tileset. For the key frame, usually just duplicating 00.png is sufficient. That's what we'll do for this tutorial. So with that said, let's create a copy of 00.png and call it key.png. Your key frame must be called key.png. See below:

Using Your Key Frame

Now that you have your animation frames and your key frame configured, you can go ahead and use your animation on your layer PNGs. It's simple. For any subtile (i.e. 8x8 tile) within a metatile that you want to animate, in the corresponding spot on your layer PNG, use the corresponding keyframe tile. For our case, this is easy. We'll just copy-paste the flower key frame directly onto the middle layer PNG. Your middle.png should look just like the example shown earlier. Notice how the flowers on the middle PNG match our key frame?

If you are trying to animate something more complex, like ocean water or beach tiles, you may not be able to just copy your key frame over. Rather, you'll need to copy just the relevant subtiles from the key frame into the relevant metatiles on your layer sheet. If you find this confusing, don't fret. I'll cover how this works in an appendix at the bottom of this tutorial.

Compiling The Animated Tileset

Ok. We now have the animation assets in place. Assuming you have been following the previous two tutorials on primary and secondary compilation, go ahead and recompile the primary tileset like so. Please note that this time, we are compiling porytiles-anim-tutorial and not porytiles-primary-tutorial. If you are following along with the res/examples assets, make sure you compile the correct assets.

$ porytiles compile-primary -dual-layer -Wall -o $HOME/pokeemerald/data/tilesets/primary/porytiles_primary_tutorial $HOME/porytiles-anim-tutorial $HOME/pokeemerald/include/constants/metatile_behaviors.h

# Note that if your project is using triple layer tiles, simply omit the `-dual-layer` flag.

Adding The Driving C Code

Setup Code

Next, we will need to make some modifications to the game's C files in order to get the animation to display in-game.

First thing: open include/tileset_anims.h, and add a declaration for InitTilesetAnim_PorytilesPrimaryTutorial at the bottom:

// ### include/tileset_anims.h ###
void InitTilesetAnim_EliteFour(void);
void InitTilesetAnim_BattleDome(void);
void InitTilesetAnim_BattlePyramid(void);

// Our custom tileset animations:

void InitTilesetAnim_PorytilesPrimaryTutorial(void);

#endif // GUARD_TILESET_ANIMS_H

Then, open up src/data/tilesets/headers.h and scroll down to the bottom. You should see two entries for the Porytiles tutorial tilesets we created in the previous tutorials. Here, let's set the .callback for gTileset_PorytilesPrimaryTutorial to InitTilesetAnim_PorytilesPrimaryTutorial, the function we just declared. The .callback for the secondary tileset can be left NULL, since we won't have any animations for our it in this tutorial.

// ### src/data/tilesets/headers.h ###
const struct Tileset gTileset_PorytilesPrimaryTutorial =
{
    .isCompressed = TRUE,
    .isSecondary = FALSE,
    .tiles = gTilesetTiles_PorytilesPrimaryTutorial,
    .palettes = gTilesetPalettes_PorytilesPrimaryTutorial,
    .metatiles = gMetatiles_PorytilesPrimaryTutorial,
    .metatileAttributes = gMetatileAttributes_PorytilesPrimaryTutorial,
    .callback = InitTilesetAnim_PorytilesPrimaryTutorial,
};

const struct Tileset gTileset_PorytilesSecondaryTutorial =
{
    .isCompressed = TRUE,
    .isSecondary = TRUE,
    .tiles = gTilesetTiles_PorytilesSecondaryTutorial,
    .palettes = gTilesetPalettes_PorytilesSecondaryTutorial,
    .metatiles = gMetatiles_PorytilesSecondaryTutorial,
    .metatileAttributes = gMetatileAttributes_PorytilesSecondaryTutorial,
    .callback = NULL,
};

Driver Code

For our next step, open up src/tileset_anims.c. Now, we will create a definition for InitTilesetAnim_PorytilesPrimaryTutorial, along with some other helper functions and variables we'll need to drive the animation. Scroll all the way down to the bottom of the file. To keep things organized, we'll be putting all our custom animation code down here, below BlendAnimPalette_BattleDome_FloorLightsNoBlend. Otherwise, it'll quickly become a mess if we mix things together with the vanilla animation code. If you are reading this in the far future, BlendAnimPalette_BattleDome_FloorLightsNoBlend may no longer be the last function in src/tileset_anims.c. In that case, place your code below whichever is the last function.

If you just want to get the animation working, here is all the code you will need. Copy and paste this to the bottom of src/tileset_anims.c:

// ### src/tileset_anims.c ###
// Our custom animation code:

const u16 gTilesetAnims_PorytilesPrimaryTutorial_Flower_Frame0[] = INCBIN_U16("data/tilesets/primary/porytiles_primary_tutorial/anim/flower_red/00.4bpp");
const u16 gTilesetAnims_PorytilesPrimaryTutorial_Flower_Frame1[] = INCBIN_U16("data/tilesets/primary/porytiles_primary_tutorial/anim/flower_red/01.4bpp");
const u16 gTilesetAnims_PorytilesPrimaryTutorial_Flower_Frame2[] = INCBIN_U16("data/tilesets/primary/porytiles_primary_tutorial/anim/flower_red/02.4bpp");

const u16 *const gTilesetAnims_PorytilesPrimaryTutorial_Flower[] = {
    gTilesetAnims_PorytilesPrimaryTutorial_Flower_Frame0,
    gTilesetAnims_PorytilesPrimaryTutorial_Flower_Frame1,
    gTilesetAnims_PorytilesPrimaryTutorial_Flower_Frame0,
    gTilesetAnims_PorytilesPrimaryTutorial_Flower_Frame2
};

static void QueueAnimTiles_PorytilesPrimaryTutorial_Flower(u16 timer)
{
    u16 i = timer % ARRAY_COUNT(gTilesetAnims_PorytilesPrimaryTutorial_Flower);
    AppendTilesetAnimToBuffer(gTilesetAnims_PorytilesPrimaryTutorial_Flower[i], (u16 *)(BG_VRAM + TILE_OFFSET_4BPP(1)), 4 * TILE_SIZE_4BPP);
}

static void TilesetAnim_PorytilesPrimaryTutorial(u16 timer)
{
    if (timer % 16 == 0) {
        QueueAnimTiles_PorytilesPrimaryTutorial_Flower(timer / 16);
    }
}

void InitTilesetAnim_PorytilesPrimaryTutorial(void)
{
    sPrimaryTilesetAnimCounter = 0;
    sPrimaryTilesetAnimCounterMax = 256;
    sPrimaryTilesetAnimCallback = TilesetAnim_PorytilesPrimaryTutorial;
}

If you want to understand the above code step by step, continue to the section below.

Driver Code, Explained

Frames And The Frame Table

First up, we'll need to frame data.

const u16 gTilesetAnims_PorytilesPrimaryTutorial_Flower_Frame0[] = INCBIN_U16("data/tilesets/primary/porytiles_primary_tutorial/anim/flower_red/00.4bpp");
const u16 gTilesetAnims_PorytilesPrimaryTutorial_Flower_Frame1[] = INCBIN_U16("data/tilesets/primary/porytiles_primary_tutorial/anim/flower_red/01.4bpp");
const u16 gTilesetAnims_PorytilesPrimaryTutorial_Flower_Frame2[] = INCBIN_U16("data/tilesets/primary/porytiles_primary_tutorial/anim/flower_red/02.4bpp");

const u16 *const gTilesetAnims_PorytilesPrimaryTutorial_Flower[] = {
    gTilesetAnims_PorytilesPrimaryTutorial_Flower_Frame0,
    gTilesetAnims_PorytilesPrimaryTutorial_Flower_Frame1,
    gTilesetAnims_PorytilesPrimaryTutorial_Flower_Frame0,
    gTilesetAnims_PorytilesPrimaryTutorial_Flower_Frame2
};

This part is straightforward. We need to define some frame data variables for our three frames. Notice that we do not define one for the key frame. The key frame will never actually be displayed in-game. Rather it is simply a Porytiles construct, to help Porytiles determine how to lay out your tiles.png correctly. Also note that we use u16, since VRAM data likes to come in halfwords. In addition to the frame data, we define a frame table. The frame table stores the animation frames in the order in which we want them to appear in the animation cycle. Try changing up this table and recompiling the game to see what happens.

Animation Buffer Copy

Next, we need to create a function that can load our animation into the tile buffer:

static void QueueAnimTiles_PorytilesPrimaryTutorial_Flower(u16 timer)
{
    u16 i = timer % ARRAY_COUNT(gTilesetAnims_PorytilesPrimaryTutorial_Flower);
    AppendTilesetAnimToBuffer(gTilesetAnims_PorytilesPrimaryTutorial_Flower[i], (u16 *)(BG_VRAM + TILE_OFFSET_4BPP(1)), 4 * TILE_SIZE_4BPP);
}

You may ask, what's the timer parameter that is passed into this function? For now, just understand that every game frame, the engine calls a function called UpdateTilesetAnimations (which you can find in tileset_anims.c). This function increments a counter and passes it into the callback we registered in InitTilesetAnim_PorytilesPrimaryTutorial, which you'll see below. The callback can then use the counter to decide which animation it should update, which is very useful for tilesets that have more than one animation. See TilesetAnim_General for a nice example.

We need to highlight a couple more things in this function, specifically with this line. This is the secret sauce that makes your animation work:

AppendTilesetAnimToBuffer(gTilesetAnims_PorytilesPrimaryTutorial_Flower[i], (u16 *)(BG_VRAM + TILE_OFFSET_4BPP(1)), 4 * TILE_SIZE_4BPP);

The first argument to this function, gTilesetAnims_PorytilesPrimaryTutorial_Flower[i], is just an element from the frame table we defined earlier, i.e. the frame we want to copy into the tile buffer. Here, the i variable is computed using the timer value we discussed earlier.

The second argument to this function, (u16 *)(BG_VRAM + TILE_OFFSET_4BPP(1)), is very important. This value is effectively the starting offset of your animation within the tileset's tiles.png. In order to set this correctly, you need to compute the correct value to pass to the TILE_OFFSET_4BPP macro. In our case, this value is 1. Why? Let's take a look below, at this tileset's tiles.png:

You can see here that our animation tiles are stored starting at tile 1, which is highlighted in cyan (recall that tile 0 is always the transparent tile). That's why we pass 1 to the TILE_OFFSET_4BPP macro! For example, if our animation had started at tile 10, we would have passed 10 to this macro instead.

Also note: Porytiles fixes the start tile of your animation based on its alphabetical folder name. If you add another animation, Porytiles may place it before or after this animation on your tiles.png, so you may need to update the values you pass to TILE_OFFSET_4BPP. But that's ok! Once you are done adding animations, their location on tiles.png will remain stable, so you won't need to mess with this anymore. It's just worth mentioning, because if you add another animation after this tutorial, you may have to come back to the C code to make a couple tweaks. It's annoying, but this will be remedied by a future feature.

Finally, the third argument to this function, 4 * TILE_SIZE_4BPP, is just the size of your animation, in bytes. As you can see in the above tiles.png, our animation is 4 tiles long. Thus the 4 in our size expression. If you change the animation frame size, you will need to update this number to the new size!

To summarize, the function AppendTilesetAnimToBuffer takes an animation frame as the first argument, copies it to the VRAM location specified by the second argument, with a copy buffer length specified by the third argument. The animation effect is generated by calling this function every few frames and dynamically copying new tiles into VRAM. That's all there is to it! So how does this function actually get called? Read on.

Main Animation Driver

Next, we'll need to define a function to drive the animation buffer copy. That part is pretty simple. It looks like this:

static void TilesetAnim_PorytilesPrimaryTutorial(u16 timer)
{
    if (timer % 16 == 0) {
        QueueAnimTiles_PorytilesPrimaryTutorial_Flower(timer / 16);=====
    }
}

This function receives the timer variable we discussed earlier. As you can see, it uses timer % 16 == 0 to gate the buffer copy for our flower animation. Every 16 frames, the timer will be divisible by 16, and thus it will perform the buffer copy. The GBA runs at around 60 fps, which means it updates the animation frame roughly 3.75 times per second. Why do we pass the queueing function timer / 16? Recall that the queueing function computes the current frame index via timer % ARRAY_COUNT(frame_table), where timer in the queueing function is the moduloed-by-16 argument. If you think a bit about the logic here (maybe grab a pencil and paper!), you can see how this will simply increment through the array, resetting once it goes past the end. Pretty clever, right?

To see how to handle a tileset that has more than one animation, check out TilesetAnim_General. All you have to do is add more branches to the timer-check if statement. Easy!

Animation Init Function

Finally, we'll need to actually initialize the animation. We will finally define the function we declared in include/tileset_anims.h. It's easy enough, just:

void InitTilesetAnim_PorytilesPrimaryTutorial(void)
{
    sPrimaryTilesetAnimCounter = 0;
    sPrimaryTilesetAnimCounterMax = 256;
    sPrimaryTilesetAnimCallback = TilesetAnim_PorytilesPrimaryTutorial;
}

Here, we set the counter to 0. The counter is what gets passed into our callback as timer. It's incremented every frame, as mentioned above. The max value simply defines a point at which we reset the counter. Since we have been using 16 as our modulo value, it needs to be a multiple of 16. We'll leave it set to 256. You will need to increase it if you add more than 8 animations for your tileset (which is unlikely). Finally, we set the animation callback to the function we defined in the section above. And that's it!

Viewing VRAM In Real Time

If you found any of this confusing, try using mGBA's Tools -> Game state views -> View tiles... menu to see VRAM in real time. You can see your animated tiles getting freshly copied every few frames, right in front of your eyes. This may help you understand how the buffer copy code actually animates your tiles!

Upcoming Porytiles Feature: Automatic Driver C Code Generation

As tracked here, Porytiles plans to eventually generate all this C code for you. This will make things much easier for users with tons of animated tilesets, or for users who find all this C nonsense hopelessly confusing. We've all been there! Look out for the feature in a future Porytiles release. Once the feature is implemented, I will explain how to use it in an updated version of this tutorial page.

Appendix: Adding Animated Water Tiles

TODO : fill in