tilemap phaserの使い方|古いcreateStaticLayerに注意!Tiled連携と壁判定を徹底解説

記事内に広告が含まれています。

Phaser 4でタイルマップを使う前に知る基礎知識

「Phaserでゲームのステージを作りたいけど、タイルマップって何から始めればいいの?」——そう感じているWebエンジニアの方は多いはずです。ReactやTypeScriptでの開発経験があっても、ゲーム開発特有の概念が壁になることはよくあります。

このセクションでは、Phaser 4でタイルマップを扱う上で絶対に外せない基礎知識を丁寧に解説します。ここを押さえておくと、後続の実装手順が驚くほどスムーズに理解できます。

必ず押さえるべき4つの重要用語:Tile・Tileset・Layer・Collision

タイルマップの世界には、最初に必ず覚えるべき4つの用語があります。この4つを混同していると、Phaserのドキュメントを読んでいても何の話をしているのか分からなくなります。

① Tile(タイル)

マップを構成する最小単位の正方形画像です。たとえば「草地」「土」「岩」などのグラフィックがそれぞれ1枚のタイルにあたります。よく使われるサイズは16×16px、32×32px、48×48pxです。Phaserではこのタイル1枚ずつに対してインデックス番号(0始まりまたは1始まり)が割り振られており、後述の衝突判定などはこのインデックスを元に操作します。

② Tileset(タイルセット)

複数のタイルを1枚の画像ファイルにまとめたものです。スプライトシートに近いイメージで、「tileset.png」のような形で用意します。PhaserはこのTileset画像を内部でグリッド状に分割し、各タイルをインデックスで参照します。TiledというマップエディタとPhaserの両方に「Tileset」という概念があり、後の手順で両者を正確に紐付ける必要があります。

③ Layer(レイヤー)

タイルを重ねて配置するための層です。PhotoshopやFigmaのレイヤーと同じ考え方で、「背景レイヤー(空・地面)」「障害物レイヤー(壁・ブロック)」「前景レイヤー(草や木のかぶり部分)」のように役割ごとに分けて管理します。Phaserではレイヤーごとに表示・非表示の切り替えや、当たり判定の有効・無効を独立して制御できます。

④ Collision(コリジョン/衝突判定)

プレイヤーや敵がタイルをすり抜けないようにする仕組みです。「このタイルは壁なので通れない」という情報を各タイルに持たせ、Phaserの物理エンジンと組み合わせることで、キャラクターがマップ上を自然に歩けるようになります。PhaserではsetCollisionByPropertysetCollisionBetweenといったメソッドでコリジョンを設定します。

整理すると: Tileset(素材集)の中にある Tile(1枚の部品)を Layer(層)に敷き詰め、特定のTileにCollision(壁属性)を付けるのがタイルマップの全体像です。

古い記事に注意!Phaser 3.50以降(およびPhaser 4)のTilemap APIの変更点

Phaser 4は内部アーキテクチャ(WebGPUファーストなど)が大きく刷新された最新メジャーバージョンですが、Tilemapの実装において初心者が最もハマりやすいのは、実はPhaser 3.50.0の段階で行われたAPIの統合です。ネット上にある数年前の日本語記事を参考にする際は、以下の変更点に非常に注意する必要があります。

最も大きな罠はcreateStaticLayercreateDynamicLayerの廃止です。

Phaser 3の初期バージョンでは「描画後に変更しない静的なレイヤー(背景など)」と「実行中にタイルを書き換える動的なレイヤー」を別メソッドで生成していましたが、Phaser 3.50以降(当然現在のPhaser 4も含む)ではこの区別がなくなり、createLayer()に統合されました。内部的な最適化は自動で行われるため、開発者が静的・動的を意識して書き分ける必要はありません。

古い記事やStack Overflowの回答でcreateStaticLayerを見かけたら、それは過去の仕様向けのコードです。現在の環境ではcreateLayerに読み替えてください。

// ❌ 古い仕様の書き方(現在の環境では動作しない)
const groundLayer = map.createStaticLayer('Ground', tileset, 0, 0);

// ✅ Phaser 3.50以降 & Phaser 4の正しい書き方
const groundLayer = map.createLayer('Ground', tileset, 0, 0);

配列管理との違い|なぜTilemapを使うべきなのか

「PhaserのImageSpriteを2D配列で並べればマップは作れるのでは?」と思う方もいるかもしれません。実際、小さなプロトタイプではその方法でも動きます。しかし、ゲームの規模が少しでも大きくなった瞬間に、配列管理は深刻な問題を引き起こします。

配列管理の問題点

  • メモリ効率が悪い:100×100マスのマップをSpriteで作ると、10,000個のゲームオブジェクトが生成されます。これはブラウザゲームとして非現実的なメモリ消費です。
  • 衝突判定の実装が複雑:どのセルが壁でどのセルが通路かを自前で管理し、衝突チェックを毎フレーム計算する必要があります。
  • マップ編集が困難:コード上の2D配列を手書きで編集するのは、少し複雑なマップになった途端に現実的ではなくなります。

Tilemapを使う利点

  • 描画の最適化が自動:Phaserのタイルマップレンダラーは、画面内に見えているタイルのみを描画します(カリング)。10,000マスのマップでも、実際に描画されるのは画面内の数百タイルだけです。
  • 当たり判定が1行で設定可能setCollisionByPropertyを呼ぶだけで、Tiledで「collision=true」と設定したタイルすべてに衝突判定が有効になります。
  • Tiledとの連携でビジュアル編集が可能:プログラマー以外のチームメンバーがGUIツールでマップを編集でき、エクスポートしたJSONをPhaserが直接読み込めます。
  • カメラとの連携が容易tilemap.setPosition()やカメラのfollowを設定するだけで、スクロールするワールドマップが実現できます。

結論として: タイルが20枚を超えるような本格的なステージを作るなら、最初からTilemapを使うべきです。配列管理は「ちょっとした動作確認」以上には向きません。

次のセクションでは、実際にTiled Map Editorをインストールし、Phaserが読み込めるタイルマップを作成する手順を解説します。

Tiled Map Editorの使い方|Phaser用タイルマップを作成する手順

基礎知識が整ったところで、いよいよ実際にマップを作っていきましょう。このセクションではTiled Map Editor(以下、Tiled)を使って、Phaserが読み込めるタイルマップファイルを作成する手順を、画面操作レベルで丁寧に解説します。

「GUIツールの操作説明は読み飛ばしがち」という方も、ここで紹介するレイヤーの分け方エクスポート形式の選択は後の実装に直結するため、ぜひ一度通読してください。

Tiledのインストールとグリッド・Layer・Tilesetの初期設定

Tiledのインストール

Tiledは無料で使えるオープンソースのタイルマップエディタです。公式サイト(mapeditor.org)からお使いのOS向けのインストーラーをダウンロードしてください。

  • Windows.msiインストーラーを実行
  • macOS.dmgを開いてApplicationsフォルダへドラッグ
  • Linux:Snap / Flatpak、またはAPTで導入可能

本記事執筆時点の最新安定版はTiled 1.11.xです。バージョンによってUI配置が多少異なる場合がありますが、基本操作は同じです。

TILEDキャプション

新規マップの作成と初期設定

インストール後、Tiledを起動して「新しいマップ」を作成します。メニューから ファイル → 新規 → 新しいマップ を選択すると、以下のダイアログが表示されます。

各設定値の意味と推奨値は次のとおりです。

設定項目推奨値説明
方向直交(Orthogonal)見下ろし型・横スクロール型ゲームに使用
タイルレイヤー形式CSVJSONエクスポート時に人間が読みやすい形式
タイルのレンダリング順序右下Phaserのデフォルトと一致
マップサイズ(幅×高さ)20×15タイルまず小さめに始めるのがおすすめ
タイルサイズ(幅×高さ)32×32pxPhaserとの相性が良く、素材も豊富

設定が完了したら「OK」をクリックします。グレーのグリッドが表示されたキャンバスが出現すれば成功です。

Tilesetの読み込み

次に、マップに使う素材画像(Tileset)を登録します。

  1. 右下のパネルにある「新しいタイルセット」ボタン(+アイコン)をクリック
  2. 「タイルセットに基づく」を選択し、「参照」から素材画像ファイルを選択(例:tileset.png
  3. タイルサイズをマップと同じ32×32pxに設定(ここがずれると表示が崩れます)
  4. 「タイルセットを埋め込む」のチェックを外した状態で保存する(理由は後述)
  5. 名前を入力(例:tileset)して「OK」をクリック

右下パネルにタイルが一覧表示されれば、Tilesetの登録は完了です。

⚠️ 注意:「タイルセットを埋め込む」について このオプションをオンにすると、Tilesetの情報がマップファイル(.tmj)に直接書き込まれます。一見便利に見えますが、Phaserから読み込む際にTilesetの画像パスが解決できなくなるケースがあります。後述する外部.tsj形式で管理する方が安全です。

背景・障害物・前景レイヤーを分ける方法

Tiledでマップを作る際、レイヤーを適切に分けることが最重要ポイントです。後からPhaserで「この層だけ当たり判定を有効にする」「この層はプレイヤーより手前に描画する」といった制御をレイヤー単位で行うからです。

推奨レイヤー構成

実際のゲーム開発でよく使われる3層構成を紹介します。

レイヤー(上から描画順)
├── Foreground   ← 前景(木の葉、屋根など。プレイヤーの前面に表示)
├── Collision    ← 障害物(壁、床、ブロック。当たり判定を付けるレイヤー)
└── Background   ← 背景(地面、空など。当たり判定なし)

Background(背景レイヤー)

地面や空など、プレイヤーが乗る面より後ろの装飾タイルを置きます。当たり判定は不要です。

Collision(障害物レイヤー)

壁・床・ブロックなど、プレイヤーが衝突すべきタイルだけを配置します。PhaserでこのレイヤーのみにsetCollisionByPropertyを適用するため、当たり判定を持たせたいタイルを他のレイヤーと混在させないことが重要です。

Foreground(前景レイヤー)

木の葉や屋根のひさしなど、プレイヤーの前面に重なって表示したいタイルを置きます。PhaserでsetDepth()の値をプレイヤーより大きくすることで、前後関係を表現できます。

Tiledでのレイヤー操作手順

  1. 右下の「レイヤー」パネルの下部にある「新しいタイルレイヤー」ボタン(+アイコン)をクリック
  2. レイヤー名を入力(BackgroundCollisionForeground
  3. レイヤーの順序はパネル上で上にあるものほど画面手前に描画される。ドラッグで並び替え可能
  4. 編集したいレイヤーをクリックして選択状態にしてからタイルを配置する(レイヤーの選択ミスは最もよくある失敗です)

CollisionレイヤーのTileにプロパティを付与する

PhaserでsetCollisionByPropertyを使う場合、TiledのTilesetエディタ側で各タイルにカスタムプロパティを設定する必要があります。

  1. メニューから タイルセット → タイルセットを編集 を開く
  2. 衝突させたいタイルを選択(壁・床など)
  3. 左下の「プロパティ」パネルで「+」をクリックし、以下を追加:
    • 名前collides
    • bool
    • true

このプロパティがJSONにエクスポートされ、Phaserで次のように1行で衝突設定が完了します。

// "collides: true" プロパティを持つタイルすべてに衝突判定を付与
collisionLayer.setCollisionByProperty({ collides: true });

Phaserに読み込める形式はどれ?JSON・TMX・TSXの違い

Tiledはいくつかの形式でマップをエクスポートできますが、Phaserとの連携においてどの形式を選ぶべきかは明確です。それぞれの違いを整理しておきましょう。

各形式の特徴

TMX(Tiled Map XML)

Tiledのデフォルト保存形式です。XML構造でマップデータを保持します。PhaserはtilemapTiledJSONローダーを使うため、TMXを直接読み込むことはできません。作業中の中間ファイルとして保存しておき、最終的にJSONへエクスポートして使います。

TSX(Tileset XML)

TilesetをTMXとは別ファイルで管理する際の形式です。TMXと同様にXMLベースで、Phaserへは直接渡しません。

JSON / TMJ(Tiled Map JSON)

Phaserが対応している形式です。エクスポート時に選ぶべきはこれ一択です。

Tiled 1.9以降、デフォルトの保存形式が.tmxから.tmj(JSON形式)と.tsj(Tileset JSON)に変更されました。拡張子は違いますが、中身は通常のJSONです。Phaserのローダーはどちらも問題なく読み込めます。

エクスポート手順

  1. メニューから ファイル → 名前を付けてエクスポート を選択
  2. ファイル形式で「JSON map files (*.tmj *.json)」を選択
  3. ファイル名を入力(例:map.tmj)し、プロジェクトのpublic/assets/ディレクトリなどに保存

プロジェクトのファイル構成例

my-phaser-game/
├── public/
│   └── assets/
│       ├── map.tmj          ← Tiledからエクスポートしたマップファイル
│       ├── tileset.tsj      ← Tilesetの定義ファイル(外部管理の場合)
│       └── tileset.png      ← Tileset画像
└── src/
    └── scenes/
        └── GameScene.ts

ポイント: tileset.pngmap.tmjから相対パスで参照されています。ファイルを移動する際は両者の相対関係を保つようにしてください。パスがずれるとPhaserがTileset画像を読み込めず、マップが真っ黒になります。

以上でTiledによるマップ作成の準備が整いました。次のセクションでは、このJSONファイルをPhaserで実際に読み込み、画面に表示して当たり判定を実装するコードを書いていきます。

格安ドメイン取得サービス─ムームードメイン─

tiledjson読み込みから表示・衝突判定まで

いよいよPhaserのコードを書いていきます。このセクションでは「Tiledで作ったマップをPhaserで読み込み、プレイヤーが壁にぶつかって止まる」というゲームとして最低限機能するステージを、TypeScriptで一から実装します。

コードはすべてコピペして動かせる形で提供します。まず動かしてみて、その後コメントを読みながら理解を深めるという進め方が、実践派の方には最もおすすめです。

無料tileset素材の導入とtiledjsonエクスポート手順

無料素材の入手先

自前でTileset画像を用意するのは初学者には大変です。まずは以下の無料素材を使って実装を進めましょう。

  • Kenney.nl:ゲーム素材の定番サイト。商用利用無料でライセンスも明快(CC0)。「Platformer Pack」や「Tilemap Pack」が特に使いやすい。
  • itch.io(無料フィルター):多様なスタイルの素材が揃っており、フィルターで無料のみ表示可能。

本記事ではKenney.nlの「Pixel Platformer」を使用する前提でコードを書きます。ダウンロード後、zipを解凍してtilemap_packed.png(タイル1枚あたり18×18px)を使用します。

ファイル配置

my-phaser-game/
├── public/
│   └── assets/
│       ├── map.tmj          ← TiledからエクスポートしたJSONマップ
│       └── tilemap_packed.png  ← Kenney素材のTileset画像
└── src/
    └── scenes/
        └── GameScene.ts

Tiledでのエクスポート最終確認チェックリスト

エクスポート前に以下を必ず確認してください。後で「なぜか読み込めない」という問題の9割はここに起因します。

  • レイヤー名にスペルミスがない(BackgroundCollisionForeground
  • 衝突させたいタイルにcollides: trueプロパティが付与されている
  • Tilesetのタイルサイズがマップのタイルサイズと一致している
  • エクスポート形式が**JSON(.tmjまたは.json)**になっている
  • map.tmjtilemap_packed.png同じディレクトリに配置されている

tileset画像の紐付けと古いメソッド(createStaticLayer等)の注意点

前セクションで説明したとおり、現在のPhaser(v3.50以降およびPhaser 4)ではcreateStaticLayercreateDynamicLayerというメソッド自体が廃止されています。ここからは、古い情報に惑わされず、最新の正しい書き方でTilemapを読み込む実装全体を見ていきます。

プロジェクトのセットアップ

まずPhaser 4をインストールします。

npm create vite@latest my-phaser-game -- --template vanilla-ts
cd my-phaser-game
npm install phaser@latest

main.ts:Phaserゲームの起動設定

// src/main.ts
import Phaser from 'phaser';
import { GameScene } from './scenes/GameScene';

const config: Phaser.Types.Core.GameConfig = {
  type: Phaser.AUTO,          // WebGPU → WebGL → Canvasの順で自動選択
  width: 640,
  height: 480,
  backgroundColor: '#1a1a2e',
  physics: {
    default: 'arcade',        // タイルマップとの衝突判定にはArcadePhysicsを使用
    arcade: {
      gravity: { x: 0, y: 600 },  // 重力(横スクロールアクションの場合)
      debug: true             // 開発中はtrueにして当たり判定を可視化
    }
  },
  scene: [GameScene]
};

new Phaser.Game(config);

GameScene.ts:マップの読み込みと表示

// src/scenes/GameScene.ts
import Phaser from 'phaser';

export class GameScene extends Phaser.Scene {
  // レイヤーとプレイヤーをクラスフィールドとして宣言
  private player!: Phaser.Physics.Arcade.Sprite;
  private collisionLayer!: Phaser.Tilemaps.TilemapLayer;

  constructor() {
    super({ key: 'GameScene' });
  }

  // -------------------------------------------------------
  // preload:アセットの読み込み
  // -------------------------------------------------------
  preload(): void {
    // タイルマップのJSONを読み込む
    // 第1引数のキー名は後でcreateで参照するため一致させること
    this.load.tilemapTiledJSON('map', 'assets/map.tmj');

    // Tileset画像を読み込む
    // 第3・4引数はTilesetの1タイルあたりの幅・高さ(px)
    this.load.spritesheet('tileset', 'assets/tilemap_packed.png', {
      frameWidth: 18,
      frameHeight: 18
    });

    // プレイヤー用スプライト(仮素材)
    this.load.spritesheet('player', 'assets/player.png', {
      frameWidth: 24,
      frameHeight: 24
    });
  }

  // -------------------------------------------------------
  // create:ゲームオブジェクトの生成
  // -------------------------------------------------------
  create(): void {
    // ① Tilemapオブジェクトを生成
    // preloadで指定したキー名'map'を渡す
    const map = this.make.tilemap({ key: 'map' });

    // ② TilesetをTilemapに紐付ける
    // 第1引数:Tiled上でのTileset名(Tiledの「タイルセット」パネルに表示されている名前)
    // 第2引数:preloadで読み込んだ画像のキー名
    // ※ 第1引数がTiled上の名前と一致しないと、タイルが表示されないので注意
    const tileset = map.addTilesetImage('tileset', 'tileset');

    if (!tileset) {
      console.error('Tilesetの読み込みに失敗しました。Tiled上のTileset名を確認してください。');
      return;
    }

    // ③ レイヤーを生成(Phaser 4では createLayer() に統一)
    // 第1引数:Tiledのレイヤー名(大文字・小文字を含め完全一致が必要)
    // 第2引数:紐付けるTilesetオブジェクト
    // 第3・4引数:マップの描画開始座標(通常は0, 0)
    const backgroundLayer = map.createLayer('Background', tileset, 0, 0);
    const collisionLayer  = map.createLayer('Collision',  tileset, 0, 0);
    const foregroundLayer = map.createLayer('Foreground', tileset, 0, 0);

    if (!backgroundLayer || !collisionLayer || !foregroundLayer) {
      console.error('レイヤーの生成に失敗しました。Tiledのレイヤー名を確認してください。');
      return;
    }

    // ④ 前景レイヤーをプレイヤーより手前に表示するため depth を設定
    // プレイヤーのdepthを1とした場合、前景は2以上にする
    foregroundLayer.setDepth(2);

    // ⑤ プレイヤーをクラスフィールドに保持
    this.collisionLayer = collisionLayer;

    // ⑥ プレイヤーを生成(詳細は次の節で解説)
    this.createPlayer(map);

    // ⑦ カメラをプレイヤーに追従させる
    this.cameras.main.startFollow(this.player);
    this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels);
  }

  // -------------------------------------------------------
  // プレイヤー生成のヘルパーメソッド
  // -------------------------------------------------------
  private createPlayer(map: Phaser.Tilemaps.Tilemap): void {
    // マップのスポーンポイントからプレイヤーの初期位置を取得する場合:
    // const spawnPoint = map.findObject('Objects', obj => obj.name === 'Spawn');
    // 今回はシンプルに固定座標で配置
    this.player = this.physics.add.sprite(100, 100, 'player');
    this.player.setDepth(1);

    // プレイヤーがマップ外に出ないよう物理ボディの境界を設定
    this.player.setCollideWorldBounds(true);
    this.physics.world.setBounds(
      0, 0,
      map.widthInPixels,
      map.heightInPixels
    );
  }

  // -------------------------------------------------------
  // update:毎フレーム呼ばれる処理
  // -------------------------------------------------------
  update(): void {
    // カーソルキーの入力を取得
    const cursors = this.input.keyboard!.createCursorKeys();

    // 左右移動
    if (cursors.left.isDown) {
      this.player.setVelocityX(-200);
    } else if (cursors.right.isDown) {
      this.player.setVelocityX(200);
    } else {
      // キー入力がなければ水平速度を0に(慣性なし)
      this.player.setVelocityX(0);
    }

    // ジャンプ(地面に接地しているときだけ)
    if (cursors.up.isDown && this.player.body!.blocked.down) {
      this.player.setVelocityY(-500);
    }
  }
}

Phaser 3との対比まとめ

処理Phaser 3Phaser 4
Tilemap生成this.make.tilemap()this.make.tilemap()(同様)
Tileset紐付けmap.addTilesetImage()map.addTilesetImage()(同様)
静的レイヤー生成map.createStaticLayer()map.createLayer()
動的レイヤー生成map.createDynamicLayer()map.createLayer()

壁にぶつかるマップを作る|setCollisionByPropertyで「通れる/通れない」タイルを設定しプレイヤーを動かす

マップが表示されても、このままではプレイヤーがタイルをすり抜けてしまいます。最後のステップとして、衝突判定を有効にしてプレイヤーがマップ上を歩けるようにする実装を加えます。

衝突判定の実装

先ほどのGameScene.tscreate()メソッドに、以下のコードを追加します。

create(): void {
  // ...(前述のコードに続けて追記)

  // ① Tiledで設定した "collides: true" プロパティを持つタイルに衝突判定を付与
  // このメソッド1行で、プロパティが付いた全タイルに自動的に設定される
  collisionLayer.setCollisionByProperty({ collides: true });

  // ② プレイヤーとCollisionレイヤーの衝突を物理エンジンに登録
  // addColliderに渡すだけでArcadePhysicsが毎フレーム衝突チェックを行う
  this.physics.add.collider(this.player, collisionLayer);

  // デバッグ時にコリジョン範囲を可視化したい場合(開発中のみ使用)
  // 衝突判定が付いたタイルが水色でハイライトされる
  if (process.env.NODE_ENV === 'development') {
    collisionLayer.renderDebug(this.add.graphics(), {
      tileColor: null,                              // 通常タイルは表示しない
      collidingTileColor: new Phaser.Display.Color(243, 134, 48, 200),  // 衝突タイルをオレンジで表示
      faceColor: new Phaser.Display.Color(40, 39, 37, 255)              // 衝突面を黒で表示
    });
  }
}

setCollisionByProperty以外の衝突設定方法

状況に応じて使い分けられるよう、他の衝突設定メソッドも紹介します。

// 方法①:タイルのカスタムプロパティで設定(推奨)
// Tiledで設定した collides: true のタイルのみ衝突
collisionLayer.setCollisionByProperty({ collides: true });

// 方法②:タイルインデックスの範囲で設定
// インデックス1〜10のタイルすべてに衝突判定を付与
collisionLayer.setCollisionBetween(1, 10);

// 方法③:タイルインデックスを個別に指定
// 特定のタイル(インデックス5と12)だけ衝突させたい場合
collisionLayer.setCollision([5, 12]);

// 方法④:-1以外の全タイルに衝突判定を付与(レイヤー全体を壁にする場合)
// -1はTiledで「何も置いていない空のセル」を意味する
collisionLayer.setCollisionByExclusion([-1]);

実務での推奨は方法①です。タイルインデックスはTiledの編集中に変わることがあり、インデックスベースの設定はメンテナンスが困難になります。プロパティベースにしておくと、Tiled側での変更がコードに影響しません。

完成したGameScene.tsの全体像

ここまでの実装をまとめた、コピペで動く最終版コードです。

// src/scenes/GameScene.ts
import Phaser from 'phaser';

export class GameScene extends Phaser.Scene {
  private player!: Phaser.Physics.Arcade.Sprite;
  private cursors!: Phaser.Types.Input.Keyboard.CursorKeys;

  constructor() {
    super({ key: 'GameScene' });
  }

  preload(): void {
    this.load.tilemapTiledJSON('map', 'assets/map.tmj');
    this.load.spritesheet('tileset', 'assets/tilemap_packed.png', {
      frameWidth: 18,
      frameHeight: 18
    });
    this.load.spritesheet('player', 'assets/player.png', {
      frameWidth: 24,
      frameHeight: 24
    });
  }

  create(): void {
    // ── マップ生成 ──────────────────────────────────
    const map = this.make.tilemap({ key: 'map' });
    const tileset = map.addTilesetImage('tileset', 'tileset');

    if (!tileset) {
      console.error('Tilesetの読み込みに失敗しました');
      return;
    }

    // ── レイヤー生成 ────────────────────────────────
    const backgroundLayer = map.createLayer('Background', tileset, 0, 0)!;
    const collisionLayer  = map.createLayer('Collision',  tileset, 0, 0)!;
    const foregroundLayer = map.createLayer('Foreground', tileset, 0, 0)!;

    // 前景レイヤーをプレイヤーより手前に描画
    foregroundLayer.setDepth(2);

    // ── 衝突判定の設定 ──────────────────────────────
    // Tiledで collides: true を付けたタイルのみ通行不可にする
    collisionLayer.setCollisionByProperty({ collides: true });

    // ── プレイヤー生成 ──────────────────────────────
    this.player = this.physics.add.sprite(100, 300, 'player');
    this.player.setDepth(1);
    this.player.setCollideWorldBounds(true);

    // ── 物理演算:プレイヤーとマップの衝突を登録 ───────
    this.physics.add.collider(this.player, collisionLayer);

    // ── カメラ設定 ──────────────────────────────────
    this.cameras.main.startFollow(this.player);
    this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels);
    this.physics.world.setBounds(0, 0, map.widthInPixels, map.heightInPixels);

    // ── キーボード入力 ──────────────────────────────
    this.cursors = this.input.keyboard!.createCursorKeys();
  }

  update(): void {
    const { left, right, up } = this.cursors;

    // 左右移動
    if (left.isDown) {
      this.player.setVelocityX(-200);
      this.player.setFlipX(true);   // スプライトを左向きに反転
    } else if (right.isDown) {
      this.player.setVelocityX(200);
      this.player.setFlipX(false);
    } else {
      this.player.setVelocityX(0);
    }

    // ジャンプ:地面に接地中(blocked.down)のみ有効
    if (up.isDown && this.player.body!.blocked.down) {
      this.player.setVelocityY(-500);
    }
  }
}

動作確認の手順

npm run dev

ブラウザでhttp://localhost:5173を開き、以下を確認してください。

  • Tiledで作ったマップが表示されている
  • プレイヤーが矢印キーで左右に動く
  • プレイヤーがジャンプできる(↑キー)
  • Collisionレイヤーのタイルでプレイヤーが止まる(すり抜けない)
  • カメラがプレイヤーを追いかける

うまく動かない場合の確認ポイント

  • マップが真っ黒 → addTilesetImageの第1引数がTiled上のTileset名と一致しているか確認
  • プレイヤーがすり抜ける → collides: trueプロパティの付与漏れ、またはレイヤー名のスペルミス
  • コンソールにCannot read properties of nullcreateLayerの戻り値がnullになっている。レイヤー名の完全一致を再確認

以上で「Tiledでマップを作り、Phaserで読み込み、当たり判定をつけてプレイヤーを動かす」という一連の実装が完成しました。次のセクションでは、実装中によく遭遇するトラブルをFAQ形式でまとめます。

よくある質問(FAQ)

実装を進めていると、ドキュメントには載っていない細かいつまずきポイントに必ずぶつかります。このセクションでは、Phaser×Tiledの実装でよく寄せられる質問を厳選してまとめました。


addTilesetImageの第1引数に何を渡せばいいか分からない

Tiledの「タイルセット」パネルに表示されているタイルセット名を渡します。ファイル名ではありません。

たとえばTiled上でタイルセット名がtilesetなら、ファイル名がtilemap_packed.pngであっても第1引数は'tileset'です。名前を確認するにはTiledのメニューからタイルセット → タイルセットを編集を開き、上部に表示されているタイトルを確認してください。

// ✅ Tiled上のタイルセット名が "tileset" の場合
const tileset = map.addTilesetImage('tileset', 'tileset');

// ❌ ファイル名をそのまま渡しても一致しない場合がある
const tileset = map.addTilesetImage('tilemap_packed', 'tileset');

マップは表示されるのにタイルが全部黒くなる(もしくは白い四角になる)

原因はほぼ確実にTilesetとTilemap間のパス解決の失敗です。以下を順番に確認してください。

  • map.tmjtilemap_packed.pngが同じディレクトリに置かれているか
  • preloadで読み込んだ画像のキー名と、addTilesetImageの第2引数が一致しているか
  • addTilesetImageの戻り値がnullでないか(console.log(tileset)で確認)
  • Tiledでエクスポートした.tmjをテキストエディタで開き、"image"フィールドに記載されているパスが実際の画像ファイル名と一致しているか
// map.tmj の中身の一部(この "image" の値とファイル名が一致しているか確認)
{
  "tilesets": [
    {
      "image": "tilemap_packed.png",
      "name": "tileset"
    }
  ]
}

createLayerがnullを返す

Tiledのレイヤー名とcreateLayerの第1引数が完全に一致していないことが原因です。スペース・大文字・小文字の違いまで含めて一致させる必要があります。

// Tiledのレイヤー名が "Collision" の場合
// ❌ スペルや大文字小文字が違うとnullが返る
const layer = map.createLayer('collision', tileset, 0, 0);   // → null
const layer = map.createLayer('Collsion',  tileset, 0, 0);   // → null(タイポ)

// ✅ 完全一致
const layer = map.createLayer('Collision', tileset, 0, 0);

TypeScriptの!(Non-null assertion)で握りつぶす前に、必ずconsole.logで戻り値を確認する習慣をつけましょう。

プレイヤーがCollisionレイヤーをすり抜けてしまう

以下の3点を順番に確認してください。

setCollisionByPropertyが呼ばれているか

create()内でcollisionLayer.setCollisionByProperty({ collides: true })が実行されているか確認します。

② Tiledでプロパティが正しく付与されているか

Tiledのタイルセットエディタで、壁タイルにcollides(bool型、値true)が設定されているか確認します。プロパティ名のスペルミスが最も多い原因です。collide(sなし)やCollides(大文字)では一致しません。

physics.add.colliderが呼ばれているか

setCollisionByPropertyだけではプレイヤーとの衝突は有効になりません。物理エンジンへの登録が必要です。

// この行がないとすり抜ける
this.physics.add.collider(this.player, collisionLayer);

physics.arcade.debug: trueにしても当たり判定が表示されない

arcade.debugはプレイヤーなどの物理ボディを可視化するものです。タイルの衝突領域を確認したい場合はrenderDebugを別途呼ぶ必要があります。

// タイルの衝突範囲を可視化する
const debugGraphics = this.add.graphics();
collisionLayer.renderDebug(debugGraphics, {
  tileColor: null,
  collidingTileColor: new Phaser.Display.Color(243, 134, 48, 200),
  faceColor: new Phaser.Display.Color(40, 39, 37, 255)
});

Phaser 4でTilemapを使うとき、this.make.tilemapthis.add.tilemapどちらを使えばいい?

現時点ではthis.make.tilemapを使うのが安全です。

Phaser 4のリファクタリング過程でシーンAPIの一部がthis.addに統合されつつありますが、Tilemapについてはthis.make.tilemapが引き続き動作します。公式サンプルやリリースノートでthis.add.tilemapへの移行が明示されるまでは、従来のthis.make.tilemapを使い続けてください。

大きいマップでゲームが重くなる。パフォーマンスを改善するには?

以下の対策が効果的です。

  • カメラのカリングを活用する:Phaserはデフォルトでカメラ範囲外のタイルを描画しません。カメラのsetBoundsを必ず設定しておきましょう。
  • レイヤー数を最小限にする:レイヤーが増えるほど描画コストが上がります。視覚的な変化がなければレイヤーは統合を検討してください。
  • arcade.debugを本番環境でオフにする:デバッグ描画は重いため、リリース時は必ずfalseにします。
  • マップを分割してシーン遷移で読み込む:非常に広いワールドの場合、1マップをシーン単位に分割して必要なときだけロードするのが有効です。

Tiledで作ったオブジェクト(スポーン位置など)をPhaserで取得するには?

TiledのObjectレイヤーに配置したオブジェクトは、map.findObjectで取得できます。

// Tiledで "Objects" という名前のObjectレイヤーに
// "Spawn" という名前のオブジェクトを置いた場合
const spawnPoint = map.findObject(
  'Objects',
  (obj) => obj.name === 'Spawn'
) as Phaser.Types.Tilemaps.TiledObject;

if (spawnPoint && spawnPoint.x !== undefined && spawnPoint.y !== undefined) {
  this.player = this.physics.add.sprite(spawnPoint.x, spawnPoint.y, 'player');
}

スポーン位置・ゴール地点・敵の初期配置など、座標をハードコーディングせずTiledで管理できるため、マップ編集がより柔軟になります。

TypeScriptでthis.player.bodynullかもしれないと怒られる

physics.add.spriteで生成したスプライトの.bodyは型上Phaser.Physics.Arcade.Body | Phaser.Physics.Arcade.StaticBody | nullになっています。update()内で安全にアクセスするには型ガードを使います。

update(): void {
  const body = this.player.body as Phaser.Physics.Arcade.Body;

  if (!body) return; // bodyがnullの場合は早期リターン

  if (this.cursors.up.isDown && body.blocked.down) {
    this.player.setVelocityY(-500);
  }
}

Tiledで保存した.tmj.tmxは何が違う?どちらを使えばいい?

Phaserで使う場合は.tmj(JSON形式)一択です。.tmxはXML形式のためPhaserのローダーが対応していません。

Tiled 1.9以降はデフォルトの保存形式が.tmjに変わっていますが、古いバージョンや設定によっては.tmxで保存される場合があります。その場合はメニューからファイル → 名前を付けてエクスポートでJSON形式を明示的に選んでください。日常の作業保存は.tmjのまま行い、.tmxで保存する必要はありません。

まとめ

この記事では、「TiledでマップをつくりPhaserで読み込んで当たり判定をつけるまで」を、TypeScriptのコードとともに一通り解説しました。最後に要点を振り返っておきましょう。

基礎知識として押さえたこと

  • タイルマップはTile・Tileset・Layer・Collisionという4つの概念で成り立っている
  • 現在のPhaser(v3.50以降・Phaser 4)ではcreateStaticLayercreateDynamicLayer廃止され、createLayerに統一されているため古い記事を読む際は注意する
  • 配列管理と比べてTilemapは描画の最適化・衝突判定の簡潔さ・ビジュアル編集の容易さという圧倒的な利点がある

Tiledの操作として押さえたこと

  • マップはタイルサイズをPhaserの設定と揃えて作成する
  • レイヤーはBackground・Collision・Foregroundの3層に分けて管理するのが基本
  • 衝突させたいタイルにはTilesetエディタでcollides: trueプロパティを付与する
  • PhaserへはJSON形式(.tmj)でエクスポートする

    Phaserの実装として押さえたこと

    • preloadtilemapTiledJSONspritesheetを読み込む
    • addTilesetImageの第1引数はTiled上のTileset名(ファイル名ではない)
    • createLayerのレイヤー名は大文字・小文字を含め完全一致が必要
    • setCollisionByProperty({ collides: true })physics.add.collider両方を呼んで初めて衝突判定が有効になる

    ここまで実装できれば、ゲームのステージ制作における基本的なパイプラインはひと通り身についた状態です。「自分でマップを描いて、キャラクターがその上を歩く」という体験は、ゲーム開発の面白さを一気に実感できる瞬間です。ぜひ実際に手を動かしてみてください。

    この記事の内容を土台にすると、次のステップとして以下のテーマに自然につながっていきます。

    • アニメーション:歩行・ジャンプ・待機モーションをスプライトシートで実装する
    • 敵キャラクターの実装:パトロールAIと当たり判定によるダメージ処理
    • アイテムとオーバーラップphysics.add.overlapでコインや回復アイテムを取得する
    • マップの切り替え:複数シーンを管理してステージ遷移を実装する
    • Tiledのオブジェクトレイヤー活用:敵やアイテムの初期配置をコードではなくTiledで管理する

    Phaserの公式ドキュメント(phaser.io/docs)とKenney.nlの無料素材を組み合わせれば、ブラウザで動くゲームを思いのほか短期間で作り上げることができます。まずはこの記事のコードをベースに、自分だけのステージを作ることから始めてみてください。

    タイトルとURLをコピーしました