MaxTile is an extended Tilemap demo. It is designed to create
platform games and other scrolling tilemap games like RPGs. While Mintile only used 8 bits per tile, and supported 256 tiles per object. Maxtile supports 4096, and has a '4 bit drawing mode'. All tiles are 8x8 pixels, and simulated sprite objects can be formed and drawn via mini movable tilemaps. Maxtile is optimized for 4 color tiles, however works with 16 colors too, depending on the platform driver. Unlike Mintile, Maxtile does not need a second 'Draw cache' tilemap, instead a single bit 'flag' is used to mark if a tile needs redrawing, so effectively the tilemap IS the cache. The simplest drawing mode is 'Normal', but X-flip, Y-flip and XY-flip are also supported, which realtime convert the normal tile for display. Extended drawing modes include 'Fastfill' to quicly fill a tile, 'DoubleHeight' to fill an area with a stretched tile (intended to save memory for less important tiles' and 'Transparent' where 0 byte data is not drawn for simple transparency. Extended modes do not support flipping. Drawing to the screen can be done in two ways: 'DoubleBuffered' is the simplest, everything is drawn to the vram every frame, as it's intended a double buffer is being used. This is also handy for testing as there is less to go wrong! 'Cached' is more advanced. The screen is split vertically into 3 caches. Required draws are cached to these caches, and the caches are then flushed to the screen. This minimizes flicker, and the cache flushes can by synced to the raster to further cancel flicker without need for a double buffer. Maxtile works on all systems. Where hardware tile support is available (eg SMS) it will be used, where it is not (eg CPC) sofware tiles will be used, however both work in the exact same way to allow a game to be ported more easily The main code is multiplatform, and has few changes per system, but there is a 'platform driver' which efficiently draws a single tile to the screen depending on the system capabilities Due to it's 2 byte per tile tilemap, MaxTile is NOT suited to low end systems. if your machine only supports 256 hardware tile patterns or has little memory, it is probably not desirable to use MaxTile. |
![]() Tile Format: %TTTTTTTT TTTTMMMD T=Tilenum M=Mode D='Dirty flag' (Tile needs redraw) |
MaxTile is an extended
tile drawing routine, it's designed to work in the same way on all
systems, whether they have hardware tilemap or not! It provides 4096 patterns per tilemap, Flipping and more, however it is not suited to games which need to maximally use the hardware's capabilities. This is because it's designed as 'multiplatform' code, so must function on the more basic systems, often to the determent of the most advanced ones. |
![]() |
Here is Test1 running on the Amstrad CPC. Here we'll look at the multiplatform code, and we'll see how the shared modules of MaxTile are used and their function. |
![]() |
RunTest1 Executes the test routine. First we do some setup before the first draw. We define the source tilemap address, loading the base address and width in bytes (36*2)... We run SetScroll to configure the tilemap for drawing. Next we prep the cropping routines to configure them for a full screen. We draw the first screen. |
![]() |
The InfLoop is the main test loop. We change the X,Y offset of the tilemap source to effect a scroll. The format of the 'offset' is %TTTTTTPP, where T is a full tile shift, and P is a partial (2 pixel) shift and run 'SetScroll' or 'ResetScroll' to perform a redraw of the tilemap as required DrawScreen will draw the tilemap to the screen in double buffered mode, or to the cache in cached mode. |
![]() |
If we're using Cached drawing, FastDrawCaches will dump the 3
caches to the screen, without trying to sync to the raster draw. |
![]() |
BackgroundTilemapBase points to the memory address of the first
tile to draw to the screen. Recalc scroll updates the BackgroundTilemapBase using the ScrollX and ScrollY offset |
![]() |
SetScroll will redraw the full tilemap. We Recalculate the draw area with 'RecalcScroll' flag the tiles for redraw with 'ResetTilemap' Finally we draw the tiles with 'Cache_TilemapCLS' |
![]() |
ReSetScroll can only be used when tiles have moved 1 full tile. It attempts to compare the old and new position, and only redraws areas where the two do not match. This reduces draw time with no visual difference. As each tile uses 2 bytes there are 2 compares per tile. |
![]() |
'FastDrawCaches' will empty the caches to the screen. It does not attempt to sync to the raster beam, so may cause some flicker, but is fast and simple. |
![]() |
Here is Test2 running on the Amstrad CPC. Here we'll look at the multiplatform code, and we'll see how the shared modules of MaxTile are used and their function. |
![]() |
First we set the scroll position of the background tilemap with
'SetScroll' Next we set up the miniTilemap of the player with 'Expand Tilemap'... This takes a 1 byte per tile source tilemap and expands it to 2 bytes per tile (using IX as a tile pattern offset). It can also optionally perform an X/Y flip of the source data. |
![]() |
We set the cropping area for the full screen tilemap. |
![]() |
Before we move the sprites, we need to update the crop-cache which
records the area of the background tilemap which will need
redrawing. To do this we point IX to a sprite object and run 'CalcCropRemoveIX' |
![]() |
At the start of our main loop we move the CPU sprites |
![]() |
We read in from the joystick, and move the player XY pos
accordingly. |
![]() |
If the player pressed fire, we first toggle between the 4 XY flip
options. We use 'ExpandTilemapA' to update the players tilemap and flip as required. |
![]() |
We scroll the background one tick. | ![]() |
ResetTilemap Marks the Tiles in the tilecache for redrawing. ResetScroll performs the draw 'Speedscroll' is only for the CPC, Rather than redraw the tilemap one tile at a time, it uses LDIR to do a memory-memory copy of the entire tilemap effecting the scroll, as the sprites will be in the wrong position, we remove them before this copy. |
![]() |
After handling the player, we redraw all the computer sprites and
redraw the screen. "ClearBorder" is an optional extra, it draws a 4-6 pixel border around the screen (depending on if half or quarter tile scroll is enabled) - this is to cover the 'glitchy' edges of the screen - which occur because our drawing code only works in full tiles. |
![]() |
MovePlayerSprite updates the X,Y position of the player in the
sprite object pointed to by IX from BC The old sprite is then removed from the screen |
![]() |
Draw sprite performs all the tasks of redrawing the screen, All the sprite locations are updated, the tilemap is drawn, then the sprites are drawn. If we're using the cache, all these 'draws' won't appear on the screen yet, so we use 'Fast Draw Caches' to flush the caches to the screen |
![]() |
BackgroundTilemapBase points to the memory address of the first
tile to draw to the screen. Recalc scroll updates the BackgroundTilemapBase using the ScrollX and ScrollY offset |
![]() |
SetScroll will redraw the full tilemap. We Recalculate the draw area with 'RecalcScroll' flag the tiles for redraw with 'ResetTilemap' Finally we draw the tiles with 'Cache_TilemapCLS' |
![]() |
ReSetScroll can only be used when tiles have moved 1 full tile. It attempts to compare the old and new position, and only redraws areas where the two do not match. This reduces draw time with no visual difference. As each tile uses 2 bytes there are 2 compares per tile. |
![]() |
'FastDrawCaches' will empty the caches to the screen. It does not attempt to sync to the raster beam, so may cause some flicker, but is fast and simple. |
![]() |
PrepSpriteIX will perform the tasks for a redraw of the sprite
object pointed to by IX. CalcCropIX will calculate the draw area, based on screen cropping. ZeroSpriteInCache works out the areas of the background tilemap covered by the sprite, and clears the update flag so they are not drawn. FlagSpriteForRedraw flags the sprite tilemap to be redrawn. |
![]() |
CalcCropIX will calculate the area of the sprite to draw, and store the details in the sprites crop cache. | ![]() |
CalcCropRemoveIX also uses the same cropping routine, however this
version calculates the area BEHIND the sprite. It calculates the area of the background tilemap which will need to be redrawn to remove the sprite. |
![]() |
RemoveSpriteIX performs a redraw of object IX | ![]() |
Data
One Byte per tile tilemaps are convenient for storage, but must be 'Expanded' in to 2 bytes per tile for drawing | ![]() |
These are the expanded tilemaps used by the cpu sprites | ![]() |
MaxTile is
pretty complex! If you're looking for something a bit simpler see
MinTile. If you're looking for something far simpler see Grime, which was more character block based. |
![]() |
Lesson
MaxTile3 - Maxtile main code Lets take a look at the core shared module which handles tilemap drawing. |
![]() |
V1_MaxTile.asm
|
![]() |
We draw simulated sprites using the tilemap. We pass a pointer to a data structure which defines the attributes of the sprite. X,Y are the position, W,H are the size - all in logical units (Pairs of pixels) TilemapHL is the address of the tilemap which defines the sprite object. PatternHL is the base address of the pattern data for the sprite. CropCache is the data related to drawing the sprite. CropCacheBack is the data related to removing the sprite. |
![]() |
GetTilemapUnderSprite returns the HL address of the top left
corner of the background tilemap under the current sprite. This is used for removing a sprite. |
![]() |
DoCropLogicalToTile converts XYWH from logical units to tiles. as Logical units are pairs of pixels This effectively divides each by 4. If the Width or Height is zero, nothing should be drawn, so this routine sets the Z flag. |
![]() |
Shift Tilemap calculates the starting position in the background
tilemap for our sprite. If X,Y are in tiles, If effectively calculates: HL=BackgroundTilemapBase + (BackgroundTilemapWidth*Y) + X*2 For speed, This routine is hardcoded to support only a tilemap 32 or 36 tiles wide. |
![]() |
When we want to remove a sprite, we do so by flagging the tiles in
the tilemap for redraw. Each tile is defined by 2 bytes, and we set bit 0 to one to flag the tile for redraw. |
![]() |
FlagSpriteForRedraw sets bit 0 of the Sprite tilemap. This makes the sprite redraw next time. Before flagging, we check if the tile is $FFF2 or $FFF3 - these are transparent empty tiles, so it would be a waste of time processing them for redraw (though it does no harm) |
![]() |
ResetTilemap is simpler, it flags the entire tilemap for redraw. It's intended for the background, so we don't check for empty tiles. it resets BC tiles from HL. it can also be used for flagging a strip at the top or bottom of the tilemap when scrolling. |
![]() |
There may be times (like scrolling left or right) where we want to
update a block of the tilemap. ResetTilemapPart does this. We pass the tilemap address in HL and the width and height of the area to flag in DE |
![]() |
Our sprite may not have moved, but another sprite may
have covered it and moved away. We call this a 'Dirty Sprite' as it needs to be redrawn cleanly. To detect this, we check all the tiles under a sprite, if the tile in the tilemap has been flagged for redraw, then that part of the sprite also needs redrawing. BUT there's a catch! As we can move in 2 pixel jumps, we may not be exactly aligned on the tile that changed, therefore we flag a 2x2 block of tiles in our sprite tilemap for redraw to ensure we redraw everything that was lost. |
![]() ![]() |
When we're going to redraw a sprite, part of the
tileamp will be covered by the newly drawn sprite. We don't want to redraw that part of the tilemap - so ZeroSpriteinCache will clear the redraw flags of the areas covered by the sprite. |
![]() ![]() |
Cache_TilemapCLS will redraw the tilemap into the
Cache or directly to screen. Most of this code is platform specific shifts to the HL Vram destination for half and quarter tile shifts! The DrawTilemap routine is used to perform the actual draw. There are two versions of this, one draw directly to VRAM (for double buffering) the other calculates the draw and stores it in a cache. Well look at those next time! |
![]() ![]() |
DrawSpriteIX draws a sprite from it's mini tilemap. It uses the same DrawTilemap routine... unless we're using hardware sprites, in which case it used DrawTilemapH |
![]() |
Logical Cropping
To save time, we calculate the cropping when a sprite moves, and
store the results in a 'Cache' There are two caches, one for drawing the sprite, and one for redrawing the background tilemap behind it. |
![]() |
CalcCrop Calculates the crop and fills the cache width the results | ![]() |
Our cropping routine will work out the X,Y pos in logical units,
and width and height + any skipped tiles from the source tilemap registers B,C is the X,Y co-ordinate in logical units registers H,L is the Width,Height in logical units register IY is the address of the source tilemap data. First we zero D,E - they are used for temp values We load IXH with the top of screen, and IXL with the height of the screen. |
![]() |
Ok... lets crop the top of the sprite... First we remove the ypos of the first visible pixel from the draw ypos (C)... if the result is greater than zero, then nothing is off the screen at the top. if the result is less than zero we need to crop... we convert the negative to a positive and compare to the height of our sprite (L), if the amount to crop is not less than the height then the sprite is completely offscreen. Anything else is the number of lines we need to remove from the top, we store this in E As we can only draw full tiles, We calculate the correct offset for the first draw and set the new 'draw position' in C |
![]() |
Next we do the same for the bottom, We add the height to the Ypos, and subtract the height of the logical screen, if it's over the screen height (greater than zero) we need to crop again - the result is the amount to crop |
![]() |
We've calculated the top (E) and bottom (D) crop... we now use these to calculate the new height of the sprite (L). | ![]() |
We then skip over any bytes in the source (IY) based on the number
of lines of tiles we need to remove from the top. |
![]() |
now we do the same for the X axis First we remove the xpos of the first visible pixel from the draw Xpos (B) ... if the result is greater than zero, then nothing is off the screen at the left. if the result is less than zero we need to crop... we convert the negative to a positive and compare to the width of our sprite (H), if the amount to crop is not less than the width then the sprite is completely offscreen. Anything else is the number of lines we need to remove from the left, we store this in E. We calculate any needed X offset for the first draw in B |
![]() |
Next we do the same for the right, We add the width (H) to the Xpos, and subtract the width of the logical screen, if it's over the screen width (greater than zero) we need to crop again - the result is the amount to crop from the right (D) |
![]() |
We need to crop, so we calculate the new with in H, and update the
First source byte in IY if required We return Carry clear to confirm the crop found something to draw. Otherwise we set the carry to mark the sprite offscreen. |
![]() |
Draw Tilemap
DE=Source tilemap DE'=Pattern Data HL=Screen Base IYH=Tilemap Width IXH/IXL=Draw Width / Height At the start of each line, we need to prepare the values in our registers, we also back up the stack pointer, as our drawing routines often use stack misuse. On the CPC we have an alternative version of the drawing routines for a 4 pixel vertical shift. |
![]() |
Each tile has two bytes defining it in the tilemap. If the bit 0=1 then the tile needs redrawing, otherwise we skip over it. |
![]() |
There is a some platform code for each system - This moves the
vram address HL across the screen one tile (8 pixels. We repeat for all the remaining tiles on the line. After the line we restore HL and DE |
![]() |
After each line there is some move platform code - this move the
VRAM address DOWN one tile (8 pixels) We add the width of the tilemap to the tilemap source in DE, and repeat for the remaining lines |
![]() |
Expanders
The expanders take a 1 byte per tile tilemap, and expand it into
the 2 byte per tile format used by the drawing routines. the value in IX is used as an offset to the tile numbers in the 1 byte tilemap. ExpandTileMap is the simplest version... ExpandOne coverts a single tile. |
![]() |
ExpandTilemapA also allows for X,Y or XY flip of the tilemap. A defines the Flip Mode (0/1/2/3 =Normal/Xflip/Yflip/XYflip) If we're XY flipping the source data, we need to move HL to the end of the source data, we also need to set the bits of the tile offset in IX to enable flipping of the bitmap data at draw. We work through each line in reverse |
![]() |
The X-flip routine works in a similar way, moving from right to
left through the source tilemap data. |
![]() |
The Y-flip routine is also similar. It first moves to the first tile on the last line, and works backwards through the tilemap. |
![]() |
Each routine uses 'ExpandOne' to convert the individual tiles. A tile value of 0 is a special case - this is a 'transparent tile', and is converted to &FFF2 All others are multiplied by 16 (as MaxTile is optimized for 4 color - 16 byte patterns), and added to iX, before being stored into the destination tile map. |
![]() |
The Cache Code
First we define the format of our cache. Each Cach has 6 bytes, and we define offsets to some of these for reading via (IY+n) addressing |
![]() |
We need to define the structure of the cache table. This differs
depending on the system, and must be writable, so cannot be held in
ROM. There are 3 entries in the tables. The first byte is the last H byte of the HL VRAM address for this section of cache, once the VRAM destination reaches this value, this cache is finished, and the next cache entry is used. "CacheTableAddr" points to the current entry. The second and third entry are the RAM area used by this cache, it's recommended the cache is 256 bytes in size. The second entry is updated as new data is entered into the cache, the third is the original value for resetting the cache. The fourth entry is the Raster timing, it is optional and only used if we're attempting to sync cache draws to the raster beam position.c |
![]() |
GetCacheIY is used when drawing the cache. IY should point to an
entry (Section) in the cache. This command will return a pointer to the cache data in IX... if the first two bytes are &00 &00, then the cache is empty, and we set the zero flag |
![]() |
'DrawTilemap' will process a tilemap and send the tile data to the
cache. When executed registers should have the following values DE=Source tilemap DE'=Pattern Data HL=Screen Base IYH=Tilemap Width IXH/IXL=Draw Width / Height First we load in the address of the entry in the cache, and get the next free memory address for new data in that cache |
![]() |
We're going to process a line of the tilemap First we check the H part of the HL vram destination, and see if we're over the limit of this cache. We use BC' to keep track of what tiles (if any) need drawing in this line of the tilemap |
![]() |
We check the tile, if it's Update flag is not set, it doesn't need
drawing so we skip it. If we need to draw this tile, we check if it's the first we've needed to draw in the line, if not, we skip the next bit. |
![]() |
We've found the first tile in this line... but is this the first
tile in this tilemap? If it is, we need to write the pattern information. Byte sequence &00 &zz &yy defines the pattern data as at address &yyzz 16 color systems may use an extra 'tint' byte, in which case the format would be &00 &zz &yy &tt Note: Byte sequence &00 &00 &00 denotes the end of the cache data, so the pattern data cannot be &0000 or the caches will never draw! |
![]() |
If it's not the first tile in the tilemap, but the first tile on
the line we write the info to draw this line. |
![]() |
If we found a valid tile, we update C' (which is used to
caclulate the tile count to draw. Whatever happened We then move to the next tile in the map, and update the VRAM destination HL - this is platform specific code. |
![]() |
We repeat until we've processed all the tiles. We then look at BC' and calculate the total tiles that need drawing in this line. If this is>0 we write the count into the cache, and move to the next point in the cache. We also check if the cache is full, if it is we'll have to flush it now |
![]() |
We've processed a line, so it's time to move down a line There's platform specific code to move down a strip of tiles. |
![]() |
We now move the source tilemap down a line, and repeat for the
next line of the tilemap |
![]() |
Once we've finished processing we update the address of the next
data in the cache for next time. We then write an 'end of cache marker' &00 &00 &00 |
![]() |
If we've reached the last line of a cache, we write an end marker and move to the next one, then continue processing | ![]() |
If the cache is full during processing the tilemap, we need to
stop what we're doing We write a 'cache end' marker, process the cache and reset the address of the cache data (as it's now empty) As we're starting again with an empty cache, we need to write a new pattern address, so we clear H' |
![]() |
When we're processing the caches in normal time (not because they
are full) we will want to reset them all. We do this by resetting the 'current addresses' from the backup defaults. |
![]() |
When we want to process a cache, we load IX with the address of
the cache We load in the parameters of the line. If we get a 0 length this is a special command - either the end of the cache or a new pattern address. |
![]() |
We're going to draw tiles to the screen, so we back up the stack
pointer as we may be using stack misuse. The CPC has a special 'Half tile Y shift' version to cope with the strange memory layout of the CPC, when a 4 pixel Y move occurs |
![]() |
We check the draw flag, and draw the tile if required. |
![]() |
We now need to move across one tile, This is platform specific code. | ![]() |
Once we've done a line, we restore the SP. We now move to the next line in the cache (5 bytes per line data) We repeat for this line. |
![]() |
If we read a 'Zero count' this is a special command. &00 &00 &00 is the end of cache command anything else (&00 &zz &yy) defines the pattern address for the tilemap as &yyzz - 16 color systems may use an extra 'tint' byte (&00 &zz &yy &tt) |
![]() |