FlyweightI'll explain the Lix level data structure: what the level contains after the level's text file has been parsed, but before we play the level. This is my first step to describe my
triple dispatch problem.

Consider the Lix level
Building Block Maze.
There are more than 40 yellow building blocks in this level. You'll find their graphics in
./images/geoo/sandstone/. When Lix wants to render this level, it too must load these graphics files.

Even though the level contains over 40 blocks, Lix only loads these 11 images.
Tiles and Occurrences. Look at the exit tile (archway with staircase). This is a single
Tile.
Building Block Maze contains two
Occurrences of this single
Tile, one in the top left and one in the top right.
A
Tile is the result of a tile image loaded from disk. A
Tile has a certain size, a mask, and knows whether it's terrain, steel, hatch, exit, ...
class Tile:
Bitmap bitmap
constructor(String filename):
bitmap = expensive_load_from_disk(filename)
...An
Occurrrence, for the lack of a better word, is a usage of a
Tile in a level. An
Occurrence has a position, rotation, and knows whether it must be drawn normally or rather erase other terrain.
class Occurrence:
const(Tile)* tile
Point position
int rotation
bool eraseReason. Loading a file from disk is expensive, and so is video memory. While it wouldn't be a problem loading 40 tiles instead of 11, it will be nasty when large levels contain 1,000 or 10,000
Occurrences, and we would have a disk load for each.
We want to load the same file only once, and have it in video memory only once. Since a
Tile is
immutable, i.e., it won't change anymore after it has been created successfully, many
Occurrences can refer to the same
Tile without risking bugs from sharing memory.
Problem with my naming. Tile and
Occurrence are classes in Lix. I absolutely wanted to avoid naming either class
Object,
Instance or
Class -- these names are already common OO parlance. Nonetheless, I'm unhappy. Colloquially, I often refer to an
Occurrence as a
Tile. This is a hint that the class names aren't optimal. Alternatives:
- NeoLemmix calls my Tile a MetaTile and my Occurrence a Tile.
- The traditional language of the Flyweight pattern calls my Tile the intrinsic state and my Occurrence the extrinsic state. Not short, not catchy, but standard.
Tile database. How to ensure that we really load every
Tile at most once from disk? When a level parses its text file that calls for two exit
Occurrences, the level shouldn't construct a
Tile for that exit all by itself. Instead, we
defer the responsibility of construction to a dedicated tile database, and, throughout the program, fetch the
Tiles from that database. After all, tricky construction and bookeeping is at the heart of the problem, it makes sense to relieve everybody else from that worry.
The tile database treats the tile filenames as keys. When the
Tile is wanted for the first time, the tile database loads the
Tile from disk, caches it, and returns a reference to it. Every future time the same
Tile is wanted, a reference to the already-cached
Tile is returned.
The tile database, ideally, is its own class: Occasionally, it makes sense to delete and recreate the database, e.g., to unload all
Tiles because their hardware bitmaps are are tied to a screen, and you want to change screen modes. It's also better for testing to avoid many global variables or singleton classes.
Still, even though you should design the tile database so that it's not a singleton, it still behaves much like global state, with the usual problems: You must either pass it around everywhere, or put it in your context object, or just make a single global database object because that's the least nasty solution.
Summary. Flyweight decomposes an object into two parts, the
intrinsic immutable platonic ideal (Lix's Tile) and the
extrinsic part that may vary every time we need the object (Lix's Occurrence). This is useful when the intrinsic part is expensive (Lix must load an image from disk), yet we want hundreds of instances. A
key-value storage creates and remembers the intrinsics. The remainder of the program gets its intrinsics from there via keys (Lix's tile filenames).
Game programmers may
invent all of this themselves. Then, years later, we read on the internet that this is a classic object-oriented pattern with a dedicated name, Flyweight.

-- Simon