Inside Sovereign: Tileset-Animationen

Inside Sovereign ist ein Format, in dem ich über einige aktuelle Entwicklungen meines Romhacks „Pokémon Sovereign of the Skies“ berichte, oder aktuelle Geschehnisse berichte. Die entnommenen Codebeispiele sind dem Source Code von Pokémon Sovereign of the Skies oder dem Sovereign of the Skies g3headers Fork entnommen. Es handelt sich bei Pokémon Sovereign of the Skies um einen Romhack des Spiels Pokémon Feuerrot (U) [BPRE v1.0]. Etwaige Adressierung und Funktionalität ist möglicherweise nicht auf andere Versionen des Spiels übertragbar. Es werden desweiteren Begriffe aus Romhacking, Programmierung und anderen technischen Disziplinen verwendet, die für das Verständnis dieses Artikels möglicherweise essentiell sind.


Ein, nun, nicht unbedingt ungelöstes, Mysterium der Pokémon Romhacker waren die Animationen, die auf verschiedenen Maps angezeigt werden. Im Grunde ist die Materie simpel, nur gab man sich damit zufrieden, die Animationen mit dem „Tileset Animation Editor“ zu bearbeiten, einem Tool von Lu-Ho. Das Tool ist alt, aber in der Lage, Animationen wie sie im Originalspiel vorkommen, zu erstellen:

Einfache Tileset Animation

Das Tool überschreibt dabei die Bedeutung des function Feldes in der MapBlockset Struktur des Spiels:

/**
 * Blocks from which the map is constructed.
 */
struct MapBlockset {
    /**
     * Whether the tiles are compressed or not.
     */
    bool compressed;

    /**
     * Whether this tileset is to be used as a secondary tileset or primary tileset.
     */
    bool secondary;
    u16 padding;

    /**
     * Tiles used to build blocks.
     */
    void* tiles;

    /**
     * Palettes for the blockset.
     */
    void* palettes;

    /**
     * Block description.
     */
    struct MapBlock* blocks;

    /**
     * Tileset initialization function. Called to set up animation functions.
     */
    void (*function)(void);

    /**
     * Block behaviours.
     */
    struct MapBlockBehavior* behaviors;
};

In einem Vanilla Rom ist dieses Feld einfach ein wie oben beschriebener Funktionspointer, welcher die Animationen des aktuell geladenen Blocksets initialisieren soll. Das geschieht zum Beispiel, wenn eine Map betreten wird, oder ein komplexes Menü geladen wird. (Sprich: Wenn die Tilesets vorher entladen wurden, weil die Character Base im VRAM für andere Grafiken benötigt wird)

Die Grenzen des Animations Editors

Der „Tileset Animation Editor“ überschreibt einen Teil des Loaders mit eigenem Assembly Code, und sorgt so dafür, dass dieses Feld keine Funktion mehr, sondern eine eigene Struktur darstellt, welche einfach ein Framework für Standard Animationen wie oben gegeben bietet. Dadurch verschließt sich natürlich die Möglichkeit, sich selbst eine Animation zu bauen, die nicht nur einfach Frame für Frame im Tileset agiert. (Wie eine GIF Animation)

Wenn man diesen Animations Editor nicht benutzt, kann man sich natürlich trotzdem eine Methode schreiben, die nach einer Standard Struktur Tiles im VRAM ersetzt. Das zu tun, ist grundsätzlich die Hauptaufgabe dieses Konstrukts. Wenn man dafür nicht einfach das function Feld der MapBlockset Struktur überschreibt, hält man sich aber die Möglichkeit für ein paar mehr Animationen offen, die man sonst nicht hätte.

Die Alternative: Handarbeit

Grundsätzlich geht es also zuerst darum, einen Initializer zu haben. Dieser bereitet etwaige Grafiken vor, und wird immer ausgeführt, wenn das Tileset neu geladen werden muss. Dort werden ein paar Konstanten, so wie der Handler für das jeweilige Blockset festgelegt. Die Konstanten sind blockset_???_current_frame (current_frame) sowie blockset_???_max_frame (max_frame). Hierbei handelt es sich um Frame Counter, sie zählen jeweils hinauf bis max_frame und resetten anschließend wieder auf 0. So kann man relativ einfach seine Animationstimings lösen. Für max_frame wählt man am besten einen Wert, der relativ viele Frequenzteiler der später definierten Animationen enthält. Optimal währe das kleinste gemeinsame Vielfache jener Frequenzen, die über den Frame Counter abgewickelt werden. Das hängt ganz einfach damit zusammen, dass man später seine Animationen vermutlich über eine Modulo Operation in die verschiedenen Frames einteilen will. Wenn die Frequenz einer Animation also kein Teiler des max_frame ist, kann es passieren, dass Ungenauigkeiten entstehen, wenn der Frame Counter sein Maximum erreicht. In dieser Beispielmethode, welche den Main Initializer von Pokémon Sovereign of the Skies darstellt, wird max_frame einfach auf 0x280 gesetzt. Dieser Wert enthält relativ viele Teiler, und etwaige Ungenauigkeiten sind kaum sichtbar, wenn das Wasser eines Sees einmal 1-2 Frames früher aktualisiert wird (Das sind Zeiten von bis zu 2/60 Sekunden, da der GBA im Optimalfall mit 60 FPS läuft)

void main_animator_init(void) {
    blockset_one_current_frame = 0;
    blockset_one_max_frame = 0x280;
    blockset_one_animator = main_animator;
}

Der blockset_???_animator (Handler) ist nun verantwortlich, die Tiles auch wirklich zu aktualisieren. Er folgt dem Muster void main_animator(u16 current_frame);. current_frame wird der globalen Variable blockset_???_current_frame entnommen, je nachdem welcher Handler gerade ausgeführt wird. (Für zwei Tilesets existieren natürlich zwei Handler)

Es bietet sich an, eine deskriptive Struktur für seine Animation zu verwenden:

struct TilesetAnimation {
    u16 tile_start;
    u16 frame_length;
    u16 tile_length;
    u16 frame_count;
    const void *image;
};

So oder so ähnlich, geschieht das auch im Animations Editor. (Nur dass wir sie dann verwenden müssen) Ein Beispiel für eine solche Standardmethode könnte so aussehen, sie wird dann einfach vom main_animator mit den entsprechenden Parametern ausgeführt:

void animate_from_structure(const struct TilesetAnimation *anim, u16 tile_skip, u16 current_frame) {
    void *vram_address = (void *)(0x06000000 + (tile_skip * 0x20));
    u8 current_animation = 0;
    while (anim[current_animation].image != (void *)0xFFFFFFFF) {
        void *current_vram = vram_address + (0x20 * anim[current_animation].tile_start);
        u16 max_frame = anim[current_animation].frame_length * anim[current_animation].frame_count;
        u16 used_frame = current_frame % max_frame;
        used_frame /= anim[current_animation].frame_length;
        memcpy(current_vram, anim[current_animation].image + (0x20 * anim[current_animation]
            .tile_length * used_frame), anim[current_animation].tile_length * 0x20);
        current_animation++;
    }
}

Hierbei wird zuerst eine Zieladresse berechnet (Je nachdem ob wir gerade im ersten, oder im zweiten Tileset agieren) – Danach durchforsten wir unsere Liste an Animationen und kopieren jeweils mit memcpy den aktuell zu verwendenden Tile Block. Hier wird jeden Frame etwaig alles kopiert, was nicht unbedingt für die Performance der Routine spricht, aber an Memory spart. Mit den hier gewonnenen Informationen, sollte es aber ein Leichtes sein, eine eigene Methode zu schreiben, die ein Tileset animiert.

Vorteile der Methode

Ich habe gezeigt, wie man die Funktionalität eines alten Tools mit eigenem Code nachbauen kann, aber natürlich ist es jetzt auch möglich, Animationen zu erzeugen, die früher nicht möglich gewesen werden. Ein kleines Beispiel sind Anzeigetafeln aus Pokémon Sovereign of the Skies, welche den Weg oder aus Städten weisen:

Schriftzug Animation

Ein Codebeispiel dazu, lässt sich auch im Github Repository von Pokémon Sovereign of the Skies finden: Text Animator