关于 phaser-tilemap
phaser-tilemap 是用于在 Phaser 4 中使用 Tiled JSON 地图的技能,涵盖地图制作、图层与对象配置、碰撞处理及运行时操作。
主要功能
- 01Tiled JSON 地图工作流
- 02瓦片图层与对象配置
- 03按属性或索引设置碰撞
- 04设置摄像机和物理边界
- 05运行时瓦片编辑与视差
适用场景
- 01为 Phaser 4 创建 Tiled 地图
- 02配置可行走和危险图层
- 03加载出生点与敌人对象
- 04调试瓦片碰撞
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ yakoub-ai/ phaser4-gamedev --skill phaser-tilemap安装后在 Claude Code 中输入/phaser-tilemap确认已加载。
附带文件references/tiled-workflow.mdreferences/tilemap-api.md
详细介绍
Phaser 4 Tilemaps
Phaser 4 原生支持 Tiled 地图文件(JSON 格式)。推荐工作流:在 Tiled 中设计 → 导出 JSON → 在 Phaser 中加载 → 创建图层 → 设置碰撞。
Tiled 编辑器工作流
在 mapeditor.org 免费下载 Tiled。
创建新地图
- File → New → New Map
- 将 Orientation 设为 Orthogonal(适用于大多数 2D 游戏)或 Isometric(适用于俯视角 RPG/策略游戏)
- 将 Tile layer format 设为 CSV 或 Base64 — 两种格式都适用于 Phaser
- 设置 Tile size,与图块集保持一致(常见尺寸:16x16、32x32)
- 设置 Map size(以图块为单位),例如:对于使用 32px 图块的 1280x736 地图,设置为宽 40 × 高 23
添加图块集
- 在 Tilesets 面板(右下方)中,点击 + 按钮 → New Tileset
- 设置 Name — 这个确切名称会用作
map.addTilesetImage()的第一个参数 - 将 Type 设为 Based on Tileset Image
- 选择 PNG,并设置与之匹配的图块宽度和高度
- Margin 和 spacing:如果图块集中的图块之间有填充,请设置这两个值(通常为 0)
标记碰撞图块
- 在 Tilesets 面板中选择图块集
- 点击扳手图标(Edit Tileset),打开图块集编辑器
- 选择需要发生碰撞的图块(点击即可选择,Ctrl+点击可多选)
- 在 Properties 面板(左侧)中,点击 + 按钮添加属性
- 将 Name 设为
collides,将 Type 设为bool,将 Value 设为true - 关闭图块集编辑器
在 Phaser 中,调用 layer.setCollisionByProperty({ collides: true }) 以启用这些图块的碰撞。
图层命名约定
使用一致的图层名称 — Phaser 通过字符串引用图层:
| 图层名称 | 类型 | 用途 |
|---|---|---|
Background | Tile Layer | 天空、远景 — 不启用碰撞 |
Ground | Tile Layer | 主要可行走表面 — 启用碰撞 |
Hazards | Tile Layer | 尖刺、熔岩 — 使用重叠检测(而非碰撞) |
Foreground | Tile Layer | 树木、拱门等渲染在玩家前方的元素 |
Objects | Object Layer | 生成点、触发器、敌人 |
对象图层用法
Tiled 中的对象图层允许放置带名称的点、矩形和多边形,供 Phaser 查询。
命名对象(用于玩家生成点等独特对象):
- 添加名为
Objects的 Object Layer - 选择 Rectangle 工具,在地图上放置一个对象
- 在 Properties 中,将 Name 设为
PlayerSpawn
类型化对象(用于敌人等同类对象组):
- 放置对象,并将 Type(Tiled 1.8 中使用 Class)设为
Enemy - 添加自定义属性:点击 +,添加
health(int,100)和patrol(bool,true)
导出设置
- File → Export As → JSON Map Files (
.json) - 在导出选项中,启用 Embed tilesets — 这样可以避免依赖外部
.tsx文件 - 保存到
public/assets/tilemaps/level1.json - 将图块集 PNG 放到
public/assets/images/terrain.png
加载资源
preload(): void {
// Key must match first arg of map.addTilesetImage()
this.load.tilemapTiledJSON('level1', 'assets/tilemaps/level1.json');
// Key must match second arg of map.addTilesetImage()
this.load.image('terrain', 'assets/images/terrain.png');
}
创建地图和图层
private map!: Phaser.Tilemaps.Tilemap;
private groundLayer!: Phaser.Tilemaps.TilemapLayer;
create(): void {
this.map = this.make.tilemap({ key: 'level1' });
// First arg: tileset name as set in Tiled (must match exactly, case-sensitive)
// Second arg: the this.load.image() key
const tileset = this.map.addTilesetImage('terrain', 'terrain');
// Decorative background — no collision
const bgLayer = this.map.createLayer('Background', tileset!, 0, 0);
// Main ground layer — collision enabled below
this.groundLayer = this.map.createLayer('Ground', tileset!, 0, 0)!;
// Foreground renders above the player
const fgLayer = this.map.createLayer('Foreground', tileset!, 0, 0);
fgLayer!.setDepth(10); // player depth should be 1–9
}
图块地图不显示的常见原因: addTilesetImage 中的图块集名称与 Tiled 中设置的名称不完全一致。打开 JSON 文件,检查 "tilesets" 中的 "name" 字段。
碰撞
按属性设置(推荐)
使用在 Tiled 图块集编辑器中设置的 collides: true 属性:
this.groundLayer.setCollisionByProperty({ collides: true });
this.physics.add.collider(this.player, this.groundLayer);
按图块索引范围设置
对 GID(全局 ID)为 1 到 10 的图块启用碰撞:
this.groundLayer.setCollisionBetween(1, 10);
按排除项设置
对除空图块(-1)和指定索引以外的所有图块启用碰撞:
this.groundLayer.setCollisionByExclusion([-1, 0]);
调试渲染
在开发过程中可视化碰撞图块:
const debugGraphics = this.add.graphics();
this.groundLayer.renderDebug(debugGraphics, {
tileColor: null, // non-colliding tiles
collidingTileColor: new Phaser.Display.Color(243, 134, 48, 128), // orange
faceColor: new Phaser.Display.Color(40, 39, 37, 255), // face outlines
});
多个图层
create(): void {
const tileset = this.map.addTilesetImage('terrain', 'terrain')!;
const bgLayer = this.map.createLayer('Background', tileset, 0, 0); // depth 0 (default)
const groundLayer = this.map.createLayer('Ground', tileset, 0, 0)!; // depth 0 (default)
const fgLayer = this.map.createLayer('Foreground', tileset, 0, 0);
fgLayer!.setDepth(10); // renders above player
groundLayer.setCollisionByProperty({ collides: true });
// Player and enemies should have depth between 1 and 9
this.player.setDepth(5);
}
对象图层
从 Tiled 的对象图层读取生成点、触发器和实体位置:
create(): void {
// Find a single named object — returns the first match
const spawnPoint = this.map.findObject('Objects', obj => obj.name === 'PlayerSpawn');
this.player = this.physics.add.sprite(spawnPoint!.x!, spawnPoint!.y!, 'player');
// Get all objects of a given type (Tiled "Class" field in 1.8+)
const enemyObjects = this.map.filterObjects('Objects', obj => obj.type === 'Enemy');
enemyObjects?.forEach(obj => {
// Access custom properties as an array: obj.properties
const props = this.parseProperties(obj.properties);
this.spawnEnemy(obj.x!, obj.y!, props.health ?? 100);
});
// Find trigger zones (rectangles placed in Tiled)
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}`);
});
});
}
// Helper: convert Tiled properties array to plain object
private parseProperties(props?: { name: string; value: unknown }[]): Record<string, unknown> {
if (!props) return {};
return Object.fromEntries(props.map(p => [p.name, p.value]));
}
摄像机和世界边界
始终将世界边界设为地图尺寸,以防玩家走出地图:
create(): void {
// Constrain physics bodies
this.physics.world.setBounds(0, 0, this.map.widthInPixels, this.map.heightInPixels);
// Constrain camera
this.cameras.main.setBounds(0, 0, this.map.widthInPixels, this.map.heightInPixels);
// Follow the player
this.cameras.main.startFollow(this.player, true, 0.1, 0.1); // lerp x/y = 0.1 for smooth follow
}
动态修改图块
在运行时修改图块(可破坏地形、开关等):
// Read a tile at a world position
const tile = this.groundLayer.getTileAtWorldXY(ptr.worldX, ptr.worldY);
if (tile) {
console.log(`Tile index: ${tile.index}`);
}
// Place a tile at a world position
this.groundLayer.putTileAtWorldXY(5, ptr.worldX, ptr.worldY);
// Remove a tile (sets index to -1 / empty)
this.groundLayer.removeTileAtWorldXY(ptr.worldX, ptr.worldY);
// Convert between world and tile coordinates
const tileXY = this.groundLayer.worldToTileXY(worldX, worldY)!;
const worldXY = this.groundLayer.tileToWorldXY(tileXY.x, tileXY.y)!;
视差图层
让图层以不同于摄像机的速度滚动,以营造景深效果:
create(): void {
const cloudLayer = this.map.createLayer('Clouds', tileset!, 0, 0);
cloudLayer!.setScrollFactor(0.2); // moves at 20% camera speed (furthest back)
const hillLayer = this.map.createLayer('Hills', tileset!, 0, 0);
hillLayer!.setScrollFactor(0.5); // moves at 50% camera speed
// Ground layer
const groundLayer = this.map.createLayer('Ground', tileset!, 0, 0);
groundLayer!.setScrollFactor(1); // moves at 100% (default)
}
多个图块集
单个图层可以使用来自多个图块集的图块:
create(): void {
const tiles1 = this.map.addTilesetImage('tileset-a', 'tiles-a');
const tiles2 = this.map.addTilesetImage('tileset-b', 'tiles-b');
// Pass an array of tilesets — Phaser resolves tile GIDs automatically
const layer = this.map.createLayer('Ground', [tiles1!, tiles2!], 0, 0);
}
其他资源
参考文件
references/tilemap-api.md— Tilemap、TilemapLayer、Tileset 和 MapData 的完整 API 参考references/tiled-workflow.md— Tiled Editor 设置的详细分步指南,包含常见错误