Guide

Guide Assets Tiled maps

Tiled maps

Load Tiled (.tmj) tilemaps as assets through the official @codexo/exojs-tiled extension.

Intermediate ~8 min read

What you'll learn

  • activate the official Tiled extension explicitly or via /register
  • load a .tmj map through the loader and read its layers and tilesets
  • understand tileset texture ownership and the supported format scope

Before you start

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: TiledMap ships as an official ExoJS extension package, separate from the core. Install @codexo/exojs-tiled alongside @codexo/exojs:

npm install @codexo/exojs @codexo/exojs-tiled

Like all extensions, the core ships nothing Tiled-specific — an Application only understands .tmj files once you activate the extension. There are two ways to do that.

Activation

Pass tiledExtension to ApplicationOptions.extensions. This is explicit, tree-shakeable, and order-independent — 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.

/register convenience

For an app that always wants Tiled support, import the /register entry once at startup. It registers tiledExtension in the global ExtensionRegistry and re-exports the same public API:

import '@codexo/exojs-tiled/register';
import { Application } from '@codexo/exojs';

const app = new Application(); // picks up tiledExtension automatically

The /register import must run before you construct the Application whose extensions are read from the global registry. The explicit extensions: [...] form works regardless of import order.

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 the same way you load any asset — by type token, by path, or by config-map. All three resolve to the same handler.

import { TiledMap } from '@codexo/exojs-tiled';

// By type token (most explicit)
const map = await loader.load(TiledMap, 'levels/forest.tmj');

// By path — the `.tmj` extension is registered, so the type is inferred
const sameMap = await loader.load('levels/forest.tmj');

// By config-map type name — the public `'tiledMap'` lookup
const fromConfig = await loader.load({
    level: { type: 'tiledMap', source: '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.tilesetTextures;  // readonly resolved Texture[] (one per image tileset)
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 a TilePropertyObjectRef: { kind: 'objectRef', id }. Resolve it by matching id against another object’s id in 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 own properties-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.

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.

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:

import { tiledWangSetToWangSet } from '@codexo/exojs-tiled';
import { autoTile, refreshCell } 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);

// 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: (t) => animator.update(t.deltaSeconds) });

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
  • 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 tiledWangSetToWangSet conversion step (see Wang and autotile sets above)

Not supported

These raise a clear error or are intentionally absent from this release:

  • .tmx maps and .tsx tilesets (XML) — only the JSON format is parsed: .tmj for maps, .tsj for external tilesets. Export from Tiled as JSON.
  • Infinite maps — chunked/infinite maps throw; use a fixed-size map.
  • 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 on TiledObject.template but not fetched or merged into the object. Avoid templates, or resolve them yourself.
  • Text kerning — Tiled’s per-text kerning flag is parsed (TiledTextData.kerning) but dropped during toTileMap(); it isn’t carried into TextStyle.

Where to look next

  • Package README: @codexo/exojs-tiled — install, entry points, and the supported-format matrix.
  • API reference: the Loader page documents load(...), the .tmj extension lookup, and the 'tiledMap' config-map type name.
  • Extension model: the Particles chapter walks through the same explicit-vs-/register activation for the sibling renderer extension.