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 the map, layers, tilesets and 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 |
| Embedded and external tilesets | Supported |
| Flipped and rotated tiles | Supported for square tiles — a diagonal flip on a non-square tile draws unrotated |
| Tile images that don’t match the map grid | Supported — anchored bottom-left, as Tiled draws them |
| Tile objects (an object that draws a tile) | Data only — gid, position, size and properties are parsed; the image is not drawn |
Tileset objectalignment override | Supported — a tile object’s position is normalised to its top-left corner |
Tileset tilerendersize / fillmode | Not read — a tile draws at its image’s own size |
| Collision shapes authored on a tile in the tileset | Not read |
Layer draw offsets and tileset tileoffset | Supported |
| Per-layer visibility and opacity | Supported |
| Animated tiles | Supported for evenly-spaced, equal-duration frames |
| Infinite / chunked maps | Not yet |
| Base64-encoded layer data | Not yet — export layer data as CSV |
| Group layers and image layers | 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) |
A map that uses an unsupported map, layer or tileset setting does not fail
silently: validateTiledMap() reports it against the raw JSON, and
TilemapComponent logs what it finds when the map loads — see
Checking a Map. Four limits have no diagnostic, because the
map itself parses fine: runtime tile editing, per-tile collision shapes, a
tileset’s tilerendersize / fillmode, and a diagonal flip on a non-square
tile.
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 RapierColliderPartConfigs 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() → one static body) 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 an external tileset’s image against
the tileset file’s own folder, so maps/level.json referencing
../tilesets/terrain.tsj with "image": "terrain.png" loads
tilesets/terrain.png. 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 put the handle in your scene’s
preload array:
import { Scene } from "@yagejs/core";import { tiledMap } from "@yagejs/tilemap";
const MapData = tiledMap("assets/level.json");
class LevelScene extends Scene { readonly name = "level"; readonly preload = [MapData];}The map handle owns its tileset images. Loading it loads every single-image
tileset the map references, and assets.unload(MapData) releases them again:
import { Scene } from "@yagejs/core";import { tiledMap } from "@yagejs/tilemap";
const MapData = tiledMap("assets/level.json");
class LevelScene extends Scene { readonly name = "level"; readonly preload = [MapData]; onExit() { this.assets.unload(MapData); // frees the map and its tileset images }}The images are ordinary counted texture() handles, so a second map on the
same tileset — or a sprite drawing from the same sheet — keeps the image alive
until it too releases it. A sprite drawing from the tileset sheet should
declare texture(tilesetPath) in its own preload — same type and path is the
same reference count, so the image survives the map’s unload. Do not declare
that file under a different handle type: renderAsset() counts separately
while both loaders unload the same Pixi path, so the first unload destroys it
for the other. Unloading the map also drops any external tileset (.tsj) file
it inlined.
A collection-of-images tileset works differently. It names one image per tile rather than a single sheet, and those images are drawn from an atlas you preload yourself.
TilemapComponent
Section titled “TilemapComponent”Add a TilemapComponent to an entity to render the map:
import { Scene, Transform } from "@yagejs/core";import { TilemapComponent, tiledMap } from "@yagejs/tilemap";import type { LayerDef } from "@yagejs/renderer";
const MapData = tiledMap("assets/level.json");
class LevelScene extends Scene { readonly name = "level"; readonly preload = [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. The component
uses both the parsed map data and the asset path, so the same argument also
enables the Tiled-derived auto-keys
covered below. If you don’t have a handle in scope, pass mapKey as a plain
string.
Only a tiledMap() handle loaded through the asset manager brings the tileset
images with it. A map reached by mapKey, or handed over as raw map data,
was loaded some other way — preload each tileset image yourself with
texture(path), or register it under the key the tileset’s image names.
Without it the component throws when it is added, naming the image.
Query map dimensions from the component:
import { TilemapComponent } from "@yagejs/tilemap";
const tilemap = entity.get(TilemapComponent);tilemap.widthPx; // total width in pixelstilemap.heightPx; // total height in pixelstilemap.tileWidth; // single tile widthtilemap.tileHeight; // single tile heightTinting, fading and effects
Section titled “Tinting, fading and effects”TilemapComponent is a visual component like SpriteComponent, so it accepts
the same visual options and exposes the same vocabulary — tint, alpha, blend
mode, visibility, masks, and a component-scope effects host:
import { bloom } from "@yagejs/effects";import { TilemapComponent, tiledMap } from "@yagejs/tilemap";
const MapData = tiledMap("assets/level.json");
entity.add(new TilemapComponent({ source: MapData, layer: "map", tint: 0x5566aa, // wash the whole map cold alpha: 0.85,}));
const tilemap = entity.get(TilemapComponent);tilemap.tint = 0xffffff; // back to untintedtilemap.fx.addEffect(bloom({ bloomScale: 1.5 })); // effects on the map aloneTint and alpha reach the tiles through a colour filter on the map’s container, because the tile renderer has no colour input of its own. A map at full white and full opacity has no filter attached, so an untinted map costs nothing.
Checking a Map
Section titled “Checking a Map”A Tiled map can use features this package does not render. Rather than dropping them quietly, the loader collects them:
import { TilemapComponent } from "@yagejs/tilemap";
const tilemap = entity.get(TilemapComponent);
for (const d of tilemap.data.diagnostics) { console.log(`${d.severity}: ${d.message}`);}// error: Group layer "decor" is not rendered. Its dropped children are: vines, banners.// warning: Layer "clouds" sets parallax, which the renderer does not apply.TilemapComponent already logs the same list as a single warning when it
loads a map with anything in it, so a broken map announces itself in the
console without you writing this loop.
To check a map before building anything from it — in a content pipeline, an asset test, or a level editor — run the validator over the raw JSON:
import { validateTiledMap, type TiledMapData } from "@yagejs/tilemap";
function assertMapRenders(rawTiledJson: TiledMapData) { const problems = validateTiledMap(rawTiledJson) .filter((d) => d.severity === "error");
if (problems.length > 0) { throw new Error(problems.map((d) => d.message).join("\n")); }}A diagnostic is an error when authored content is dropped or will render
wrong, and a warning when a setting is simply not applied. Each one carries
a code you can match on (group-layer, infinite-map,
unsupported-orientation, unsupported-tile-animation, and so on), plus the layer or
tileset name it came from. A gate filtered to errors lets every warning
through, so match on the codes your game cannot live with. tile-object is
an error — it names the tile objects on a layer whose images the map does not
draw — so a game that spawns its own sprites from them will want to let that
one through.
No diagnostic stops the map from rendering. What one names is skipped — a group layer, the tiles of a tileset that did not resolve, a tile whose id belongs to no tileset — and everything else draws. That is why the list is worth reading: a map missing a few tiles looks like a map, not like an error.
unknown-gid covers the last of those. It reports the cells a layer fills
with a tile id no tileset owns, which is what a map looks like after a tileset
is removed without clearing the tiles painted from it, or after a tile’s image
is deleted from a collection-of-images tileset. The message names the layer
and, per distinct id, the first cell it appears in.
An external tileset that has not been loaded yet is not reported — it resolves
during preload. A tileset image that is not loaded when the component is added
is not a diagnostic either: building the layers throws and names the image and
its tileset. Loading the map through the asset manager loads its images with
it, so you mostly meet this when building a component from raw TiledMapData.
Choosing the map input
Section titled “Choosing the map input”The constructor accepts three inputs: a source: AssetHandle<TiledMapData>
(preferred, because it carries both the parsed data and the asset path), an
in-memory map object when you already have parsed data, or a mapKey asset
path that the component resolves through the AssetManager.
import type { AssetHandle } from "@yagejs/core";import type { VisualComponentOptions } from "@yagejs/renderer";import type { TiledMapData } from "@yagejs/tilemap";
interface TilemapComponentOptions extends VisualComponentOptions { /** Asset handle — preferred. Captures both data and path in one place. */ source?: AssetHandle<TiledMapData>; /** Parsed Tiled map data. Requires keyPrefix for 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[]; /** Override prefix for object auto-keys. Defaults to mapKey. */ keyPrefix?: string; // Plus the shared visual options: layer, visible, tint, alpha, // blendMode, interactive.}Use source when the map is part of scene preload. Use mapKey when only the
asset path is in scope. Use map for generated or already parsed data; add a
keyPrefix if Tiled object ids need stable game-wide keys:
import { TilemapComponent, tiledMap, type TiledMapData } from "@yagejs/tilemap";
const MapData = tiledMap("assets/level.json");declare const parsedTiledJson: TiledMapData; // map data your code built or parsed
// Preferred for preloaded assets; enables auto-keys for Tiled objects.entity.add(new TilemapComponent({ source: MapData, layers: ["ground", "walls"], layer: "map",}));
// Generated or already parsed data. Add keyPrefix to enable auto-keys.entity.add(new TilemapComponent({ map: parsedTiledJson, keyPrefix: "generated-level-1", layer: "map",}));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:
import { TilemapComponent } from "@yagejs/tilemap";
const tilemap = entity.get(TilemapComponent);const data = tilemap.data; // TilemapData
console.log(`${data.width} x ${data.height} tiles`);console.log(`${data.tileLayers.length} tile layers`);The Inspector leaves data out of the component’s reflected state, since it
holds one id per tile per layer.
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.
import type { MapObject, MapObjectProperty, TilemapDiagnostic,} from "@yagejs/tilemap";
interface TilemapData { width: number; // tiles wide height: number; // tiles tall tileWidth: number; // pixel width of one tile tileHeight: number; properties?: MapObjectProperty[]; // the map's own custom properties tileLayers: TileLayerData[]; objectLayers: ObjectLayerData[]; tilesets: TilesetInfo[]; diagnostics: TilemapDiagnostic[];}
interface TileLayerData { name: string; data: number[]; // flat, row-major tile GIDs (0 = empty) width: number; height: number; visible: boolean; offsetX: number; // the layer's Tiled draw offset, in pixels offsetY: number; properties?: MapObjectProperty[];}
interface ObjectLayerData { name: string; objects: MapObject[]; // coordinates already include offsetX/offsetY visible: boolean; offsetX: number; offsetY: number; properties?: MapObjectProperty[];}
interface TilesetInfo { firstGid: number; name?: string; properties?: MapObjectProperty[];}Custom properties are available at every level Tiled offers them, so a map can carry its own identity — a biome, a music track, a level index — without a marker object standing in for it:
const biome = tilemap.getProperty<string>(tilemap.data, "biome");const track = tilemap.getProperty<string>(tilemap.data, "music");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 gid for
objects that draw a tile, optional point, ellipse and capsule flags, an
optional polygon, an optional polyline, 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:
import { TilemapComponent } from "@yagejs/tilemap";
const tilemap = entity.get(TilemapComponent);const worldX = 200;const worldY = 96;
const id = tilemap.getTileAt(worldX, worldY, "ground");// Returns the tile id, or null if the position is emptyThe point is converted through the inverse of the entity’s world transform
before any lookup, so a map parented under another entity, scaled or rotated
answers where its tiles are drawn. Each layer is then read through its own draw
offset, so the id you get back is the tile you see at that position. A world
scale of 0 on either axis collapses the map, and the method returns null.
A tileset’s tileoffset is the exception. It moves where a tile’s image is
drawn without moving the cell the tile occupies. A tile from an offset tileset
answers at its cell, which is where its collision and its neighbours are.
Tiles that don’t match the grid
Section titled “Tiles that don’t match the grid”A tileset’s tiles need not be the size of the map’s grid — a three-cell-tall pillar on a 16px map, or an 8px coin. Tiled anchors every tile image to the bottom-left of the cell it was painted in, so a taller image overhangs upward and a wider one to the right, and a smaller one sits on the cell’s bottom edge. The tile still belongs to that one cell. Tiles render where Tiled draws them.
In a collection-of-images tileset each tile is measured on its own, which lets one tileset mix a 16px stump with a 64px pine and place both correctly.
A tile draws at its image’s own size. A tileset can ask Tiled to scale its
tiles to the grid instead (tilerendersize: "grid"); that setting is not read,
so those tiles keep their own size here and land where the rule above puts
them.
Because the cell is unchanged, getTileAt answers at the cell a tile was
painted in, not under the part of the image that overhangs its neighbours.
Flipped and rotated tiles
Section titled “Flipped and rotated tiles”Tiled lets you flip a tile horizontally, flip it vertically, or reflect it across its diagonal, in any combination — eight orientations in all. They render the way the editor shows them, so a tileset needs only one copy of a wall corner or an arrow.
The diagonal flip holds for square tiles. On a tile whose image is not square it draws unrotated: the tile keeps its own width and height and only its image content turns, where Tiled swaps the two. Turning a 16×32 prop needs a second tile drawn at 32×16.
Tiled packs that orientation into the high bits of the tile’s GID. getTileAt
strips them, so a comparison works whichever way a tile faces:
const WALL_CORNER = 12; // the corner tile's id in your tileset
if (tilemap.getTileAt(worldX, worldY, "walls") === WALL_CORNER) { /* … */ }Raw layer data keeps the bits, because the orientation is sometimes the point — a one-way conveyor tile, say, whose direction comes from how it was placed:
import { readTileGid, TilemapComponent } from "@yagejs/tilemap";
const CONVEYOR = 40; // the conveyor tile's id in your tilesetconst tilemap = entity.get(TilemapComponent);const col = 7;const row = 3;let direction = 1;
const layer = tilemap.data.tileLayers.find((l) => l.name === "machinery")!;const { id, flippedHorizontally } = readTileGid(layer.data[row * layer.width + col]!);
if (id === CONVEYOR) { direction = flippedHorizontally ? -1 : 1;}Animated tiles
Section titled “Animated tiles”Water, torches, and machinery animated in Tiled play on their own. There is no
component option to set and no per-frame game code — author the animation in
the tileset and it runs. The clock comes from the scene, so a paused scene
freezes its tilemaps and a slowed timeScale slows them with it.
An animation plays when three things hold, and Tiled’s animation editor gives you all three by default:
- The tileset is a single image, not a collection of separate images.
- Every frame lasts the same time.
- The frames sit a constant distance apart in the tileset image — next to each other along a row, or down a column.
An animation outside that shape renders unanimated — as the tile the map
places — and reports an unsupported-tile-animation warning saying which tile
and why: durations that differ, frames scattered around the image, or a
collection-of-images tileset.
Splitting a two-second pause into ten identical 200ms frames is usually enough
to bring an animation back into the supported shape.
The animation clock is runtime state. Reconstructing the scene starts the cycle from the beginning.
Object Layers
Section titled “Object Layers”Tiled object layers contain spawn points, triggers, and other game data. Extract objects grouped by their class or name:
import { Entity, Transform } from "@yagejs/core";import { TilemapComponent } from "@yagejs/tilemap";
class EnemyEntity extends Entity { setup({ x, y }: { x: number; y: number }) { this.add(new Transform({ position: { x, y } })); }}
const tilemap = entity.get(TilemapComponent);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(EnemyEntity, { x: obj.x, y: obj.y });}Each MapObject has:
import type { MapObjectProperty } from "@yagejs/tilemap";
interface MapObject { id: number; name: string; class?: string; x: number; y: number; width: number; height: number; rotation: number; visible: boolean; gid?: number; // set only on an object that draws a tile point?: boolean; ellipse?: boolean; capsule?: boolean; polygon?: { x: number; y: number }[]; polyline?: { x: number; y: number }[]; properties?: MapObjectProperty[];}getObjects() merges every layer when you don’t name one, and it keys a
classless object by its name — fine for a lookup, wrong when two layers hold
the same class or when a class and an object share a spelling. Use
getObjectGroups() when the layer matters:
import { TilemapComponent } from "@yagejs/tilemap";
const tilemap = entity.get(TilemapComponent);
for (const group of tilemap.getObjectGroups()) { console.log(group.layer, group.class, group.objects.length);}// "spawns" "EnemySpawn" 4// "spawns" undefined 1 ← objects authored without a class// "patrol" "EnemySpawn" 2 ← same class, different layer, kept apartEach entry is { layer, class?, objects }, grouped by layer and class.
Objects with no class collect into that layer’s single class: undefined
entry.
Tile objects
Section titled “Tile objects”Dragging a tile out of the tileset onto an object layer makes a tile
object: an object that shows a tile. It has a gid, the global ID of that
tile, alongside everything a normal object has — a class, a name, custom
properties, a size you can stretch. The image is not drawn for you. An object
layer is data, so a tile object is a placement you read and turn into an
entity.
Its x and y are the top-left corner of the tile it draws, the same as
every other object type. Tiled itself stores a tile object on a different
corner — the bottom-left one, matching how tiles land in tile layers, or
whichever corner the tileset’s objectalignment names — and the parser moves
the position for you. A spawn reads it directly:
import { Entity, Transform } from "@yagejs/core";import { readTileGid, TilemapComponent } from "@yagejs/tilemap";
class TreeEntity extends Entity { setup(tree: { tileId: number; x: number; y: number; flippedHorizontally: boolean }) { this.add(new Transform({ position: { x: tree.x, y: tree.y } })); // ...plus a sprite showing tile `tree.tileId`, mirrored when flipped }}
const tilemap = entity.get(TilemapComponent);
for (const obj of tilemap.getObjects("props")["Tree"] ?? []) { if (obj.gid === undefined) continue; // authored as a plain rectangle
const { id, flippedHorizontally } = readTileGid(obj.gid); this.spawn(TreeEntity, { tileId: id, x: obj.x, y: obj.y, flippedHorizontally, });}obj.rotation turns the tile about that same corner, in degrees. Put the
position and the rotation on the entity’s Transform and anchor the sprite
top-left, and it lands where Tiled shows it at any angle. Position and
rotation belong on the Transform — the renderer copies them onto the sprite
every frame, so a rotation set on the sprite is overwritten.
import { Entity, MathUtils, Transform } from "@yagejs/core";import { SpriteComponent, type TextureRef } from "@yagejs/renderer";import type { MapObject } from "@yagejs/tilemap";
// The entity spawned for a tile object, given the object and its tile's textureclass TileObjectEntity extends Entity { setup({ obj, texture }: { obj: MapObject; texture: TextureRef }) { this.add(new Transform({ position: { x: obj.x, y: obj.y }, rotation: MathUtils.degToRad(obj.rotation), // Tiled stores degrees })); this.add(new SpriteComponent({ texture, anchor: { x: 0, y: 0 } })); }}Collision extraction reads the same corner, so its rect and the sprite agree.
The gid keeps Tiled’s flip bits, exactly like the GIDs in raw layer data, so
readTileGid splits it the same way.
Because the image is not drawn for you, validateTiledMap() reports a
tile-object error for each object layer that holds tile objects, naming
them. Collision shapes authored on the tile inside the tileset are not read
either, so collision extraction treats a tile object as its full box.
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:
import { TilemapComponent } from "@yagejs/tilemap";
const tilemap = entity.get(TilemapComponent);
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 { Entity, Transform } from "@yagejs/core";import { tiledObjectKey, TilemapComponent, type MapObject } from "@yagejs/tilemap";
class EnemyEntity extends Entity { setup({ object }: { object: MapObject }) { this.add(new Transform({ position: { x: object.x, y: object.y } })); }}
const tilemap = entity.get(TilemapComponent);
// 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`:const obj = tilemap.findObject(42);if (obj) 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 an object, a layer, a tileset, or the map
itself — all four helpers take anything carrying a properties array. The
component-method variants are typed and discoverable; the standalone helpers
exist for cases where you’ve already collected an object pool yourself.
import { TilemapComponent, type MapObject } from "@yagejs/tilemap";
const tilemap = entity.get(TilemapComponent);declare const obj: MapObject; // an object from the map
// On the component (preferred)tilemap.getProperty<number>(obj, "speed");tilemap.getPropertyArray<number>(obj, "point");tilemap.resolveRef(obj, "target"); // walks every layertilemap.resolveRefArray(obj, "spawns");
// The same helpers on the map, a layer, or a tilesettilemap.getProperty<string>(tilemap.data, "biome");for (const layer of tilemap.data.tileLayers) { tilemap.getProperty<number>(layer, "damage");}for (const tileset of tilemap.data.tilesets) { tilemap.getProperty<string>(tileset, "material");}
// Standalone (caller supplies the pool)import { getProperty, getPropertyArray, resolveObjectRef, resolveObjectRefArray } from "@yagejs/tilemap";const allObjs = tilemap.getAllObjects();getProperty<number>(obj, "speed");getPropertyArray<number>(obj, "point");resolveObjectRef(obj, "target", allObjs);resolveObjectRefArray(obj, "path", allObjs);getPropertyArray and resolveRefArray read the name[0], name[1]
convention, and the indices must run from 0 without gaps. point[0] plus
point[2] with no point[1] throws, naming the missing index, because a gap
means a property was renamed or deleted in Tiled and the array you would get
back has a hole in it.
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, rotation? } |
Tile object (carries a gid) | { type: "rect", x, y, width, height, rotation? } covering the tile’s whole box, exactly like a rectangle; shapes authored on the tile in the tileset are not read |
Ellipse (width === height) | { type: "circle", x, y, width, height, radius } |
Ellipse (width !== height) | { type: "polygon", x, y, vertices } — 24 vertices sampled on the ellipse outline (Rapier has no ellipse primitive; the ring is convex, so the physics-side convex hull matches it exactly) |
| Capsule | { type: "capsule", x, y, width, height, halfHeight, radius, axis, rotation? } (oriented along the longer axis) |
| Polygon | { type: "polyline", x, y, vertices } with the first vertex appended at the end, closing the loop (Tiled polygons are closed outlines; may be concave) |
| Polyline | { type: "polyline", x, y, vertices } verbatim (open chain) |
| Point | Skipped |
Object rotation is honored for every shape. Tiled stores it in degrees,
pivoting on the object’s position: polygon, polyline, and sampled-ellipse
vertices come out already rotated; a rotated circle has the position shift
baked into its x/y; rectangles and capsules carry a rotation field in
radians, which toPhysicsColliders() forwards to the physics collider. A
rectangle rotated in the Tiled editor — the natural way to author a ramp —
collides exactly as drawn.
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.
Pass the extracted shapes through toPhysicsColliders() and use the result as
one collider component’s parts. The collision layer then uses one static
body:
Install @yagejs/physics and import the adapter from
@yagejs/tilemap/physics. The root tilemap entry stays usable in projects that
only load and render maps.
import { Transform } from "@yagejs/core";import { TilemapComponent } from "@yagejs/tilemap";import { toPhysicsColliders } from "@yagejs/tilemap/physics";import { RigidBodyComponent, ColliderComponent } from "@yagejs/physics";
const tilemap = entity.get(TilemapComponent);const walls = this.spawn("walls");walls.add(new Transform());walls.add(new RigidBodyComponent({ type: "static" }));walls.add(new ColliderComponent({ parts: toPhysicsColliders(tilemap.getCollisionShapes("walls")),}));Each part’s own offset places its shape. A static body reads its world position
when it is created, so position walls before adding the body to move the
whole collision layer.
The component-method shortcut wraps two standalone functions for the same workflow:
import { extractCollisionShapes } from "@yagejs/tilemap";import { toPhysicsColliders } from "@yagejs/tilemap/physics";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 { Entity, Transform } from "@yagejs/core";import { CameraEntity } from "@yagejs/renderer";import { TilemapComponent } from "@yagejs/tilemap";
class Player extends Entity { setup() { this.add(new Transform({ position: { x: 64, y: 64 } })); }}
const tilemap = entity.get(TilemapComponent);const player = this.spawn(Player);
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.
Coding agents: fetch https://yage.dev/llms.txt first and prefer the Markdown references it links over these HTML pages. This page's Markdown counterpart is /llms/packages/tilemap.md.