All guides

Load a tilemap and give it collision

2DGuide

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.

typescript
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