mcpField
P
phaser-tilemap

phaser-tilemap

github.com/yakoub-ai开发29194 次安装9 周前更新
代码仓库

关于 phaser-tilemap

phaser-tilemap 是用于在 Phaser 4 中使用 Tiled JSON 地图的技能,涵盖地图制作、图层与对象配置、碰撞处理及运行时操作。

主要功能

  1. 01Tiled JSON 地图工作流
  2. 02瓦片图层与对象配置
  3. 03按属性或索引设置碰撞
  4. 04设置摄像机和物理边界
  5. 05运行时瓦片编辑与视差

适用场景

  1. 01为 Phaser 4 创建 Tiled 地图
  2. 02配置可行走和危险图层
  3. 03加载出生点与敌人对象
  4. 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。

创建新地图
  1. File → New → New Map
  2. 将 Orientation 设为 Orthogonal(适用于大多数 2D 游戏)或 Isometric(适用于俯视角 RPG/策略游戏)
  3. 将 Tile layer format 设为 CSV 或 Base64 — 两种格式都适用于 Phaser
  4. 设置 Tile size,与图块集保持一致(常见尺寸:16x16、32x32)
  5. 设置 Map size(以图块为单位),例如:对于使用 32px 图块的 1280x736 地图,设置为宽 40 × 高 23
添加图块集
  1. 在 Tilesets 面板(右下方)中,点击 + 按钮 → New Tileset
  2. 设置 Name — 这个确切名称会用作 map.addTilesetImage() 的第一个参数
  3. 将 Type 设为 Based on Tileset Image
  4. 选择 PNG,并设置与之匹配的图块宽度和高度
  5. Margin 和 spacing:如果图块集中的图块之间有填充,请设置这两个值(通常为 0)
标记碰撞图块
  1. 在 Tilesets 面板中选择图块集
  2. 点击扳手图标(Edit Tileset),打开图块集编辑器
  3. 选择需要发生碰撞的图块(点击即可选择,Ctrl+点击可多选)
  4. 在 Properties 面板(左侧)中,点击 + 按钮添加属性
  5. 将 Name 设为 collides,将 Type 设为 bool,将 Value 设为 true
  6. 关闭图块集编辑器

在 Phaser 中,调用 layer.setCollisionByProperty({ collides: true }) 以启用这些图块的碰撞。

图层命名约定

使用一致的图层名称 — Phaser 通过字符串引用图层:

图层名称类型用途
BackgroundTile Layer天空、远景 — 不启用碰撞
GroundTile Layer主要可行走表面 — 启用碰撞
HazardsTile Layer尖刺、熔岩 — 使用重叠检测(而非碰撞)
ForegroundTile Layer树木、拱门等渲染在玩家前方的元素
ObjectsObject Layer生成点、触发器、敌人
对象图层用法

Tiled 中的对象图层允许放置带名称的点、矩形和多边形,供 Phaser 查询。

命名对象(用于玩家生成点等独特对象):

  1. 添加名为 Objects 的 Object Layer
  2. 选择 Rectangle 工具,在地图上放置一个对象
  3. 在 Properties 中,将 Name 设为 PlayerSpawn

类型化对象(用于敌人等同类对象组):

  1. 放置对象,并将 Type(Tiled 1.8 中使用 Class)设为 Enemy
  2. 添加自定义属性:点击 +,添加 health(int,100)和 patrol(bool,true)
导出设置
  1. File → Export As → JSON Map Files (.json)
  2. 在导出选项中,启用 Embed tilesets — 这样可以避免依赖外部 .tsx 文件
  3. 保存到 public/assets/tilemaps/level1.json
  4. 将图块集 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 设置的详细分步指南,包含常见错误

评论

支持 Markdown