TileMap handling
Units: TMXLoad TMXDraw
The DOS Game Engine can load .tmx files (Tiled Map Editor XML format) with basic functionality:
TMXLOAD.PAS - Loading TMX files, parsing XML, managing tilesets and layers
TMXDRAW.PAS - Rendering tilemap layers to framebuffers
🎯 Layer Merging System
The loader can read multiple <layer> tags from a TMX file, but merges them into exactly 2 layers in the final TTileMap structure:
Front Layer (index 0): All layers before the first
<objectgroup>tagBack Layer (index 1): All layers after the first
<objectgroup>tag
Merging behavior:
When multiple source layers overlap at the same tile position, the higher index has priority (later layers draw on top)
If no
<objectgroup>tag exists, all layers merge into the front layer onlyEmpty tiles (ID = 0) are treated as transparent during merging
This allows complex multi-layer TMX files created in Tiled to be simplified for efficient DOS rendering.
🧱 Structures
type TTileSet = record
FirstGID Word; { The first tile ID in the set }
TileWidth: Byte; { Width of one tile }
TileHeight: Byte; { Height of one tile }
Columns: Byte; { Tile columns count }
Image: TImage; { The image that holds the graphics of the tiles. }
end;
const TileMap_MaxTileSets = 4;
const TileMapLayer_Front = 0;
const TileMapLayer_Back = 1;
type TTileMap = record
Width: Word; { Width of the tile map (in tiles) }
Height: Word; { Height of the tile map (in tiles) }
TileSetCount: Byte; { How many tilesets are present }
TileSets: array[0..TileMap_MaxTileSets] of TTileset;
Layers: array[0..1] of PWord; { Back and front layers }
{ Every word represents a TileID }
BlocksLayer: PByteArray; { Collision/blocks layer (nil if not present) }
{ Each byte is a block type (0 = passable) }
BlocksTilesetFirstGID: Word; { FirstGID of tileset with 'blocks' property (0 if none) }
end;
TTileSet represents a tileset image referenced by the map. Each tileset contains:
FirstGID: The starting tile ID for this tileset (e.g., if FirstGID=1, tile ID 1 is the first tile)TileWidth/TileHeight: Dimensions of individual tiles in pixelsColumns: Number of tile columns in the tileset image (used to calculate tile positions)Image: TheTImagestructure holding the actual tileset graphics (see VGA.PAS)
TTileMap stores the final merged map data with exactly 2 layers. Each layer is a dynamically allocated array of Word values (tile IDs), with dimensions Width × Height. Memory layout is row-major: Layers[0]^[y * Width + x] accesses tile at position (x, y).
Every / character in the paths in the TMX files should be replaced with \ because of the DOS paths.
🔥 Functions
function GetLoadTileMapError: String
Returns the last error on TileMap loading, gets it from a unit scoped variable: LoadTileMapError: String.
function LoadTileMap(const FilePath: String; var TileMap: TTileMap;
ObjectGroupProc: TObjectGroupProc): Boolean
Clears the LoadTileMapError variable, then loads the content of the .tmx file from the FilePath to an XMLNode (see MINIXML.PAS). On any fail sets the LoadTileMapError and returns False.
The ObjectGroupProc parameter is a callback procedure that will be invoked when an <objectgroup> tag is encountered. Pass nil if you don’t need to process objectgroups. The callback receives a PXMLNode pointer to the objectgroup node for custom processing.
Searches for the <map> tag, if not presents the load fails. Sets the TileMap.Width and TileMap.Height via the <map> tag’s width and height attributes, if any of these missing: the load fails.
Searches for the <tileset> tags in the <map> node and calls the LoadTileSet for each. If a tileset load fails, returns with False. The FolderPath parameter will be the folder from the FilePath. At least one tileset must be present - if no tilesets are found, the load fails with error “No tilesets found”.
Searches for the <layer> tags in the <map> node and calls the LoadTileMapLayer for each.
Layer separation logic:
Scans for the first
<objectgroup>tag to determine the split pointAll layers before the objectgroup merge into
TileMapLayer_Front(index 0)All layers after the objectgroup merge into
TileMapLayer_Back(index 1)If no objectgroup exists, all layers merge into front only
Merging priority example:
Given this layer order in a TMX file:
<layer id="0"><data encoding="csv">...</data></layer> <!-- Front source #0 (lowest priority) -->
<layer id="1"><data encoding="csv">...</data></layer> <!-- Front source #1 (highest priority) -->
<objectgroup id="2">...</objectgroup> <!-- Split point -->
<layer id="3"><data encoding="csv">...</data></layer> <!-- Back source #0 (lowest priority) -->
<layer id="4"><data encoding="csv">...</data></layer> <!-- Back source #1 (highest priority) -->
At tile position (x, y):
If Front #1 has tile ID > 0, use it (highest priority - rendered last)
Else if Front #0 has tile ID > 0, use it
Else position is empty (ID = 0)
The same priority logic applies to back layers. Higher index = higher priority during merging (later layers overwrite earlier layers).
function LoadTileSet(const FolderPath: String; const XMLNode: PXMLNode; var TileSet: TTileSet): Boolean
Loads a <tileset> XML node and populates the TileSet record. Extracts firstgid, tilewidth, tileheight, and columns attributes from the tileset tag. If any of these attributes missing sets the error message and returns False. Loads the tileset image from FolderPath using the <image source="..."> child node, replaces the file extension with pcx, for example if the source is tileset.png, the FolderPath is ../DATA the the full path of the image will be ..\DATA\tileset.pcx. Loaded by the PCX unit’s LoadPCX function. Returns False and sets LoadTileMapError if any required attribute or the image is missing.
procedure LoadTileMapLayer(const XMLNode: PXMLNode; var TileMap: TTileMap; const Layer: Byte)
Processes a <layer> tag, only with <data encoding="csv"> tags (Base64 and compressed formats are unsupported).
🎨 TMXDRAW.PAS - Rendering Functions
procedure DrawTileMapLayer(
var TileMap: TTileMap;
Layer: Byte; X: Word; Y: Word; Width: Word; Height: Word;
FrameBuffer: PFrameBuffer
)
Renders the specified layer to the framebuffer. Iterates through the specific tiles in the layer (based on X, Y, Width, Height parameters), calculates the tileset based on the FirstGID then the source tile coordinates (TRect), then blits each tile to the destination position and framebuffer with the PutImageRect (see VGA.PAS).
Unit: TMXDRAW.PAS (requires VGA.PAS and TMXLOAD.PAS)
Parameters:
Layer: UseTileMapLayer_Back(1) orTileMapLayer_Front(0)X: The start tile X positionY: The start tile Y positionWidth: The width for the drawing loop (in tiles)Height: The height for the drawing loop (in tiles)FrameBuffer: Destination buffer (320x200)
Rendering behavior:
Tile ID 0 is treated as empty/transparent (no draw)
Tiles are clipped at screen boundaries automatically
Only renders that is visible
Note on the FirstGID: needs a separate function that has a fast look up algorithm to decide which TileSet should be used, called before every tile render.
procedure FreeTileMap(var TileMap: TTileMap)
Frees dynamically allocated layer memory. Calls FreeMem on both layer arrays if they are not nil, then sets pointers to nil. Must be called before program exit to prevent memory leaks. Frees tileset images and BlocksLayer memory too.
function IsBlockType(const TileMap: TTileMap; X, Y: Word; BlockType: Byte): Boolean
Checks if the tile at position (X, Y) has the specified block type. Returns False if BlocksLayer is nil, coordinates are out of bounds, or block type doesn’t match. Returns True if the tile at (X, Y) has exactly the specified BlockType value.
Parameters:
X, Y: Tile coordinates (not pixels)BlockType: The block type to check (e.g., 1 for solid walls, 2 for platform tops)
Example:
{ Check for solid wall collision }
if IsBlockType(Map, PlayerTileX, PlayerTileY, 1) then
PlayerX := OldPlayerX; { Revert movement }
📝 Usage Example
program TileMapDemo;
uses VGA, Image, TMXLoad, TMXDraw;
var
Map: TTileMap;
TileSet: TTileSet;
Buffer: PFrameBuffer;
begin
{ Initialize graphics }
InitVGA;
Buffer := CreateFrameBuffer;
{ Load tilemap from TMX file }
if LoadTileMap('DATA\LEVEL1.TMX', Map, nil) then
begin
{ Render back layer (background scenery) }
DrawTileMapLayer(Map, TileMapLayer_Back, 0, 0, 20, 11, Buffer);
{ TODO: Draw game sprites here }
{ Render front layer (foreground objects) }
DrawTileMapLayer(Map, TileMapLayer_Front, 0, 0, 20, 11, Buffer);
{ Display result }
RenderFrameBuffer(Buffer);
ReadLn;
{ Cleanup }
FreeTileMap(Map);
end
else
WriteLn('Error: ', GetLoadTileMapError);
FreeFrameBuffer(Buffer);
DoneVGA;
end.
🗺️ Tile ID Mapping
Each tile ID in the layer data maps to a specific position in the tileset image:
Formula:
TileIndex = TileID - TileSet.FirstGid
Row = TileIndex div TileSet.Columns
Col = TileIndex mod TileSet.Columns
SourceX = Col * TileSet.TileWidth
SourceY = Row * TileSet.TileHeight
Example:
TileSet:
FirstGid=1,Columns=8,TileWidth=16,TileHeight=16Map contains Tile ID
15Calculation:
TileIndex = 15 - 1 = 14Row = 14 div 8 = 1Col = 14 mod 8 = 6Source position:
(96, 16)in tileset image
🎨 TMX Format Support
Supported features:
CSV-encoded tile data (
<data encoding="csv">)Multiple tilesets per map
External tileset files (TSX format)
Special “Blocks” and “Objects” tilesets
Multiple layers (merged to 2 final layers)
Objectgroup layer separator
Tile dimensions and map dimensions
Unsupported features:
Base64 encoding
Compression (gzip, zlib)
Infinite maps
Tile animations
Tile properties/custom data
Isometric/hexagonal orientations (orthogonal only)
TMX format info: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/
🔗 External Tileset Support (TSX Files)
Feature: Tilesets can be defined in external .tsx files and referenced from the TMX file.
How it works:
When loading a TMX file, the loader first processes all <tileset> tags in the <map> root node:
Detection: If a
<tileset>tag has asourceattribute, it’s an external tileset reference<tileset firstgid="1" source="tilesets/terrain.tsx"/>
Loading Process:
The
sourceattribute contains a path relative to the TMX file locationLoad the TSX file content into an XMLNode (called
ExternalTilesetNode)The root element in the TSX file is a
<tileset>tag with full tileset definitionCopy all attributes from the referencing
<tileset>tag to the loadedExternalTilesetNode(except thesourceattribute itself)Replace the original tileset reference node with the loaded external tileset node
Path Conversion:
All
/characters in thesourcepath are replaced with\for DOS compatibilityExample:
source="tilesets/terrain.tsx"→TILESETS\TERRAIN.TSX
Merged Processing:
After external tilesets are loaded and merged, the loading process continues normally
Each tileset (whether originally internal or external) is processed identically
Example TSX file (TERRAIN.TSX):
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.10" name="Terrain" tilewidth="16" tileheight="16"
tilecount="64" columns="8">
<image source="terrain.png" width="128" height="128"/>
</tileset>
Example TMX reference:
<map version="1.10" width="20" height="15" tilewidth="16" tileheight="16">
<!-- External tileset reference -->
<tileset firstgid="1" source="TILESETS\TERRAIN.TSX"/>
<!-- Layers use the tiles normally -->
<layer id="1" name="Ground" width="20" height="15">
<data encoding="csv">1,2,3,4,...</data>
</layer>
</map>
Benefits:
Reuse tilesets across multiple maps
Smaller TMX files
Easier tileset management and updates
🧱 Special Tilesets: Blocks and Objects
The engine recognizes two special tileset types by name:
1. “Blocks” Tileset (Collision Detection)
Type: Simple image tileset
Purpose: Contains block tiles for the collision/blocks layer
Name: Must be exactly
"Blocks"(case-sensitive)Format: Standard tileset with a single tileset image
Usage: Used by the blocks layer for tile-based collision detection
Example:
<tileset firstgid="241" name="Blocks" tilewidth="16" tileheight="16"
tilecount="16" columns="4">
<image source="blocks.png" width="64" height="64"/>
</tileset>
2. “Objects” Tileset (Entity Sprites)
Type: Image collection tileset
Purpose: Contains object tiles for game entities (player, enemies, items, etc.)
Name: Must be exactly
"Objects"(case-sensitive)Format: Collection-based tileset where each tile can have different dimensions
Usage: Entity system references tiles from this tileset for sprite rendering
Example:
<tileset firstgid="300" name="Objects" tilewidth="16" tileheight="16">
<tile id="0">
<image source="player.png" width="16" height="24"/>
</tile>
<tile id="1">
<image source="enemy.png" width="16" height="16"/>
</tile>
<tile id="2">
<image source="coin.png" width="8" height="8"/>
</tile>
</tileset>
Both tilesets can be external (TSX files):
<!-- External Blocks tileset -->
<tileset firstgid="241" source="BLOCKS.TSX"/>
<!-- External Objects tileset -->
<tileset firstgid="300" source="OBJECTS.TSX"/>
🧱 Blocks Layer (Collision Detection)
Feature: Tile-based collision detection using custom layer properties.
How it works:
Layers with
<property name="blocks">are treated as collision data (not visual tiles)The special “Blocks” tileset (see above) stores collision tile definitions
Block data is stored separately in
TTileMap.BlocksLayer(PByteArray)Each byte represents a block type (0 = passable, 1+ = different collision types)
TMX Configuration:
Create a layer with the blocks property:
<layer id="5" name="Collision" width="20" height="15">
<properties>
<property name="blocks" type="bool" value="true"/>
</properties>
<data encoding="csv">
0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,
0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,
...
</data>
</layer>
Block Type Conversion:
Tile IDs in the blocks layer are automatically converted to block types
Formula:
BlockType = TileID - BlocksTilesetFirstGID + 1Example: If “Blocks” tileset FirstGID=241:
Tile ID 241 → Block type 1 (solid wall)
Tile ID 242 → Block type 2 (platform top)
Tile ID 0 → Passable (empty)
Usage in Code:
{ Check if tile is solid wall (type 1) }
if IsBlockType(TileMap, PlayerX, PlayerY, 1) then
WriteLn('Hit a wall!');
{ Check if tile is platform top (type 2) }
if IsBlockType(TileMap, PlayerX, PlayerY, 2) then
WriteLn('Standing on platform!');
{ Direct access to block data }
Index := Y * TileMap.Width + X;
BlockType := TileMap.BlocksLayer^[Index];
Memory Usage:
1 byte per tile (Width × Height bytes)
Example: 64×64 map = 4,096 bytes (4 KB)
Automatically freed by
FreeTileMap
Visualization: See TMXTEST.PAS for an example of rendering block overlay with text labels (press ‘B’ to toggle visibility).
Status: ✅ Fully implemented and working.
📐 Coordinate Systems
Tile coordinates: Map positions measured in tiles (0 to Width-1, 0 to Height-1)
Pixel coordinates: Screen positions measured in pixels (0 to 319, 0 to 199 for Mode 13h)
Conversion:
PixelX := TileX * TileSet.TileWidth;
PixelY := TileY * TileSet.TileHeight;
💾 Memory Management
Memory usage per map:
Bytes = Width × Height × 2 layers × 2 bytes per tile
Example: 64×64 tile map = 64 × 64 × 2 × 2 = 16,384 bytes (16 KB)
DOS constraints:
The TMX file maximum size is 64KB (see MINIXML.PAS)
Tileset images also consume memory (see PCX.PAS, VGA.PAS)
Best practices:
Always call
FreeTileMapbefore exitFree unused tilesets with
FreeTileSet
⚡ Performance Considerations
Rendering speed:
Drawing a full 20×12 screen of 16×16 tiles = 240 tile blits per frame
At 60 FPS: 14,400 tile copies per second
Use VSync (
WaitForVSync) to prevent tearing
Optimization tips:
Pre-calculate visible tile range based on screen size
Only redraw changed tiles (dirty rectangle tracking)
Cache tileset row offsets to avoid multiplication in inner loops
Consider pre-rendering static backgrounds to a single buffer
🔧 Troubleshooting
“No
TMX file is corrupted or not valid XML
Wrong file format (not a Tiled map file)
“Missing width/height attributes”
Map tag must have both
width="X"andheight="Y"attributesCheck TMX file structure in text editor
“No tilesets found”
TMX file must contain at least one
<tileset>tagCheck if tileset definition exists in the map file
Verify
<tileset>tags are direct children of<map>
“Failed to load tileset”
Tileset image file not found (check relative path in
<image source="...">)Missing required tileset attributes (
firstgid,tilewidth,tileheight,columns)
“Unsupported encoding”
TMX must use CSV encoding
In Tiled: Map → Map Properties → Tile Layer Format → CSV
“Out of memory”
Map too large for DOS conventional memory
Free unused resources before loading
Consider using XMS for large maps
Tiles render incorrectly:
Verify
TileSet.Columnsmatches actual tileset image widthCheck
FirstGidmatches tileset configurationEnsure tile IDs in map data are valid for the tileset
📄 Example TMX File
Minimal working TMX file demonstrating 2-layer merging:
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal"
renderorder="right-down" width="20" height="15"
tilewidth="16" tileheight="16" infinite="0">
<tileset firstgid="1" name="Terrain" tilewidth="16" tileheight="16"
tilecount="64" columns="8">
<image source="TILES.PNG" width="128" height="128"/>
</tileset>
<!-- Front layer: Decorations (lower priority - rendered first) -->
<layer id="1" name="Decorations" width="20" height="15">
<data encoding="csv">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
...
</data>
</layer>
<!-- Front layer: Trees (higher priority - rendered last, overwrites decorations) -->
<layer id="2" name="Trees" width="20" height="15">
<data encoding="csv">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,9,10,0,0,0,0,0,9,10,0,0,0,0,0,0,
0,0,0,0,0,17,18,0,0,0,0,0,17,18,0,0,0,0,0,0,
...
</data>
</layer>
<!-- Separator: Everything after this goes to back layer -->
<objectgroup id="3" name="Objects"/>
<!-- Back layer: Ground tiles (no merging - single layer after objectgroup) -->
<layer id="4" name="Ground" width="20" height="15">
<data encoding="csv">
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,
1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,
...
</data>
</layer>
</map>
Result: “Decorations” and “Trees” merge into front layer (Trees have priority because higher index), “Ground” becomes back layer.