From Tileset to Playable Level: A Tiled Map Workflow That Survives Contact
A tile map is not an artefact. It is a chain of hand-offs. A tiled map workflow tileset to playable level runs tileset to grid, grid to layers, layers to collision, collision to engine, and each of those hand-offs has its own failure mode. In my experience the art is almost never what broke. The map broke at a boundary — a cell size that did not divide cleanly, a collision tile sitting on the ground layer, an origin set at the centre of a sprite whose feet were somewhere else. This is the pipeline as it actually runs, with the points where it usually fails marked.
The pieces map onto real tools. In Neta Studio, a world can be built from a prompt through the 3D game builder, or assembled by hand from a tileset using Agent Sprite Forge for the source art and Mini World for the small-scope grid export. The Cyclops' Island is a longer worked example of the same chain. None of that changes the hand-offs below; it only changes which tool performs them.
Choose a cell size that divides the sheet, or the grid lies
The first decision is cell size in pixels, and it is arithmetic, not taste. The tileset sheet has a width and a height. Your cell size must divide both exactly. A 512×512 sheet supports 16, 32, 64, 128, 256. It does not support 48. A 384×512 sheet supports 32 and 64 but not 128.
When the cell size does not divide the sheet, slicing tools do one of three things, and all three are wrong in different ways:
- They crop the remainder, silently dropping the last partial column or row.
- They pad the sheet to the next multiple, inserting transparent gutters that shift every subsequent index.
- They round the count, so the index you click and the index the engine receives disagree by one at the far edge.
The failure surfaces much later, as a sprite that renders the wrong neighbour or an animation that plays one frame short. You will blame the animation data. It was the cell size.
Pick the cell size before any art is drawn, and treat it as frozen. Retrofitting cell size onto finished art means resampling every tile, which reintroduces filtering artefacts on pixel art and breaks the pixel grid the art was designed against.
Sprite sheets often carry 1–2 px of padding or a 1 px margin between tiles to stop bilinear sampling bleeding colour across a tile boundary. If your sheet has a 1 px margin, the sheet dimensions are
(cell + 2) × count, notcell × count. Compute the divisor against the padded total, not the tile size, or the last tile is clipped.
The tileset also needs a stable index origin. Top-left, row-major, zero-based is the convention that most engines and most exporters assume. Document it in the tileset metadata, because an artist working top-left/origin-one and a coder reading bottom-left/origin-zero will produce a map that is correct and upside down.
Keep three layers: ground, detail, collision
The single largest cause of rework I have seen is a map with one layer doing three jobs. Separate them.
Layer | Contains | Rendered | Purpose |
|---|---|---|---|
Ground | Base terrain, one tile per cell, no gaps | Yes, bottom | Establishes walkable surface and backdrop |
Detail | Props, overlays, decorations, decals | Yes, above ground | Adds variation without changing terrain identity |
Collision | Invisible blocking and trigger tiles | No | Defines what the physics and navigation systems read |
The ground layer is a complete mosaic. Every cell has a tile. No transparent holes, because a hole means the renderer shows whatever is beneath — usually the clear colour — and later someone "fixes" it by patching the ground layer with a collision tile.
The detail layer is sparse and may hold several entries per cell where the format allows it. It never carries terrain identity. A grassy cell with a rock decal is still grassy; the walkability comes from ground plus collision, not from what the decoration looks like.
The collision layer is invisible at runtime. Its tiles are placeholders that mark blocking regions. They can be drawn as any visible tile in the editor — a red square is conventional — but they are stripped or skipped at render time.
Mixing them looks efficient at first. You place a wall on the ground layer because that's where you're painting, and the wall both renders and blocks. Then you need a wall with no collision (a fence you can see through), or collision with no visible tile (an invisible boundary at the map edge), and you have no way to express either. Unmixing three hundred cells costs more than keeping three layers from the start.
Once a collision tile is on the ground layer, deleting it removes the visual too, and the ground mosaic now has a hole. Every hole is a second bug: the renderer falls through to the clear colour, and the collision query returns unwalkable for a cell that should be grass.
Store collision apart from the visual layer
There are two ways to attach collision to a cell: per-tile flags on the tileset, or a separate collision layer.
Per-tile flags live with the tile definition. One flag, every instance of that tile blocks identically. This is cheap to author and cheap to store, and it is wrong the moment the same visual tile needs to block in one place and not another. A crate that is a wall in a corridor and a pushable object in a yard is one tile with two behaviours. Flags cannot express that without duplicating the tile.
A separate collision layer is more flexible and, in practice, cheaper. It stores one value per cell in a parallel grid — typically a small integer where 0 is walkable and non-zero names a collision class. It does not need the visual tileset at all; it can be a bitmask, a class ID, or a run-length encoded row. Its storage is proportional to the grid, not to the tile variety, and it survives art changes. You can repaint every tile on the ground layer and the collision layer is untouched.
The engineering argument: a per-tile flag couples two independent concerns into one record, so every change to one forces a review of the other. A collision layer keeps them orthogonal. Orthogonality is what makes a hand-off survivable, because the person editing art and the person tuning traversal do not contend for the same data.
Define collision classes explicitly and keep them few: walkable, blocked, water, trigger, hazard. Each engine consumes a mapping from class to behaviour. Adding a class is a data change; adding a per-tile flag is a schema change across the whole tileset.
Depth sorting on an isometric grid
On an orthographic top-down grid, draw order can follow layer order and be done. On an isometric grid it cannot, because a cell's screen position is a projection and two cells at different grid coordinates can overlap vertically.
The naive approach sorts by row index — y in grid space — and draws higher rows first. This works until two objects occupy the same row. Then the tie is unresolved, the sort falls back to insertion order, and the result depends on the order the exporter happened to serialise them. Move one prop in the editor, the serialisation order changes, and a crate that was correctly behind a barrel is now in front of it.
Three fixes, in increasing cost:
- Sort by row, then by column, with a stable tiebreak. Deterministic, cheap, still wrong for tall objects that span more than one row.
- Sort by the object's foot position in grid space. Use the bottom-centre of the sprite as its anchor, not the top-left. Two objects on the same row with different columns now sort by column consistently, and tall objects anchor where they visually sit.
- Sort by projected screen
yof the foot, thenx. This is the general fix. It handles objects of any size, including multi-cell props, as long as every object's origin is its foot.
The second fix is usually enough and costs one anchor convention. The third costs a comparison over the drawn set every frame, which for a few hundred sprites on a small map is negligible and for a few thousand is not — at which point you bucket by row and sort within buckets.
The tell that you have a tiebreak bug is a z-fighting pair that swaps when you reload. If the artefact is stable across reloads but you cannot explain the order, your sort is deterministic and wrong. If it changes on reload, your sort is non-deterministic and you have no tiebreak at all.
Set the anchor once and enforce it in the exporter. If half the props are anchored top-left and half at the foot, no sort function recovers the correct order.
Export formats, and what each one loses
Two exports cover most needs, and they lose different things.
A flat PNG is the composited image. Everything is baked: no grid, no layers, no collision, no object identity. It is ideal as a static backdrop for a 3D scene — you paint a distant wall or a skyline, export it, and apply it to a plane in Neta Studio's 3D game builder. What it loses: any ability to query a cell. You cannot ask "is (12, 7) walkable" of a PNG without re-deriving the grid, and the collision data is gone.
A TMX or JSON grid keeps the structure. TMX carries layer names, tileset references, per-cell indices, and can carry object layers and properties. JSON variants do the same in a shape most engines parse directly. What they lose: nothing structural, but they carry no pixels — the tileset is referenced by path, so the import fails if the path is wrong or the tileset was renamed. They also lose anything the exporter did not serialise; if collision was drawn as a layer but the exporter only writes visible layers, the collision is silently dropped and the map imports with every cell walkable.
Export | Keeps grid | Keeps layers | Keeps collision | Typical use |
|---|---|---|---|---|
Flat PNG | No | No | No | Backdrop plane, skybox art, distant scenery |
TMX | Yes | Yes | Only if exported as a layer | Engine import, editor round-trip |
JSON grid | Yes | Yes | Only if the schema includes it | Custom runtime, web engines |
The rule: the export format must be able to express every hand-off the runtime needs. If the runtime reads collision, the export must contain collision. A PNG cannot, so a PNG is never the runtime map — it is a layer within one.
The engine hand-off: three settings decide the import
The last hand-off is where maps that were perfect in the editor fail on import. Three settings cause most of it.
Cell size. The engine needs the same number the exporter used. If the engine defaults to 32 and the sheet is 48, every tile is scaled by 1.5 and the grid no longer aligns to the collision layer. The map renders, slightly wrong, and the collision is offset by fractions of a cell. Set cell size on import explicitly; do not accept the default.
Slice by grid. The engine must slice the sheet by the same grid the exporter assumed. Slicing by grid and slicing by alpha bounds produce different tile rectangles. Alpha slicing on a sheet with 1 px margins gives you the padded rect, not the tile — the tile arrives with a transparent border, and every placement is visually inset by a pixel. Slice by grid, with the margin declared.
Origin at the feet. The sprite's origin determines where it is drawn relative to its grid cell and how it sorts. An origin at the sprite centre places the object half a cell too high and breaks the isometric sort from the previous section. Set the origin to the bottom-centre of the sprite, per sprite, before placement. If the engine supports a per-tileset origin convention, set it there so it applies to every tile.
These three settings are the same three that a future map will need. Encode them in the import profile and reuse it; the point of the hand-off is that it is repeatable, not that it is fast once.
Pre-flight checklist before export
Run this over any map before it leaves the editor. Every item maps to a hand-off above.
- Sheet dimensions divide by cell size. Both width and height, exactly, including margins. No remainder.
- Index origin documented. Top-left, row-major, zero-based, written down where the importer will read it.
- Ground layer is a complete mosaic. No transparent holes. Every cell has a tile.
- Detail layer carries no terrain identity. Decoration only; walkability comes from ground plus collision.
- Collision is a separate layer. Not a per-tile flag, not painted onto ground.
- Collision classes are defined and few. Walkable, blocked, water, trigger, hazard — mapped to engine behaviours.
- Every prop's anchor is its foot. Bottom-centre, per sprite, consistent across the set.
- Sort is deterministic with a tiebreak. Reload twice; the order must not change.
- Export contains the collision layer. If the runtime reads collision, the export must carry it.
- Tileset path in the export resolves. No renames between export and import.
- Engine cell size matches the exporter. No default accepted.
- Engine slices by grid. Not by alpha bounds, margins declared.
- Engine origin is at the feet. Applied per tileset, not per instance.
Thirteen checks, all mechanical. A map that passes all thirteen will import and play. A map that fails one will import and be subtly wrong — which is worse, because subtle wrongness survives review and surfaces in a playtest two weeks later.
The through-line is that a tiled map workflow tileset to playable level is a sequence of contracts between tools. Each contract has a small number of fields that must agree on both sides: cell size, index origin, layer roles, collision representation, anchor, export schema. Break one and the map still looks fine in the editor. It breaks in the engine, or in sorting, or in a test someone files as "collision is off by a bit." Check the contracts at the boundary, and the art gets to be the only thing you worry about.
In Neta Studio the same contracts apply whether the world is prompted into the 3D game builder, authored by hand with Agent Sprite Forge, or exported small-scope through Mini World. The tools change. The hand-offs do not.
Keep going
Build the world it goes in
A tileset is a material with nowhere to stand. Every world on the 3D game builder was made from one written description and published as a page you can play — including The Cyclops' Island and Mini World.
