This is an example of constructing and storing maps in a way that is efficient in terms of both bytes and tiles.

The map in this demo takes only 98 bytes and 26 tiles (not counting the player sprite). That's about 2.4% of the dedicated map space or 1.2% of the total map space. As implemented here, the decoding and map functions - the main things you'd actually need in a game - take 666 tokens. It's meant more as an example than a library, but it's usable as it is, and you're welcome to do so. There's plenty of room for optimization, but I didn't want to make it too hard to read.

The first points of interest are the encode() and decode() functions, which allow you to write and read arbitrary numbers of bits. These makes it trivial to serialize data with a minimum of waste.

The map is made of of "objects." Each object has a type, a shape, and a rectangular bounding box.

The map is rectangular and divided into 8x8 tile sections, referred to here as "fragments." Dividing up the map this way reduces the storage needed for each object. If the map is 256 tiles wide, storing each object's X position would take a full byte per object. By dividing it into fragments 8 tiles wide, an object's X position takes just 3 bits. The tradeoff is a small (2 bits, in this case) end-of-fragment marker per fragment and a little more code.

A map fragment consists of any number of objects followed by an end-of-fragment marker. Format:
shape/eof: 2 bits (if 0, there are no more objects in this fragment; 1-3 indicates a shape)
type: 4 bits +1
x: 3 bits
y: 3 bits
width: 3 bits +1
height: 3 bits +1

That makes the total size of a map 12 bits + 8 bits per screen + 18 bits per object. This format can be adjusted easily; see the load_map() and read_fragment() functions for reading and the create_level() and write_objects() functions for writing.

An object's type definition simply specifies its draw function and base tile. The number stored is an array index.

Giving object 1 a blank draw function is a handy trick: it allows object 1 to be used as an eraser. That is, it can be placed on top of other objects to erase parts of them, allowing you to create complex shapes with fewer objects and without adding more shape functions. One caveat, however: this doesn't always work right when an object extends beyond the boundary of the fragment it's placed in. Objects in later fragments will always appear on top of those in earlier ones.

The shape of an object determines which tiles in its bounding box it actually occupies:

The number stored identifies a function that takes a position in the rectangle and returns true or false, indicating whether that position should be filled.

As for tiles, there are a few different drawing functions here. One simply calls spr(), drawing a single tile as usual. The others divide tiles into 4x4 sub-tiles and construct larger tiles from those. While this is a bit more limited than using full 8x8 tiles, it means every possible edge and corner piece can be drawn with just five tiles.

There's nothing in this map besides background objects, mind. In a real game, you'd probably want to place enemies, items, etc. These can be encoded in a similar way, with a few bits each for the type and coordinates. You might put them after the map objects for each fragment, but it might make more sense to store them separately.

This is a pretty basic implementation of these concepts, and there are a number of ways it could be made more efficient. However, this is highly dependent on the specific game and maps. What's more efficient for large maps with lots of similar objects may be less efficient for smaller maps with fewer but more varied objects. There's always a tradeoff; saving a few bits in one area will require either using more elsewhere or adding more code. If you want to cram as much stuff as you can into a game, it's worth working out how to save as much space as possible. A couple of bits here and there can add up to a lot with hundreds of objects.

One of the simplest things to change is fragment size. In general, smaller fragments are better for maps with more objects. Comparing 8x8 and 16x16 fragments, 8x8 requires 6 more bits per screen (3 additional end-of-fragment markers), while 16x16 requires 2 more bits per object (coordinates are 0-15 instead of 0-7). Fragments don't have to be square; 8x16 or 32x4 will work just fine. They can also be different sizes on different maps.

Increasing the number of bits used for an object's size may or may not be a good idea. Increasing width and height by one bit each increases the maximum size to 16x16, but it also means using 2 more bits for every single object. If you've got a lot of large objects, it's probably worth it. But if most things are 8x8 or smaller, it would make more sense to use fewer bits and build larger objects from multiple smaller ones when needed.

Some other possible modifications for efficiency or different behavior:

Tilesets - Use fewer bits for the object type, but with a different set of objects depending on the map.

Meta-objects - Instead of placing rectangles directly in the map, use them to build more complex objects or screen layouts and place those in the map. Metroid is a very noticeable example of this.

Variable-length encoding - You can save more space by using fewer bits for more common values. You can also use different numbers of bits depending on object type. For instance, wall and floor objects might use 4 bits for width and height, while flat platforms use 3 for width and none for height.

Background and foreground layers - Each tile can have two objects and two draw functions. It may be worth flagging foreground objects that completely hide the background to avoid unnecessary drawing.

Different tile sizes - If 8x8 pixel tiles don't work for you, it's pretty easy to make them bigger or smaller; just use pixel coordinates on the tile sheet instead of tile numbers in the object type definitions. Using flags becomes trickier, however.

Different sub-tile sizes - For a game with a top-down perspective, where the front of an object is taller and the back is shorter, it might make sense to use different tile quadrant sizes. It's a simple change to make the corners and edges 4x5 pixels on the bottom and 4x3 on top.

Note: This cartridge's settings do not allow embedded playback. A [Play at lexaloffle] link will be included instead.

If anyone wants them, the unminified game, map editor, and other stuff are available here. They're pretty messy, but feel free to do whatever with them. Just be aware that most things "save" via printh. Not a great way to do things, but it worked out.