More in 2D
Three components share the work. Tilemap owns the .tilemap.json — the grid, the tilesets and
the per-tile colliders; TilemapRenderer draws it from an atlas, in chunks it culls; and
TilemapCollider2D turns the Tilemap beside it into Rapier shapes. The collision data is merged
per chunk into as few polygons as the tiles allow, so a wall is one shape, not one per cell.
The collider reads the tilemap itself, so the same three components in a scene file collide with
no code. 2D physics checks the tilemap's data every fixed step and rebuilds the shapes when it
changes, so tilemap.setTile(...) reaches physics with no subscription of your own.
Cell (0, 0) is the bottom-left of the map, because the world is +Y up — but the file stores
its rows top first, like every editor, and the loader flips them. worldToCell speaks cells
and is exact; tiles carry no component, so app.twoD.pickAt never returns one.
The file's objects layer is data: nothing spawns from it. This level is built in code, so it reads
definition.objects and places the player itself; a level in a scene file gets its objects as
entities from ignifx import tilemap. Every sortingLayer a layer names must exist in the core
sortingLayers setting, or the layer reports IGX-1107.
level.tilemap.json beside this file is what 2d/level.tilemap.json resolves to.
import { Tilemap, TilemapRenderer, twoD } from "@ignifx/2d";import { Vec2, createApp } from "@ignifx/core";import { TilemapCollider2D, physics2d } from "@ignifx/physics-2d";import type { SpriteAtlasAsset, TilemapAsset } from "@ignifx/2d";const canvas = document.querySelector("canvas");if (!(canvas instanceof HTMLCanvasElement)) { throw new Error("ignifx renders into a <canvas> element.");}const app = await createApp({ canvas, settings: { assets: { root: "assets" }, sortingLayers: { sortingLayers: ["Background", "Terrain", "Default"] }, }, extensions: [twoD({ pixelsPerUnit: 16 }), physics2d()],});const map = await app.assets.loadAsync<TilemapAsset>("2d/level.tilemap.json");const tiles = await app.assets.loadAsync<SpriteAtlasAsset>("2d/tiles.atlas.json");const level = app.world.createEntity("Level");const tilemap = level.addComponent(Tilemap, { chunkSize: 16 });tilemap.map = map;level.addComponent(TilemapRenderer, { sortingLayer: "Terrain", cullChunks: true }).atlas = tiles;// One static body for the whole level: the collider reads the merged polygons of the tilemap beside it.level.addComponent(TilemapCollider2D);// The objects layer is data; a level built in code places what it needs from it.for (const object of map.value.definition.objects) { if (object.type === "spawn") { app.world.createEntity(object.name).transform.position2D = new Vec2(object.x + object.width / 2, object.y); }}await app.start();// Digging a hole re-merges the chunk and bumps `collisionData.version`; physics rebuilds it.const cell = tilemap.worldToCell({ x: 2.5, y: 0.5 }, new Vec2());tilemap.setTile(0, cell.x, cell.y, 0);app.log.info("tile under the player:", tilemap.getTile(0, cell.x, cell.y));window.addEventListener("pagehide", () => { map.release(); tiles.release(); app.dispose();});Source: examples/recipes/load-a-tilemap-with-collision/main.ts