Tiled maps
Load Tiled (.tmj) tilemaps as assets through the official @codexo/exojs-tiled extension.
Tiled maps
Tiled is a popular open-source map editor. The official @codexo/exojs-tiled extension adds a TiledMap asset type that loads Tiled’s JSON map format (.tmj) through the normal Loader pipeline — fetching the map, resolving its tileset image references, and returning a TiledMap you can read for layer and tile data.
Note:
TiledMapships as an official ExoJS extension package, separate from the core. It is built on@codexo/exojs-tilemap, and both that package and@codexo/exojsare peer dependencies — install all three explicitly, on the same version:npm install @codexo/exojs @codexo/exojs-tilemap @codexo/exojs-tiled
TiledMap is data, not something you can draw
Loading a map gives you layer grids and tileset textures — it is not a Drawable, so adding it to the scene puts nothing on screen. You build the rendering yourself (batched Sprites or a Mesh) from the data it exposes.
Like all extensions, the core ships nothing Tiled-specific — an Application only understands .tmj files once you activate the extension.
Activation
Pass tiledExtension to ApplicationOptions.extensions. The extension is bound to exactly this Application:
import { Application } from '@codexo/exojs';
import { tiledExtension } from '@codexo/exojs-tiled';
const app = new Application({ extensions: [tiledExtension] });
The package root (@codexo/exojs-tiled) is side-effect-free: importing it registers nothing globally. You decide which Application gets the extension.
Core-only
An Application constructed with an explicit empty list takes no extensions — not even globally registered ones — and will not recognise .tmj files:
import { Application } from '@codexo/exojs';
const app = new Application({ extensions: [] }); // core only; no TiledMap
Loading a .tmj here throws No renderer/handler style errors directing you to import the extension. This is the same add-only model the Particles extension uses.
Loading a map
Once the extension is active, load a .tmj map with an explicit descriptor or by path. Both resolve to the same handler.
Only orthogonal Tiled JSON loads — the rest throw by design
XML (.tmx/.tsx), isometric or hexagonal orientations, collection-of-images tilesets, and zstd compression each raise a clear error rather than loading. Export an orthogonal JSON map (.tmj) with CSV, gzip, or zlib data. Infinite (chunked) maps are supported: toTileMap() converts each chunked layer to an unbounded runtime layer whose data streams in on demand via getChunkSource() — see the Infinite maps chapter.
import { Asset } from '@codexo/exojs';
// Explicit type
const map = await loader.load(Asset.type('tiledSource', 'levels/forest.tmj'));
// By path — the `.tmj` extension is registered, so the type is inferred
const sameMap = await loader.load('levels/forest.tmj');
The handler fetches the JSON (cache-routed), validates it, then loads every tileset image it references as a Texture through the same loader, returning a fully-resolved TiledMap.
Reading the map
TiledMap exposes the parsed metadata, layers, and the resolved tileset textures:
map.width; // map width in tiles
map.height; // map height in tiles
map.tileWidth; // tile width in pixels
map.tileHeight; // tile height in pixels
map.layers; // readonly tile/object layers
map.tilesets; // readonly tileset definitions
map.tilesets[0].texture; // the Texture resolved for an image tileset (undefined for a collection)
map.data; // the raw parsed TiledMapData
TiledMap is a plain data asset, not a Drawable. It gives you the layer grids and tileset textures; you decide how to render them (for example, by building a Mesh or batching Sprites per visible tile). A built-in tilemap renderer is out of this extension’s initial scope.
Custom properties
Tiled’s custom properties (on maps, layers, tiles, and objects) survive the trip through map.toTileMap(). Every runtime ObjectLayer, TileMapObject, and TileDefinition carries a properties bag — a flat, frozen Readonly<Record<string, TilePropertyValue>> built from the map’s Tiled property list. Plain-scalar Tiled types (string, int, float, bool, color, file) come through as their equivalent JS value:
const tileMap = map.toTileMap();
const [spawnLayer] = tileMap.objectLayers;
const spawn = spawnLayer.objects.find(object => object.name === 'PlayerSpawn');
spawn.properties.maxHealth; // number, e.g. 100
spawn.properties.label; // string
spawn.properties.locked; // boolean
Two Tiled property types aren’t plain scalars and convert to structured values instead:
-
object-typed properties — a reference to another Tiled object by numeric id — convert to aTilePropertyObjectRef:{ kind: 'objectRef', id }. Resolve it by matchingidagainst another object’sidin the same map:import { TilePropertyKind } from '@codexo/exojs-tilemap'; const door = spawnLayer.objects.find(object => object.name === 'Door'); const target = door.properties.linkedSwitch; if (target?.kind === TilePropertyKind.ObjectRef) { const linkedSwitch = spawnLayer.objects.find(object => object.id === target.id); } -
class-typed properties — Tiled’s nested custom-property groups — recursively convert into their ownproperties-shaped bag instead of being dropped. Nesting can be arbitrarily deep, and each nested bag reads the same way as the top-level one:const stats = spawn.properties.stats; // a nested TileProperties bag stats.health; // number stats.resistances.fire; // further nesting reads the same way
Before this conversion existed, object- and class-typed properties were indistinguishable from a plain number or silently dropped; both are now tagged (TilePropertyObjectRef) or preserved (nested TileProperties) so you can tell them apart from real scalars at runtime.
Tile objects and the anchor convention
An object placed with Tiled’s insert tile tool converts to a TileObject — kind is 'tile' and tile carries the resolved ResolvedTile (tileset, local tile id, flip flags):
import { ObjectKind } from '@codexo/exojs-tilemap';
const tileMap = map.toTileMap();
const [propLayer] = tileMap.objectLayers;
const [chest] = propLayer.byKind(ObjectKind.Tile);
chest.tile.tileset; // the runtime TileSet the tile came from
chest.tile.localTileId; // index within that tileset
chest.tile.transform; // { flipX, flipY, diagonal }
Tiled does not store a tile object’s position the way it stores every other object’s. A rectangle, ellipse, point, polygon, or text object records the top-left corner of its bounding box; a tile object records its alignment anchor instead — by default the bottom-left corner in an orthogonal map and the bottom-centre in an isometric one. A tileset can override that with its objectalignment property (topleft, top, topright, left, center, right, bottomleft, bottom, bottomright).
toTileMap() resolves that alignment and converts the anchor to the top-left corner, so x/y mean the same thing on every TileMapObject kind:
// A 16×24 tile object stored at (32, 48) in a default orthogonal map
// (bottom-left anchor) converts to a corner at (32, 24).
chest.x; // 32
chest.y; // 24
Rotation still pivots about the Tiled anchor
Tiled rotates a tile object’s image about its alignment anchor, not about the bounding box corner. A tile object with a non-zero rotation therefore pivots about the anchor even though x/y now report the corner — recover the anchor with getTiledObjectAnchorOffset if you need the exact pivot.
The two helpers behind this are exported for when you work with the parsed source model directly: resolveTiledObjectAlignment(tileset.objectAlignment, map.orientation) applies Tiled’s orientation-dependent default, and getTiledObjectAnchorOffset(alignment, width, height) returns the anchor’s offset from the corner.
Text objects
An object drawn with Tiled’s text tool converts to a TextObject — a TileMapObject whose kind is 'text' and whose text field carries the styled content:
import { ObjectKind } from '@codexo/exojs-tilemap';
const tileMap = map.toTileMap();
const [captionLayer] = tileMap.objectLayers;
const [caption] = captionLayer.byKind(ObjectKind.Text);
caption.text.text; // the text content
caption.text.color; // 0xRRGGBB, or null for the default colour
caption.text.fontFamily; // e.g. 'sans-serif'
caption.text.pixelSize; // font size in px
caption.text.bold;
caption.text.italic;
caption.text.wrap; // wrap within the object's [width, height] bounds
caption.text.halign; // 'left' | 'center' | 'right' | 'justify'
caption.text.valign; // 'top' | 'center' | 'bottom'
caption.x, caption.y, caption.width, and caption.height (inherited from the common TileMapObject fields) give the text box’s placement and bounds; ExoJS does not lay out or draw the text for you — pair caption.text with a Text node or your own renderer.
Image layers
A Tiled image layer converts to an ImageLayer — a data-only layer holding a single resolved image, alongside the same offset/parallax/tint/opacity/properties fields tile and object layers carry:
const tileMap = map.toTileMap();
const [background] = tileMap.imageLayers;
background.texture; // the resolved Texture (Loader-owned), or null if the image failed to load
background.repeatX; // tiles the image horizontally when true
background.repeatY; // tiles the image vertically when true
background.parallaxX; // horizontal parallax factor
background.parallaxY; // vertical parallax factor
background.properties.fogDensity; // custom properties — same TileProperties bag as other layers
Like ObjectLayer, ImageLayer is not a Drawable — it exposes the resolved image and its placement so a renderer or scene node can draw it; there is no built-in image-layer renderer.
Per-tile collision shapes
Shapes drawn in Tiled’s tile collision editor (the objectgroup on an individual tile within a tileset) survive the trip through toTileMap() as TileDefinition.collision — a list of TileMapObjects in tile-local pixel space (origin at the top-left of the cell):
const tileMap = map.toTileMap();
const [ground] = tileMap.layers;
const tile = ground.getTileAt(3, 5);
const definition = tile?.tileset.getTileDefinition(tile.localTileId);
for (const shape of definition?.collision ?? []) {
shape.kind; // 'rectangle' | 'ellipse' | 'polygon' | 'polyline' | 'point'
shape.x; // tile-local pixel space
shape.y;
shape.width;
shape.height;
}
collision is only present on definitions for tiles that actually declare shapes in Tiled; getTileDefinition returns undefined for a tile with no per-tile metadata at all. Combine the shape’s local coordinates with the cell’s world position (tx * tileWidth, ty * tileHeight) to place it in map space.
buildTileCollisionGeometry(layer) does that placement for a whole layer, applying each tile’s flip/rotation transform plus the tileset and layer offsets and merging adjacent whole-cell boxes into as few rectangles as it can. It returns plain geometry — this package has no physics dependency.
To go straight to colliders, @codexo/exojs-tilemap-physics builds and streams static bodies from that geometry:
import { PhysicsWorld } from '@codexo/exojs-physics';
import type { TileLayer } from '@codexo/exojs-tilemap';
import { TileColliderStreamer } from '@codexo/exojs-tilemap-physics';
declare const ground: TileLayer;
const world = new PhysicsWorld({ gravity: { x: 0, y: 1600 } });
const colliders = new TileColliderStreamer(world, ground);
// Tick this from your update loop.
colliders.sync();
It owns one static body per resident chunk and keeps them in step with the layer, so a streamed map gains and loses collision along with its chunks. See Physics basics for the mapping table and the two region modes.
Base64 and compressed tile data
Tiled can write a tile layer’s data as a plain CSV array, or as a base64 string that’s optionally gzip- or zlib-compressed. @codexo/exojs-tiled decodes both transparently during the async load phase — by the time you read map.layers, every layer’s data is a plain GID array regardless of which encoding the map was exported with. There’s no user-facing API for this step; it just works.
The one exception is zstd: Tiled’s zstd tile-layer compression has no native decoder in this extension and throws TiledFormatError. Re-export the map with gzip, zlib, or uncompressed CSV data instead.
Wang and autotile sets
Wang sets — Tiled’s terrain/auto-tile definitions — parse onto TiledTileset.wangSets as raw TiledWangSetData. Convert one with tiledWangSetToWangSet into a runtime WangSet, then apply it to a layer with autoTile (a full-layer pass) or refreshCell (an incremental, single-cell update) from @codexo/exojs-tilemap:
const tileMap = map.toTileMap();
const [ground] = tileMap.layers;
// wangSets live on the parsed TiledTileset, keyed by the runtime tileset's
// index within `layer.tilesets` (0 here - the layer's only tileset).
const [wangSetData] = map.tilesets[0].wangSets;
const wangSet = tiledWangSetToWangSet(wangSetData, 0);
// A 'mixed' wangset has no single-mask equivalent and converts to null.
if (wangSet) {
// Re-derive every autotiled variant across the whole layer, e.g. after
// generating terrain procedurally:
autoTile(ground, wangSet);
// Or, after painting a single cell, refresh just that cell and its eight
// neighbours instead of re-running autoTile over the whole layer:
refreshCell(ground, tx, ty, wangSet);
}tiledWangSetToWangSet handles Tiled’s 'corner' (blob-style, 8-neighbour) and 'edge' (4-neighbour) wangset types; 'mixed' wangsets interleave both semantics in a way that can’t be faithfully represented by a single mask and convert to null. This conversion is a deliberate manual step (not run automatically by toTileMap()) — you decide which layers get autotiled and when.
Per-tile animation
Tiled’s tile editor lets you give a tile an animation: a sequence of frames, each showing a different local tile ID for a set duration. toTileMap() carries these frames into TileDefinition.animation; @codexo/exojs-tilemap ships TileAnimator to drive them at runtime, RPG-Maker style — advancing every animated cell across one or more layers on a shared clock:
import { TileAnimator } from '@codexo/exojs-tilemap';
const tileMap = map.toTileMap();
const animator = new TileAnimator(tileMap.layers);
// Somewhere in your update loop:
scene.systems.add({ update: (delta) => animator.update(delta) });
TileAnimator only rewrites a cell — and rebuilds the chunk that contains it — on the frame it actually changes; static tiles, and animated tiles between frame boundaries, are left untouched. Call animator.reset() to snap every animated cell back to its first frame (useful before serialising or pausing), and animator.rescan() after structural edits that add or remove animated tiles.
Tileset texture ownership
The tileset textures a TiledMap exposes are loaded through the Loader and owned by the loader cache — not by the TiledMap. This matters for teardown:
map.destroy(); // releases the TiledMap; does NOT destroy tileset textures
TiledMap.destroy() deliberately does not destroy its tileset textures, because the same texture may be shared with other maps or sprites through the cache. Release shared textures via the loader’s own unload path when nothing else needs them — never from the map.
Supported scope
The extension covers the common orthogonal workflow, including format details beyond the basics:
- Orthogonal orientation
- Tile layers, including base64 and gzip/zlib-compressed
data - Infinite (chunked) maps — each chunked layer becomes an unbounded runtime layer whose data streams in on demand via
getChunkSource()(see the Infinite maps chapter) - Image-based (atlas) tilesets, embedded in the map or referenced externally (
.tsj) - Object layers — rectangle, ellipse, point, polygon, polyline, tile, and text objects
- Image layers
- Per-tile collision shapes, animation frames, and custom properties
- Wang/autotile sets, via the
tiledWangSetToWangSetconversion step (see Wang and autotile sets above)
Not supported
These raise a clear error or are intentionally absent from this release:
.tmxmaps and.tsxtilesets (XML) — only the JSON format is parsed:.tmjfor maps,.tsjfor external tilesets. Export from Tiled as JSON.- Non-orthogonal orientations — isometric, staggered, and hexagonal throw.
- Collection-of-images tilesets — a tileset built from one image per tile, rather than a single atlas, throws in
toTileMap(). Use an atlas tileset. zstd-compressed tile data — no native decoder is wired up; re-export with gzip, zlib, or uncompressed CSV data instead.- Object templates (
.tx) — an object’s template path is preserved onTiledObject.templatebut not fetched or merged into the object. Avoid templates, or resolve them yourself. - Text kerning — Tiled’s per-text
kerningflag is parsed (TiledTextData.kerning) but dropped duringtoTileMap(); it isn’t carried intoTextStyle.
Where to look next
- Package README:
@codexo/exojs-tiled— install, entry points, and the supported-format matrix. - API reference: the
Loaderpage documentsload(...), explicitAsset.type(...)descriptors, and the.tmjextension lookup. - Spawning objects: Worlds and level streaming turns an object layer into game objects with
MapObjectSpawner, and shows how to drive several Tiled maps as one streamed world. - Extension model: the Particles chapter walks through the same activation model for the sibling renderer extension.

