Quick answer: Tile IDs are zero-indexed across the tileset image, left-to-right then top-to-bottom. SetTileAt takes pixel coordinates, not column/row. Multiply column and row by tile size. Verify IDs by hovering in the Tilemap bar.
Here is how to fix Construct 3 Tilemap drawing wrong tiles. You set up a level generator that calls SetTileAt(col, row, tileID) in a loop. The result looks like a jumbled mess — walls where floors should be, grass on ceilings. Either the coordinates are wrong (pixels vs grid) or the tile IDs shifted after a tileset edit.
The Symptom
Tiles placed via events or code appear at wrong positions or show wrong tile art:
- All tiles cluster in the top-left corner (coordinates not pixel-scaled)
- Correct positions but wrong tile images (ID numbering mismatch)
- Tiles appear shifted by one row or column (off-by-one in ID calculation)
- Collision works for some tiles but not others (collision polygon on wrong ID)
What Causes This
Pixel coordinates, not grid indices. SetTileAt(x, y, tile) takes x and y in pixels. Passing column and row numbers directly (e.g. 5, 3) places tiles at pixel position (5, 3) — barely visible in the top-left. Multiply by tile width/height for correct placement.
Tile ID shift after tileset edit. IDs are assigned left-to-right, top-to-bottom across the tileset image. Adding a row of tiles at the top shifts every ID below. A wall that was ID 12 becomes ID 20 if 8 tiles were inserted above it.
Zero-indexed IDs. The first tile is ID 0, not 1. Forgetting this places everything one tile off. Tile -1 means “erase tile” (empty).
Tileset image resized. Changing the tileset image dimensions without matching tile size in Tilemap properties produces ID misalignment. A 256x256 image with 32px tiles = 64 tiles. Resize to 512x256 and IDs remap.
The Fix
Step 1: Use pixel coordinates correctly.
// Correct: multiply column and row by tile size
var tileSize = 32;
Repeat 20 times (loopindex "col"):
Repeat 15 times (loopindex "row"):
Tilemap: SetTileAt(loopindex("col") * tileSize,
loopindex("row") * tileSize,
levelData[col][row])
// Wrong: raw column/row without scaling
// Tilemap: SetTileAt(col, row, tileID) // places at pixel 0-19
For a 32x32 tile grid, column 5 = pixel 160, row 3 = pixel 96. SetTileAt(160, 96, tileID).
Step 2: Verify tile IDs in the Tilemap bar. Open the Tilemap object. In the Tilemap bar at the bottom of the editor, hover over each tile. The status bar shows the tile ID. Write these down or reference them in your level data.
After any tileset image edit, re-verify IDs. They change whenever the image layout changes.
Step 3: Use constants or a lookup for tile IDs. Instead of hardcoded numbers, define constants in an event sheet or global variables:
On start of layout:
Set TILE_WALL = 0
Set TILE_FLOOR = 1
Set TILE_GRASS = 8
Set TILE_WATER = 9
Set TILE_EMPTY = -1
// Use names instead of raw numbers
Tilemap: SetTileAt(x, y, TILE_WALL)
When tileset IDs shift, update the constants once rather than hunting through every SetTileAt call.
Step 4: Fix collision polygons per tile. Select the Tilemap object. In the Tilemap bar, select the tile by ID. Click the collision polygon editor (wrench icon). Draw or auto-generate the collision shape for that tile.
Common mistake: setting collision on tile 0 (wall) but not tile 8 (different wall variant). Every solid tile needs its own collision polygon. Empty/passable tiles should have no collision.
Reading Tiles
Use TileAt(x, y) to read what tile is at a pixel position. Returns the tile ID or -1 for empty. Useful for debugging:
On Mouse Click:
var tx = Mouse.X
var ty = Mouse.Y
var id = Tilemap.TileAt(tx, ty)
Text: Set text to "Tile at (" & tx & "," & ty & ") = " & id
Click anywhere on the tilemap during preview to see which tile ID is at that spot. Mismatch between expected and actual reveals the bug.
Flipped and Rotated Tiles
SetTileAt has optional parameters for flipping and rotation. Forgetting these when your level data includes flip flags produces wrong orientations. Check if your map format encodes flip/rotation bits and pass them through.
Performance
SetTileAt is O(1) per call but calling it thousands of times in one tick can hitch. For large procedural maps, spread generation across multiple frames using a counter and “Wait 0 seconds” between chunks.
“Tilemaps are a grid of IDs at pixel positions. Get the coordinates right, get the IDs right, and the map draws itself.”
Related Issues
For collision issues, see Construct 3 Collision Not Detecting. For pathfinding on tilemaps, Construct 3 Pathfinding Not Finding Route.
Pixel coordinates, not grid indices. Zero-indexed IDs. Constants for tile names. Three rules.