Using MAME's tilemap system
Many arcade systems have features for displaying tilemaps. Unlike a bitmap or pixelmap, which specifies an image in terms of a grid of pixels, a tilemap specifies an image in terms of a grid of tiles. Why use tilemaps? First of all, with all the tiles drawn in advance, the game can be simpler because it just needs to say where to place the tiles, rather than drawing each pixel. Second, you can save storage space by reusing tiles.
Some games use fixed, ROM-based tilemaps, where the arrangement of tiles is stored in ROM. One example of this is background in some scrolling vertical shooters: the graphics ROM contains reusable terrain tiles, and the tilemap ROM contains the tile codes and attributes defining the layout. Some games use dynamic, RAM-based tilemaps, where the game stores the tile codes and attributes in RAM. This kind of system is often used for status overlays, showing things like your score, remaining lives, credits, etc.
This page gives a general overview of how you can use MAME's tilemap system. For more detailed information, look at the comments in src/emu/tilemap.h. For more examples, look through src/mame/video for games with tilemaps.
Core Concepts
A tilemap is a 2D grid of rectangular tiles. Each tile has its own characteristics that determine how it is rendered. When creating a tilemap, you specify the number of rows and columns, and the size of the tiles in pixels.
A tile is a single rectangular element within a tilemap. Tiles may be any size, but all tiles in a tilemap must be the same size as each other.
A 4-bit category can be specified for each tile. Each category can be rendered independently.
Depending on the pen, each pixel will be assigned to a layer. There are three layers, but in general, only layer 0 is used. If a pixel doesn't belong to a layer, it will be considered transparent.
Creating your Tilemaps
In MAME, a tilemaps are created by calling tilemap_manager::create. You usually create tilemaps when initialising the video hardware emulation. Before you can create a tilemap, you need to answer two questions: how big are the tiles, and how big is the tilemap? The answer to the first question is usually obvious: it will be the size of the graphics elements used by the hardware. The answer to the second question may seem obvious, too: wouldn't you just make it as big as what the hardware deals with? Usually yes, but some games use absolutely massive ROM-based tilemaps, and making MAME render the whole thing just wastes a lot of memory. In these cases, it's better to make your tilemap cover just a small window of what the hardware sees, and invalidate it when the game scrolls out of range.
tilemap_t &tilemap_manager::create(decoder, tile_get_info, mapper, tilewidth, tileheight, cols, rows, allocated);
You can usually get a reference to your tilemap manager by calling machine().tilemap().
Pass a pointer to your tile info callback for tile_get_info (this is discussed later).
The mapper defines how a row/column location is mapped to a tile number. You can pass an enumerated value for a standard mapper, or supply your own function. Standard mappers support perfect packing in row-major or column-major order, covering all the most common use cases. If you need to supply a custom function, it will be supplied with a driver_device reference, the row and column to map, and the number of rows and columns in the tilemap.
Standard Mapper | Description | Tile Order |
---|---|---|
TILEMAP_SCAN_ROWS | scans rows from left to right consecutively from top to bottom | 0 1
2 3 |
TILEMAP_SCAN_ROWS_FLIP_X | scans rows from right to left consecutively from top to bottom | 1 0
3 2 |
TILEMAP_SCAN_ROWS_FLIP_Y | scans rows from left to right consecutively from bottom to top | 2 3
0 1 |
TILEMAP_SCAN_ROWS_FLIP_XY | scans rows from right to left consecutively from bottom to top | 3 2
0 1 |
TILEMAP_SCAN_COLS | scans columns from top to bottom consecutively from left to right | 0 2
1 3 |
TILEMAP_SCAN_COLS_FLIP_X | scans columns from top to bottom consecutively from right to left | 2 0
3 1 |
TILEMAP_SCAN_COLS_FLIP_Y | scans columns from bottom to top consecutively from left to right | 1 3
0 2 |
TILEMAP_SCAN_COLS_FLIP_XY | scans columns from bottom to top consecutively from right to left | 3 1
2 0 |
tilewidth and tileheight are the width and height of the tiles in pixels. Remember all tiles must be the same size.
cols and rows are the number of columns and rows in the tilemap, respectively. The size of the tilemap in pixels can be found by multiplying the number of columns by the tile width and the number of rows by the tile height.
You can optionally supply a pointer to memory you've allocated yourself for the new tilemap_t instance, but in general letting the tilemap manager handle allocation is a good idea.
Tilemaps are tracked by the MAME core, so they are automatically saved and freed. There is no corresponding call to manually dispose of a tilemap.
The Tile Info Callback
When MAME wants to know what to draw at a particular location in your tilemap, it calls the tile info callback function that you supplied when you created the tilemap. This function takes the tile index as a parameter, and sets the tile information. You can declare tile info callbacks with the TILE_GET_INFO_MEMBER macro. Your callback takes three parameters: a reference to the tilemap itself called tilemap, a reference to the tile_data to fill called tileinfo, and the tile index according to your mapper as tile_index.
You usually set the tile information with the SET_TILE_INFO_MEMBER macro or by calling tile_data::set (they accept the same parameters):
SET_TILE_INFO(gfx, code, color, flags) void tile_data::set(gfxnum, rawcode, rawcolor, flags);
gfx is the graphics bank to take the tile from, code is the graphics code and color is the color code. You can combine the following values for the flags:
- TILE_FLIPX to flip the tile horizontally
- TILE_FLIPY to flip the tile vertically
- TILE_4BPP if the tile data is packed four bits per pixel format
- TILE_FORCE_LAYER0, TILE_FORCE_LAYER1 or TILE_FORCE_LAYER2 to force all pixels in the tile to be assigned to a particular layer (TILE_FORCE_LAYER0 is often used to force a tile to be opaque)
There are several additional members not set. The most important are category which lets you render groups of tiles independently, and group which lets you select one of 256 sets of pen-to-layer mappings. Remember categories let you render groups of tiles independently, while layers let you render pixels within tiles independently. If you don't need this level of control, you can leave both of them with their default of zero.
Setting Up Transparency
Tilemaps handle transparency using the layer mechanism. When drawing any of the three layers, any pixel mapped to a different layer is rendered transparent. There's also a virtual transparent layer for pixels that are transparent on all three layers. There are a few methods for setting up transparency, depending on the flexibility you need:
void tilemap_t::map_pens_to_layer(group, pen, mask, layermask); void tilemap_t::map_pen_to_layer(group, pen, layermask); void tilemap_t::set_transparent_pen(pen); void tilemap_t::set_transmask(group, fgmask, bgmask); void tilemap_t::configure_groups(gfx, transcolor);
The most flexible way to map pens to layers is by calling map_pens_to_layer. For tiles in group, map pixels to to layermask where the pen number masked with mask equals pen. Set layermask to either TILEMAP_PIXEL_TRANSPARENT or a bitwise combination of the three TILEMAP_PIXEL_LAYERn flags. To map all pens in the group to the same layermask, set mask to 0; to map a single pen to a layermask, set mask to ~0 (this is what the helper map_pen_to_layer does).
If a single pen colour is used for transparent pixels, set_transparent_pen is the choice for you. Just supply the pen number you want to make transparent, and all other pens will be mapped to layer 0. Note that this is only applied to group zero (the default tile group), so you'll need to set up additional groups using other methods. It also maps all other pens to layer 0, so if you're using this as part of a more complex setup, remember to call it before mapping other pens.
You can map any of the first 32 pens to layers 0 and/or 1 by calling set_transmask. For any bit set in fgmask, the corresponding pen is mapped to layer 0, and for any bit set in bgmask, the corresponding pen is mapped to layer 1 (LSB corresponds to pen 0, MSB corresponds to pen 32). If a bit is unset in both fgmask and bgmask, the corresponding pen is unaffected; pens with no corresponding bit are always unaffected.
TODO: describe configure_groups
Manipulating Tilemaps
MAME can do a few tricks with tilemaps with no work on your part. The most common tilemap manipulation functions are detailed here.
tilemap_t::set_user_data(user_data);
Sets an arbitrary kept by the tilemap that you can retrieve later by calling tilemap_t::user_data (e.g. in your tile info callback).
tilemap_t::set_palette_offset(offset);
Apply a palette offset to the color values for a tilemap, to be added to each pixel value before looking up the palette. offset is the palette offset.
tilemap_t::enable(enable);
Enable or disable drawing of a tilemap. When a tilemap is disabled, requests to draw it are ignored. The default value for enable is true, so calling with no arguments will enable the tilemap.
tilemap_t::set_flip(attributes); tilemap_manager::set_flip_all(attributes);
Set flipping for a tilemap, or for all tilemaps created by a tilemap manager. attributes should be zero, TILEMAP_FLIPX, TILEMAP_FLIPY or the logical sum of these values.
Scrolling
Although some games use tilemaps that are the same size as the display area (and therefore fit on the screen neatly), many use tilemaps that are much larger than the screen and scroll them around as necessary. Lots of games can scroll parts of a tilemap independently, too. MAME caters for most of the common cases very elegantly.
One thing to note before going further is that MAME considers tilemaps to be circular: if you scroll it so the left edge is to the right of the left edge of the screen, MAME will wrap the right edge of the tilemap around to fill in the space and vice versa (and likewise vertically). If that isn't the behaviour you want (for example if you want the space to be all transparent), you will need to tell MAME that the tilemap is bigger than it really is when you create it, and write a custom mapper function to make life easier when writing your tile info callback.
Let's start with the simple functions that scroll the whole tilemap. These functions may be called in the VIDEO_START handler to set up global scroll offsets, or they may be called in write handlers for control registers that scroll the whole tilemap:
void tilemap_t::set_scrolldx(dx, dx_flipped);
This function scrolls the entire tilemap horizontally, dx is the amount to scroll by when the tilemap is not flipped horizontally, and dx_flipped is the amount to scroll by if the tilemap is flipped horizontally (both in pixels).
Positive dx moves the tilemap to the right and negative dx moves the tilemap to the left. Since dx_flipped applies when the tilemap is flipped horizontally, its behaviour is the inverse: positive values move the tilemap to the left and negative values move the tilemap to the right. Note that values passed to this function are absolute, in that they are not relative to the current horizontal scroll offset of the tilemap.
void tilemap_t::set_scrolldy(dy, dy_flipped) const;
This is the equivalent function for scrolling the entire tilemap vertically: dy is the amount to scroll by when the tilemap is not flipped vertically, and dy_flipped is the amount to scroll by if the tilemap is flipped vertically (both in pixels).
Positive dy moves the tilemap down and negative dy moves the tilemap up. Since dy_flipped applies when the tilemap is flipped vertically, its behaviour is the inverse: positive values move the tilemap up and negative values move the tilemap down. Note that values passed to this function are absolute, in that they are not relative to the current vertical scroll offset of the tilemap.
You can also get the current overall scroll offsets for a tilemap:
int tilemap_t::get_scrolldx() const; int tilemap_t::get_scrolldy() const;
These functions return the horizontal and vertical scroll offsets for the tilemap passed in tmap, respectively. The values returned are appropriate for the current flip state of the tilemap (you can't easily retrieve the values for the opposite flip states).
That's all very well and good if you only want to scroll the whole tilemap at once, which may be all the hardware can do. But that isn't the always the case: often a machine is capable of independently scrolling parts of the tilemap. This is often called row/column scroll. Before you can scroll parts of a tilemap, you need to tell MAME how many independently scrollable rows and/or columns the hardware supports:
void tilemap_t::set_scroll_rows(scroll_rows); void tilemap_t::set_scroll_cols(scroll_cols);
These functions set the number of independently scrollable rows and columns, respectively, where scroll_rows is the number of independently scrollable rows and scroll_cols is the number of independently scrollable columns.
These functions are usually called in the VIDEO_START handler, as this is generally a fixed hardware capability. If you don't call these functions, MAME will assume the tilemap consists of one row and one column: that is, you can only scroll the whole lot at once.
How does MAME know how tall each row is and how wide each column is? It just divides the height and width by the number of rows and columns, respectively. If your hardware has uneven or unusual size columns, you'll need to do something fancier. MAME makes the common cases easy, and the exotic cases possible.
Now that you've told MAME how many independent rows and columns there are, you can scroll parts of the tilemap. These functions are usually called from write handlers for the tilemap chip's control registers:
void tilemap_t::set_scrollx(which, value);
Scrolls one row of the tilemap horizontally, where which is the row number (0 for top row up to scroll_rows - 1 for bottom row) and value is the amount to scroll by (in pixels). Positive value moves the row to the right, and negative value moves the row to the left. Note that value is absolute (not relative to the current scroll value for the row), and things are reversed if the tilemap is flipped.
void tilemap_t::set_scrolly(which, value);
Scrolls one column of the tilemap vertically, where which is the column number (0 for leftmost column up to scroll_cols - 1 for rightmost column) and value is the amount to scroll by (in pixels). Positive value moves the column down, and negative value moves the column up. Note that value is absolute (not relative to the current scroll value for the column), and things are reversed if the tilemap is flipped.
When Things Change
MAME caches tilemaps aggressively to avoid unnecessary CPU load, so you need to let it know when parts of a tilemap need to be redrawn. Examples of when this may happen are when the game writes to tilemap memory for a RAM-based tilemap, or when the tilemap scrolls out of range when you're only rendering part of a large ROM-based tilemap. There are two functions that can be used: tilemap_mark_tile_dirty marks a single tile as needing to be redrawn, and tilemap_mark_all_tiles_dirty marks entire tilemaps as needing to be redrawn.
void tilemap_t::mark_tile_dirty(memory_index);
Marks a single tile as needing to be redrawn. memory_index is the index of the tile to be redrawn.
void tilemap_t::mark_all_dirty(); void tilemap_manager::mark_all_dirty();
Mark an entire tilemap or all tilemaps created by a tilemap manager as needing to be redrawn.
Drawing Tilemaps
You can draw a tilemap into a bitmap any time you want. In general, though, you do this in your VIDEO_UPDATE callback, to get graphics onto the emulated display. The most common function used for this is tilemap_draw:
void tilemap_t::draw(screen, dest, cliprect, flags, priority, priority_mask);
screen and dest are the destination device and bitmap, respectively. Usually, you pass the screen and bitmap arguments you received in your VIDEO_UPDATE callback.
cliprect is the clipping rectangle. Drawing will be restricted to this area. You usually pass the cliprect argument that you received in your VIDEO_UPDATE callback.
flags is a combination of the following values:
- TILEMAP_DRAW_CATEGORY( x ) specifies the category of tiles to draw (default 0)
- TILEMAP_DRAW_LAYER0 to draw only layer 0
- TILEMAP_DRAW_LAYER1 to draw only layer 1
- TILEMAP_DRAW_LAYER2 to draw only layer 2
- TILEMAP_DRAW_OPAQUE to draw everything, even if it's normally transparent
- TILEMAP_DRAW_ALPHA( x ) to draw with alpha blending
- TILEMAP_DRAW_ALL_CATEGORIES to draw all tiles irrespective of their category
If no TILEMAP_DRAW_LAYERn flags are specified, layer 0 is assumed. If a combination of these flags are specified, only pixels mapped to all specified layers are drawn.
priority is the priority value to store in the priority bitmap for pixels that are not transparent (default 0), and priority_mask controls which bits are set (default 0xff).
Advanced Tilemap Drawing
Haven't thought about this section yet, either. It should cover priority masks, ROZ, etc.
Examples
Will get to this when time permits.