MaxTile
- Enhanced Software/Hardware Tilemap
Introduction to the MinTile
Series...
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) |
Tile definitions in detail:
The two byte tile definitions contain 4 bits
which define the tile type.
These work as a 'binary tree', so the
simplest mode is drawn most quickly.
Some bits, such as BB can be used for a bank,
palette or just as extra tile number bits depending on the platform
driver.
%NNBBXYPU U=update
P=Program YX=flip NN NN=Tile H
BB = Bank / palette / top 2 bits of tile num
&???0 %???????0 ???????? = Tile to be skipped
&???1 %???????1 ???????? = Tile to be drawn
&nnn1 %nnnnBB01 nnnnnnnn = Normal
tile (&-1)
&nnP3 %BBPP0011 nnnnnnnn = Custom Program PP (&-3) (PP=0... Fill /
PP=1... Stretch / pp=3+bb=3... empty (for sprites / pp=2 unused)
&nn03 %BBPP0011 nnnnnnnn = Program Fill
&nn13 %BBPP0011 nnnnnnnn = Program Stretch
&FFF3 %11110011 11111111 = Program Empty tile
(&F3 FF / F2 FF)
&nn33 %nn110011 nnnnnnnn = Transparent tile (&33
nn / &7C nn / BC nn / F3nn)
&nnn7 %nnnn0111 BBnnnnnn = Y flipped tile (&-7)
&nnnB %nnnn1011 BBnnnnnn = X flipped tile (&-B)
&nnnF %nnnn1111 nnnnnnnn = XY flipped tile (&-F)
Lesson
MaxTile1 - The Minimal Test 1
Lets take a look at the 'Minimal Test.' for MaxTile
This produces a simple scrolling background on a system, and is
intended to test the Flip, Fill and stretch tile functions during
development of MaxTile |
 |
MaxTile_Test1.asm
|
|
 |
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.
|
 |
The Tilemap
The Tilemap contains the sample data. Each
tile is 2 bytes.
The example tilemap is split into lines by
type, these lines can be remmed out to disable types of tile during
testing/development of a new platform driver
Lesson
MaxTile2 - The Midpoint Test 2
Lets take a look at the Extended Test.
This provides a scrolling tilemap, and various moving 'sprite
objects'
One is controllable, and flipable by the user, The others are
controlled by the CPU, and one uses 0-byte transparency. |
 |
MaxTile_Test2.asm
|
|
 |
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
The Tilemap contains the sample data. Each
tile is 2 bytes.
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. |
 |
Lesson
MaxTile4 - DirectDriver and Expanders
Lets take a look at the simplest Draw routine, the one which writes
straight to VRAM - intended for Double Buffered screens.
We'll also look at the 'expanders' which convert a 1 byte per tile
tilemap to a 2 byte one - This is used for expanding the player
sprite tilemaps into the cache for drawing, it also X and Y flips
them |
 |
V1_MaxTile_Expanders.asm
V1_MaxTile_DirectDriver.asm
|
|
 |
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. |
 |
Lesson
MaxTile5 - Cache Driver
The Cache driver calculates all the tile draws required, and stores
them in 3 caches. This allows the delays during drawing to be
reduced, meaning less flicker during redraw, it also means we can
time the draw of the caches to the raster beam. |
 |
V1_MaxTile_CacheDriver.asm
|
|
 |
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)
|
 |
| |
Buy my Assembly programming book on Amazon in Print or Kindle!



Available worldwide! Search 'ChibiAkumas' on your local Amazon website!
Click here for more info!
Buy my Assembly programming book on Amazon in Print or Kindle!



Available worldwide! Search 'ChibiAkumas' on your local Amazon website!
Click here for more info!
Buy my Assembly programming book on Amazon in Print or Kindle!



Available worldwide! Search 'ChibiAkumas' on your local Amazon website!
Click here for more info!
|