Skip to content

Game engine documentation

Marc-Andre Dion edited this page Jan 6, 2023 · 48 revisions

Overview

This page describes the high-level workings of Link's Awakening game engine. It explains how the engine is structured, how the various subsystems are made, and how the programming decisions impact the game design itself.

(N.B.: for low-level details or hacking notes, use more detailed documentations, like the Maps data format documentation.)

Summary

  1. Top-level architecture
  2. Graphics
  3. Rooms
  4. Entities
  5. Audio
  6. Color conversion

1. Top-level architecture

Rendering on the Game Boy is not straightforward. It has no double-buffering, and the PPU (the graphics processor) can only be accessed during the short amount of time between two frames: the V-blank interval.

So the standard rendering setup is to pre-compute each frame while the previous one is being drawn by the PPU – and then, when the short V-blank interval occurs, to copy all pre-computed resources and values to the now-unlocked VRAM on the PPU.

This is why Link's Awakening engine has two main systems that perform these tasks: the Render loop and the V-blank interrupt handler.

The render loop

The main render loop is the piece of code responsible for handling all the game logic. It runs while the PPU is rendering a frame (during which access to VRAM is locked). It is responsible for:

  • Reading joypad inputs,
  • Executing the gameplay code,
  • Configuring the audio output,
  • Configuring the rendering of special effects using H-blank manipulation.

The main render loop can also upload large chunks of graphics to VRAM, by temporarily disabling the LCD screen (see the "Graphics" section below).

At the end of the loop, it waits for V-Blank, then starts rendering a new frame.

See https://kemenaran.winosx.com/posts/links-awakening-render-loop/

The V-blank interrupt handler

The V-blank interrupt handler is a smaller piece of code, called by the CPU as soon as the PPU enters the V-blank period. During this short time, it can:

  • Copy a small amount of tiles to VRAM,
  • Update animated tiles,
  • Update the tiles representing Link's sprite (depending on its animation state),
  • Copy the palettes buffer to VRAM,
  • Copy the sprites attributes buffer to the OAM memory in VRAM.

It is also responsible for handling some animations during the photos cutscenes.

2. Graphics

As we saw above, graphics can only be uploaded to VRAM during the V-blank interval, or while the LCD screen is off. This is why the gameplay code never copy graphics directly to VRAM: it instead sets flags to tell the engine which graphics should be uploaded at the next iteration of the render loop.

Link's Awakening engine provides a few different facilities for this.

The gameplay code can set flag to request these resources to be loaded during the V-blank interval between two frames:

  1. Uploading a new objects tilesheet,
  2. Uploading a new entity spritesheet,
  3. Uploading color palettes (CGB only).

But some graphics resources are quite large, and uploading them only using the few cycles available during the V-blank interval would take a lot of frames.

This is why the gameplay code can also request graphic resources to be loaded all at once, while the rendering is disabled. For this, the LCD screen is shut down (which displays a white screen, and unlocks VRAM), then graphics can be copied to VRAM using an arbitrary amount of time. At the end of the copy, the screen is enabled again. This results in a white screen being displayed during the upload, but it is much faster than using the V-blank time interval alone.

This technique can be used by the gameplay code to:

  1. Upload a large tileset,
  2. Upload a large tilemap (and, on GBC, the associated tilemap attributes).

2.1 Updating Objects tilesheets (during V-blank)

Rooms are displayed by composing two high-level primitives: objects and entities. The objects are the static 16x16 blocks that compose the room structure; they are displayed using the Game Boy Background map. Entities are the dynamic actors of the room (NPCs, enemies, etc); they are displayed using sprites.

Objects in the overworld rooms are rendered using two types of tiles:

  1. A common set of overworld tiles shared between all overworld rooms;
  2. A set of tiles specific to the current section on the overworld.

So during a transition from a room to another, the gameplay code can to request the tilesheet for the new room to be loaded during the next V-blank period.

Overworld rooms are grouped in 2x2 sections (i.e. 4 rooms). Each section has a tileset ID. When a room is transitioned to, before starting the transition, the game checks if the tileset of the new room is different from the current one, and if so write to hNeedsUpdatingBGTiles to request the 32 specific tiles for this section to be uploaded to VRAM during the next V-blank period.

To avoid glitches during the transition, there is a special “Keep Current” tileset, which instructs the engine not to change the tileset, whichever it is. The world map is constructed so that most of the time, the player always walks through a “Keep Current” section between two different sections, which avoids transition glitches. See “The hidden structure of Link's Awakening Overworld map”

2.2 Updating entity spritesheets (during V-blank)

Entities are the dynamic actors in the game, usually displayed using sprites.

At a given moment, 4 spritesheets of 16 tiles each are reserved for entity sprites. The gameplay code can write to hNeedsUpdatingEntityTilesA or hNeedsUpdatingEntityTilesB to request a spritesheet to be loaded.

During the following V-Blank periods, when the VRAM is unlocked, the interrupt code reads that request, and copies 4 tiles from the spritesheet to the requested location. Only 4 tiles are copied because the V-Blank period is quite short; which means loading a whole spritesheet spans across several frames.

2.3 Uploading a color palette (during V-blank)

TODO: document usage of wBGPal1

2.4 Uploading a large tileset (screen off)

TODO: document usage of wTilesetToLoad

2.5 Uploading a large tilemap and tilemap attributes (screen off)

TODO: document usage of wBGMapToLoad

2.6 Animated tiles

TODO

3. Rooms

The main gameplay takes place in non-scrollable screen-wide rooms. These rooms can be outdoor (on the Overworld) or indoor (inside houses, dungeons, etc.)

Room groups

The game rooms are split into 3 room groups, of 256 rooms each (plus a special group):

  • Rooms group 1: contains all the 256 Overworld rooms;
  • Rooms group 2: contains rooms for dungeons 1 to 6, plus some caves;
  • Rooms group 3: contains rooms for dungeons 7 and 8, and the remaining of caves, houses, etc.;
  • Special group: contains rooms for the color dungeon.

So a specific room can be identified using its room group (1-3) and its room id in this group (0-256).

The three room groups, and their respective rooms. The Color Dungeon special group is not represented here. Credits: Saver

Maps and layouts

TODO: maps

On the Overworld, rooms are stored as they are laid out during the game, on a 16x16 grid of rooms.

But indoors, for space efficiency, rooms are not stored continuously. For instance, when moving through the right door of a dungeon room (e.g. room 0x34), the new room is probably not the next room in the rooms group (that would be room 0x35).

To know which rooms are North, South, East and West of a given room, the game uses layouts. These look like the dungeons maps, but store the ids of each of the dungeons room as they are arranged geographically.

The map layout for the first dungeon.

Room structure

A room is defined by:

  • The floor object it uses;
  • The animated tiles it uses;
  • A list of static objects in the room (walls, trees, etc);
  • The dynamic entities in the room (NPCs, enemies, etc.).

To load a given room, the game constructs an in-memory map of the room objects. For this, the game:

  1. Reads the room floor object, and fill the whole objects map with it;
  2. For each object in the objects list:
    1. Read the object command (which object, where, spanning how many slots, vertical or horizontal);
    2. Write the objects to the map.

3.1 Displaying a room directly

After selecting a save file, when entering a door, or when warping to a new location, the game loads a new room in a single pass.

The Overworld gameplay handler is responsible for this: it can load a room’s data all at once.

However this takes way more time than a single frame. To avoid blocking the main loop for a long time, which would prevent the engine from processing audio, events, etc., the loading is split into several loading stages. When a room is loaded, the Overworld gameplay handler will execute one of these stages, then mark the stage as completed and return to the main loop. The next iteration of the loop will then dispatch to the next loading stage. Each stage should then take no more time than a frame.

The loading stages are:

  1. Decode the room object map to memory, and instanciate the room entities.
  2. Lookup the room tileset to use, and request it to be copied to the VRAM. _(The copy will be requested using hTilesetToLoad, which will shut the console screen off to unlock VRAM and perform the copy faster).

Once all the stages are done, it then transition to the 8 stage, which is the interactive gameplay.

3.2 Transitioning between rooms

Transitioning smoothly between rooms, using the translation animation seen in the game, is quite more complex.

This is for two reasons:

  • As the transition is continuous, the game must render continuously: it can’t shut the screen off to copy new data faster.
  • During the transition, both the old and new room will be visible. So graphics for both rooms must be available at the same time.

To do this, the game will perform roughly the same steps, but with special care to keep all of them within the frame budget.

TODO: continuous loading steps

4. Entities

Entities are the dynamic actors present during the main gameplay (NPCs, enemies, etc); they are displayed using sprites.

4.1 Loading entity graphics

Entities are displayed on screen using sprites. On the Game Boy, sprites can be either a single tile (8x8 px) or two tiles stacked vertically (8x16 px). And usually, entities are composed of several sprites stitched together.

So for an entity to be displayed on-screen, its tiles need to be copied to VRAM, and then the tiles indices must be referenced properly (usually depending on the entity position: facing frontward, backwards, left or right).

As we saw above, at any point in the main gameplay, there are 4 slots available in VRAM, corresponding to 4 entity spritesheets. Each spritesheet has 16 tiles. A spritesheet may contain tiles for a single entity (like a Moblin), for several entities (like Keeses and Beetles, both on the same spritesheet), or for only part of an entity (like the Bottle Grotto genie, which uses 2 spritesheets, or the Angler Fish, which uses all 4).

Each room defines which spritesheets should be loaded for its entities to be displayed. For this, each room defines four associated spritesheet-ids. When transitioning from a room to another, the game engine compares the spritesheets currently loaded in VRAM with the spritesheets requested by the new room, and marks the non-loaded-yet ones as needing to be copied to VRAM (see 2.2 Updating entity spritesheets (during V-blank)).

Example of spritesheet The fours spritesheets for room 07 on the Overworld.

Number of spritesheets available

During a scrolling transition from one room to another, the spritesheets for both the old and the new rooms need to be available (otherwise entities from the old room would look corrupted during the transition). So a room can only load two new spritesheets: the other twos, used by the entities of the previous room, need to remain available.

This limitation doesn't apply when warping directly to a new room, without a scrolling transition (for instance when moving through stairs, or from the overworld to indoor rooms). In that case, all four spritesheets are loaded at once. Some large entities are specifically placed in rooms accessible only by warps, so that they can use all four spritesheets (for instance Evil Eagle or Manbo the Sunfish).

Also, on the overworld, the first spritesheet is always overwritten with the sprites of the NPC following the player (Bow-Wow, Marin, ghost, etc.). Inside dungeons and houses, although, the first spritesheet can be used to display extra enemies.

Mapping an entity to tiles

Interestingly, there's no dynamic allocation of sprite slots. For a given room, the spritesheets will always be loaded in the same slots. Most of the entities require a specific slot to be used: for instance, Octorocks are expected to have their spritesheet loaded in slot 2, as well as Moblins. Which also means that Octorocks and Moblin cannot be displayed in the same room (because they both expect the same spritesheet to be used).

(Some entities use different spriteslots depending on the room they are in, but that's an exception.)

That means the entity-to-sprite mapping is a simple array of tiles indices, hard-coded depending on the expected spriteslot.

For instance, let's say the tiles for a front-facing Moblin are the tiles n° 2, 3, 4 and 5 in its spritesheet. We're using 8x16px tiles, so referencing tiles 2 and 4 will be enough. As rooms load the Moblin spritesheet in slot 2, for our two sprites to reference these tiles, we'll write something like:

MoblinFrontFacingSprites:
  db ($20 x 2) + 2
  db ($20 x 2) + 4

Color dungeon special cases

TODO

5. Audio

TODO

6. Color conversion

TODO

Coloring room objects require two extra pieces of data: tile attributes and color palettes.

6.1 Coloring Background objects

A tile attribute defines which palette is applied to this tile. As an object is 2x2 tiles, each object needs 4 tile attributes.

Unlike the object-to-tilemap mapping, which is the same for all rooms of a map group, the object-to-tile-attributes mapping is room-specific. This allows the game engine to, for instance, color the rocks of a certain room using the BG0 palette – but in another room, to use the BG2 palette for those same rocks.

To use less space, object-to-tile-attributes mappings are factored into groups, which can be used by several different rooms. That means that finding the proper tile attributes for an object is done with the following steps:

  1. Get the map group and room id,
  2. Find the pointer to the tile attributes mapping,
  3. Get the object id,
  4. Find the tile attributes (4 bytes) in the mapping for this object id,
  5. Copy the tile attributes to the BG VRAM.

6.2 Coloring sprites

6.3 Loading palettes

Which palette is used for a room is defined by an index at 21:42EF (one byte per room) which is then used to index an table at 21:402B to get the pointer to the actual palette in bank $21.