Tilemaps
The @yagejs/tilemap package loads and renders maps created with the
Tiled map editor. It supports tile layer rendering,
object layer extraction, and physics collision shape generation.
Capabilities & Limits
Section titled “Capabilities & Limits”Worth knowing up front so you don’t reach for something that isn’t there.
| Feature | Status |
|---|---|
| Orthogonal Tiled JSON maps | Supported |
| Multiple tile layers per map | Supported |
| Object layers (spawns, triggers, collision shapes) | Supported |
| Custom properties on objects | Supported |
| Object-reference resolution (Tiled object refs) | Supported |
| Collision-shape extraction (rect / ellipse / capsule / polygon → polyline) | Supported |
toPhysicsColliders() adapter for Rapier | Supported |
| Tileset-image and collection-of-images tilesets | Supported |
| Animated tiles | Not yet |
| Infinite / chunked maps | Not yet |
| Isometric / hexagonal / staggered orientations | Not planned |
| Dynamic tile editing at runtime | Not yet |
| Parallax / background scroll layers | Not built-in (use a regular sprite layer) |
For a worked example of loading a Tiled map and extracting collision
shapes, see the
tilemap
example — it parses a Tiled JSON, renders the layers, calls
getCollisionShapes("walls"), and visualizes the resulting rectangles
through the debug overlay. Two distinct steps make up the Tiled-collision-to-Rapier workflow:
- Extraction —
tilemap.getCollisionShapes("walls")returns raw shape data in Tiled’s coordinate system (top-left-originrect/circle/capsule/polygon/polylineconfigs). - Physics conversion —
toPhysicsColliders(shapes)adapts those raw shapes into RapierColliderConfigs with the offsets baked in. Bounding-box shapes (rect, circle, capsule) become center-origin configs (offset =x + width/2, y + height/2); polygons and polylines keep their original vertices with a top-left-based offset (offset = { x, y }), since vertex-defined shapes don’t need centering.
The wiring (extracted shapes → toPhysicsColliders() → static
RigidBodyComponent + per-shape ColliderComponent) is shown inline
in the Collision Extraction section below.
Staging Tiled Assets
Section titled “Staging Tiled Assets”Tiled’s “Export As JSON” dumps the tileset wherever you point it, but
the JSON itself still references its source PNG by path-relative-to-
the-JSON. The loader resolves it as
dirname(tilesetSrc) + tileset.image. Tiled writes that field exactly
as it was authored — typically a path that walks out of the project
("../../Downloads/tiled-projects/dungeon/spr_tileset.png"). The
moment you copy the JSON into public/, that path no longer resolves
in the browser and you get a silent 404: the tileset object loads, the
texture doesn’t, and tiles render as blank rectangles.
The fix is mechanical — rewrite image to a sibling-relative path
whenever you stage a tileset:
{ "tilewidth": 16, "tileheight": 16, "tilecount": 256, "image": "../../Downloads/tiled-projects/dungeon/spr_tileset.png", "image": "spr_tileset.png", ...}…and drop spr_tileset.png next to dungeon.tsj in
public/assets/maps/. The same rule applies to embedded tilesets
inside a map JSON: the image field is resolved relative to the map
file’s directory, so the PNG needs to sit next to the map (or at any
sibling path you reference). Watching for this saves a frustrating
“why is my map blank?” debugging cycle.
TilemapPlugin Setup
Section titled “TilemapPlugin Setup”import { TilemapPlugin } from "@yagejs/tilemap";
engine.use(new TilemapPlugin());The plugin depends on @yagejs/renderer.
Loading a Tiled Map
Section titled “Loading a Tiled Map”Use the tiledMap() asset factory and pair it with a spritesheet texture:
import { tiledMap } from "@yagejs/tilemap";import { renderAsset } from "@yagejs/renderer";
const MapData = tiledMap("assets/level.json");const Tileset = renderAsset("assets/tileset.png");Add both to your scene’s preload array. The tileset texture must load before
the map so tile rendering can resolve texture frames:
class LevelScene extends Scene { readonly preload = [Tileset, MapData];}TilemapComponent
Section titled “TilemapComponent”Add a TilemapComponent to an entity to render the map:
import { Scene, Transform } from "@yagejs/core";import { TilemapComponent } from "@yagejs/tilemap";import type { LayerDef } from "@yagejs/renderer";
class LevelScene extends Scene { readonly name = "level"; readonly preload = [Tileset, MapData];
// Declare the scene's layers — the renderer plugin materializes them // automatically when the scene is pushed. readonly layers: readonly LayerDef[] = [ { name: "map", order: -10 }, ];
onEnter(): void { const map = this.spawn("map"); map.add(new Transform()); map.add(new TilemapComponent({ source: MapData, // asset handle — preferred form layers: ["ground", "walls"], // which tile layers to render (omit for all) layer: "map", // render layer name })); }}source takes the same AssetHandle you passed to preload. Internally
the component captures both the parsed map data and the asset path, so the
same constructor argument enables save/load and the
Tiled-derived auto-keys
covered below. If you don’t have a handle in scope, you can still pass
mapKey as a plain string.
Query map dimensions from the component:
const tilemap = map.get(TilemapComponent);tilemap.widthPx; // total width in pixelstilemap.heightPx; // total height in pixelstilemap.tileWidth; // single tile widthtilemap.tileHeight; // single tile heightSerialization
Section titled “Serialization”TilemapComponent is @serializable, but the live parsed map it wraps is
not — the parsed TiledMapData carries PixiJS textures, which don’t survive
JSON.stringify. The constructor therefore accepts three alternative
inputs: a source: AssetHandle<TiledMapData> (preferred — captures both the
parsed data and the asset path in one argument, which doubles as the prefix
for object auto-keys), an in-memory map object for the fast-path case
where you already have the parsed map, and a mapKey asset path that can
be re-resolved from the AssetManager after a reload.
interface TilemapComponentOptions { /** Asset handle — preferred. Captures both data and path in one place. */ source?: AssetHandle<TiledMapData>; /** Parsed Tiled map data — not serializable, no auto-keys. */ map?: TiledMapData; /** Asset path string — equivalent to `source` when you don't have the handle. */ mapKey?: string; /** Which tile layers to render. Omit to render all. */ layers?: string[]; /** Render layer name. Default: "default". */ layer?: string; /** Override prefix for object auto-keys. Defaults to mapKey. */ keyPrefix?: string;}For any game that uses the save/load system, pass source (or mapKey)
rather than map. When a TilemapComponent constructed with an inline
map gets serialized, serialize() emits a warning and the snapshot cannot
round-trip through afterRestore(). The asset-path form lets the save
system store just the reference and re-look-up the texture-backed map on
restore:
// Recommended: save/load friendly + auto-keys for Tiled objectsmap.add(new TilemapComponent({ source: MapData, layers: ["ground", "walls"], layer: "map",}));
// One-shot prototypes only — will NOT survive a save/load cycle and// auto-keys are unavailable.map.add(new TilemapComponent({ map: parsedTiledJson, layer: "map",}));The snapshot shape itself (TilemapComponentData) is just
{ mapKey, layers?, layer, keyPrefix? } — the serialized entity stores
only the asset key, layer selection, and any explicit keyPrefix override,
and reconstructs the live map on load.
Map Data Structure
Section titled “Map Data Structure”Sometimes you need to reach past the renderer and read raw tile data — for
building a pathfinding grid, counting tile types, or driving custom gameplay
off tile positions. TilemapComponent exposes a format-agnostic snapshot of
the parsed map on its data property:
const tilemap = map.get(TilemapComponent);const data = tilemap.data; // TilemapData
console.log(`${data.width} x ${data.height} tiles`);console.log(`${data.tileLayers.length} tile layers`);The TilemapData shape is deliberately decoupled from the Tiled JSON
format — you can build your own parser for a different editor and reuse the
same downstream consumers.
interface TilemapData { width: number; // tiles wide height: number; // tiles tall tileWidth: number; // pixel width of one tile tileHeight: number; tileLayers: TileLayerData[]; objectLayers: ObjectLayerData[];}
interface TileLayerData { name: string; data: number[]; // flat, row-major tile GIDs (0 = empty) width: number; height: number; visible: boolean;}
interface ObjectLayerData { name: string; objects: MapObject[]; visible: boolean;}TileLayerData.data is a single flat array in row-major order. To read the
tile at tile-coordinate (tx, ty), index with data[ty * width + tx]. A GID
of 0 means “no tile at this position”. That flat layout makes it cheap to
iterate for pathfinding or damage maps:
// Count all solid tiles in the "walls" layerconst walls = data.tileLayers.find((l) => l.name === "walls")!;let solidCount = 0;for (const gid of walls.data) { if (gid !== 0) solidCount++;}MapObject (the element type in objectLayers[i].objects) carries id,
name, optional class, position and size, rotation, an optional point
flag, an optional polygon, and an optional properties: MapObjectProperty[]
array of Tiled custom properties — covered in detail below.
Tile Queries
Section titled “Tile Queries”Look up tile data at a world position:
const gid = tilemap.getTileAt(worldX, worldY, "ground");// Returns the tile GID or null if emptyObject Layers
Section titled “Object Layers”Tiled object layers contain spawn points, triggers, and other game data. Extract objects grouped by their class or name:
const objects = tilemap.getObjects("spawns");// Returns: Record<string, MapObject[]>// e.g. { "player": [{ x, y, ... }], "enemy": [{ x, y, width, height, ... }] }
for (const obj of objects["enemy"] ?? []) { this.spawn(EnemyBP, { x: obj.x, y: obj.y });}Each MapObject has:
interface MapObject { id: number; name: string; class?: string; x: number; y: number; width: number; height: number; rotation: number; visible: boolean; point?: boolean; polygon?: { x: number; y: number }[]; properties?: MapObjectProperty[];}Object Lookups
Section titled “Object Lookups”The component exposes a few direct accessors so you don’t have to flatten
getObjects() for one-off lookups:
tilemap.getAllObjects(); // flat list across every layertilemap.findObject(42); // by Tiled idtilemap.findObjectByName("Player"); // first match across all layersSpawning Entities from Tiled Objects (auto-keys)
Section titled “Spawning Entities from Tiled Objects (auto-keys)”Tiled assigns every object a stable per-map id. Combine that id with the
map’s asset path to derive a stable entity.key so persistent stores
(createSet<string>, createMap<string, T>) can be keyed off authored
content without your game inventing its own naming scheme. The component
exposes the prefix wiring as objectKey() and forEachObject():
import { tiledObjectKey, TilemapComponent } from "@yagejs/tilemap";
// Standalone helper, format: "<prefix>#object:<id>"tiledObjectKey("/assets/dungeon.json", 42);// → "/assets/dungeon.json#object:42"
// On the component — prefix already wired up from `source` / `mapKey`:tilemap.objectKey(obj);
tilemap.forEachObject("interactables", (obj, key) => { if (obj.class === "EnemySpawn") { this.spawn(EnemyEntity, { object: obj }, { key }); }});Pass keyPrefix: "level1" to the component constructor when multiple
instances of the same map need distinct identity namespaces (instanced
dungeons, per-floor layouts).
objectKey and forEachObject throw if the component was constructed from
raw map: data without a mapKey, source, or explicit keyPrefix — auto-keys
need a stable prefix.
See Persistent World State for the
canonical pattern that pairs these auto-keys with createSet /
createMap to remember which chests are open, which triggers fired,
etc., across save/load.
Custom Properties
Section titled “Custom Properties”Read custom Tiled properties from objects. The component-method variants are typed and discoverable; the standalone helpers exist for cases where you’ve already collected an object pool yourself.
// On the component (preferred)tilemap.getProperty<number>(obj, "speed");tilemap.getPropertyArray<number>(obj, "point");tilemap.resolveRef(obj, "target"); // walks every layertilemap.resolveRefArray(obj, "spawns");
// Standalone (caller supplies the pool)import { getProperty, getPropertyArray, resolveObjectRef, resolveObjectRefArray } from "@yagejs/tilemap";getProperty<number>(obj, "speed");getPropertyArray<number>(obj, "point");resolveObjectRef(obj, "target", allObjs);resolveObjectRefArray(obj, "path", allObjs);Collision Extraction
Section titled “Collision Extraction”Tiled objects map to physics-agnostic TilemapColliderConfig variants:
| Tiled object | Emitted shape |
|---|---|
| Rectangle | { type: "rect", x, y, width, height } |
Ellipse (width === height) | { type: "circle", x, y, width, height, radius } |
Ellipse (width !== height) | { type: "circle", ..., radius: max(w, h) / 2 } + dev warning (Rapier has no real ellipse — author it as a capsule for a true non-circular round shape) |
| Capsule | { type: "capsule", x, y, width, height, halfHeight, radius, axis } (oriented along the longer axis) |
| Polygon | { type: "polyline", x, y, vertices } (Tiled polygons are outlines; may be concave) |
| Point | Skipped |
Because Tiled polygons are emitted as polylines (chain of line
segments), they support concave outlines such as a winding water edge
or a U-shaped wall. The trade-off: polylines are static-only — they
must be attached to a type: "static" rigid body (no mass/inertia is
computed). For dynamic concave bodies, decompose the outline into
convex pieces manually.
Wire extracted shapes through toPhysicsColliders() to spawn one static
body with one ColliderComponent per shape:
import { toPhysicsColliders } from "@yagejs/tilemap";import { RigidBodyComponent, ColliderComponent } from "@yagejs/physics";
const walls = this.spawn("walls");walls.add(new Transform());walls.add(new RigidBodyComponent({ type: "static" }));
for (const cfg of toPhysicsColliders(tilemap.getCollisionShapes("walls"))) { walls.add(new ColliderComponent(cfg));}The component-method shortcut wraps two standalone functions for the same workflow:
import { extractCollisionShapes, toPhysicsColliders } from "@yagejs/tilemap";Camera Bounds
Section titled “Camera Bounds”Use the tilemap dimensions to constrain the camera. The camera is an entity you spawn in the scene:
import { CameraEntity } from "@yagejs/renderer";
const cam = this.spawn(CameraEntity, { follow: player.get(Transform) });cam.bounds = { minX: 0, minY: 0, maxX: tilemap.widthPx, maxY: tilemap.heightPx,};This prevents the camera from scrolling past the map edges.