| name | phaser-tilemap |
| description | This skill should be used when the user asks to "add tilemap", "create a level", "Tiled editor", "tile collision", "object layer", "create map", "tilemap not showing", "level design", "tile layer", "game map", "spawn point", "trigger zone", or "map collision". |
| version | 0.4.0 |
Phaser 4 Tilemaps
Phaser 4 has first-class support for Tiled map files (JSON format). The recommended workflow is: design in Tiled → export JSON → load in Phaser → create layers → set up collision.
Tiled Editor Workflow
Download Tiled for free at mapeditor.org.
Creating a New Map
- File → New → New Map
- Set Orientation to Orthogonal (most 2D games) or Isometric (top-down RPG/strategy)
- Set Tile layer format to CSV or Base64 — both work with Phaser
- Set Tile size to match your tileset (common: 16x16, 32x32)
- Set Map size in tiles (e.g. 40 wide × 23 tall for a 1280x736 map at 32px tiles)
Adding a Tileset
- In the Tilesets panel (bottom right), click the + button → New Tileset
- Set Name — this exact name is used in
map.addTilesetImage() as the first argument
- Set Type to Based on Tileset Image
- Browse to your PNG, set tile width/height to match
- Margin and spacing: set these if your tileset has padding between tiles (often 0)
Marking Collision Tiles
- Select your tileset in the Tilesets panel
- Click the wrench icon (Edit Tileset) to open the tileset editor
- Select the tiles that should collide (click to select, Ctrl+click for multiple)
- In the Properties panel (left side), click the + button to add a property
- Set Name to
collides, Type to bool, Value to true
- Close the tileset editor
In Phaser, call layer.setCollisionByProperty({ collides: true }) to activate these tiles.
Layer Naming Conventions
Use consistent layer names — Phaser references them by string:
| Layer Name | Type | Purpose |
|---|
Background | Tile Layer | Sky, distant scenery — no collision |
Ground | Tile Layer | Main walkable surface — collision enabled |
Hazards | Tile Layer | Spikes, lava — overlap (not collide) |
Foreground | Tile Layer | Trees, arches that render in front of player |
Objects | Object Layer | Spawn points, triggers, enemies |
Object Layer Usage
Object Layers in Tiled let you place named points, rectangles, and polygons that Phaser can query.
Named objects (for unique things like player spawn):
- Add Object Layer named
Objects
- Select the Rectangle tool, place an object on the map
- In Properties, set Name to
PlayerSpawn
Typed objects (for groups of the same kind, like enemies):
- Place objects and set Type (Tiled 1.8: use Class) to
Enemy
- Add custom properties: click +, add
health (int, 100), patrol (bool, true)
Export Settings
- File → Export As → JSON Map Files (
.json)
- In export options, enable Embed tilesets — this avoids external
.tsx dependencies
- Save to
public/assets/tilemaps/level1.json
- Place the tileset PNG at
public/assets/images/terrain.png
Loading Assets
preload(): void {
this.load.tilemapTiledJSON('level1', 'assets/tilemaps/level1.json');
this.load.image('terrain', 'assets/images/terrain.png');
}
Creating the Map and Layers
private map!: Phaser.Tilemaps.Tilemap;
private groundLayer!: Phaser.Tilemaps.TilemapLayer;
create(): void {
this.map = this.make.tilemap({ key: 'level1' });
const tileset = this.map.addTilesetImage('terrain', 'terrain');
const bgLayer = this.map.createLayer('Background', tileset!, 0, 0);
this.groundLayer = this.map.createLayer('Ground', tileset!, 0, 0)!;
const fgLayer = this.map.createLayer('Foreground', tileset!, 0, 0);
fgLayer!.setDepth(10);
}
Common reason tilemap does not show: the tileset name in addTilesetImage does not exactly match the name set in Tiled. Open the JSON file and check the "name" field inside "tilesets".
Collision
By Property (recommended)
Uses the collides: true property set in Tiled's tileset editor:
this.groundLayer.setCollisionByProperty({ collides: true });
this.physics.add.collider(this.player, this.groundLayer);
By Tile Index Range
Collide tiles with GID (global ID) 1 through 10:
this.groundLayer.setCollisionBetween(1, 10);
By Exclusion
Collide all tiles except empty (-1) and a specific index:
this.groundLayer.setCollisionByExclusion([-1, 0]);
Debug Rendering
Visualize collision tiles during development:
const debugGraphics = this.add.graphics();
this.groundLayer.renderDebug(debugGraphics, {
tileColor: null,
collidingTileColor: new Phaser.Display.Color(243, 134, 48, 128),
faceColor: new Phaser.Display.Color(40, 39, 37, 255),
});
Multiple Layers
create(): void {
const tileset = this.map.addTilesetImage('terrain', 'terrain')!;
const bgLayer = this.map.createLayer('Background', tileset, 0, 0);
const groundLayer = this.map.createLayer('Ground', tileset, 0, 0)!;
const fgLayer = this.map.createLayer('Foreground', tileset, 0, 0);
fgLayer!.setDepth(10);
groundLayer.setCollisionByProperty({ collides: true });
this.player.setDepth(5);
}
Object Layers
Read spawn points, triggers, and entity placements from Tiled's Object Layer:
create(): void {
const spawnPoint = this.map.findObject('Objects', obj => obj.name === 'PlayerSpawn');
this.player = this.physics.add.sprite(spawnPoint!.x!, spawnPoint!.y!, 'player');
const enemyObjects = this.map.filterObjects('Objects', obj => obj.type === 'Enemy');
enemyObjects?.forEach(obj => {
const props = this.parseProperties(obj.properties);
this.spawnEnemy(obj.x!, obj.y!, props.health ?? 100);
});
const triggers = this.map.filterObjects('Objects', obj => obj.type === 'Trigger');
triggers?.forEach(obj => {
const zone = this.add.zone(obj.x! + obj.width! / 2, obj.y! + obj.height! / 2, obj.width!, obj.height!);
this.physics.world.enable(zone);
this.physics.add.overlap(this.player, zone, () => {
console.log(`Entered trigger: ${obj.name}`);
});
});
}
private parseProperties(props?: { name: string; value: unknown }[]): Record<string, unknown> {
if (!props) return {};
return Object.fromEntries(props.map(p => [p.name, p.value]));
}
Camera and World Bounds
Always set world bounds to the map size so the player cannot walk out of the map:
create(): void {
this.physics.world.setBounds(0, 0, this.map.widthInPixels, this.map.heightInPixels);
this.cameras.main.setBounds(0, 0, this.map.widthInPixels, this.map.heightInPixels);
this.cameras.main.startFollow(this.player, true, 0.1, 0.1);
}
Dynamic Tile Manipulation
Modify tiles at runtime (destructible terrain, switches, etc.):
const tile = this.groundLayer.getTileAtWorldXY(ptr.worldX, ptr.worldY);
if (tile) {
console.log(`Tile index: ${tile.index}`);
}
this.groundLayer.putTileAtWorldXY(5, ptr.worldX, ptr.worldY);
this.groundLayer.removeTileAtWorldXY(ptr.worldX, ptr.worldY);
const tileXY = this.groundLayer.worldToTileXY(worldX, worldY)!;
const worldXY = this.groundLayer.tileToWorldXY(tileXY.x, tileXY.y)!;
Parallax Layers
Scroll a layer at a different speed than the camera to create depth:
create(): void {
const cloudLayer = this.map.createLayer('Clouds', tileset!, 0, 0);
cloudLayer!.setScrollFactor(0.2);
const hillLayer = this.map.createLayer('Hills', tileset!, 0, 0);
hillLayer!.setScrollFactor(0.5);
const groundLayer = this.map.createLayer('Ground', tileset!, 0, 0);
groundLayer!.setScrollFactor(1);
}
Multiple Tilesets
A single layer can use tiles from multiple tilesets:
create(): void {
const tiles1 = this.map.addTilesetImage('tileset-a', 'tiles-a');
const tiles2 = this.map.addTilesetImage('tileset-b', 'tiles-b');
const layer = this.map.createLayer('Ground', [tiles1!, tiles2!], 0, 0);
}
Additional Resources
Reference Files
references/tilemap-api.md — Complete API reference for Tilemap, TilemapLayer, Tileset, and MapData
references/tiled-workflow.md — Detailed step-by-step Tiled Editor setup guide with common mistakes