🌏

Phaser3 TileMapの基礎

に公開

主な改定履歴

  • 2025/05/02 新規公開

はじめに

この記事では Tiled というマップエディタを使用し、簡単な横スクロールゲームを作成します。
まず前半では Tiled のソフトの使い方について解説します。
そのあと Phaser3 でのマップデータの扱い方と、コードの書き方について見ていきます。

この記事の前提条件

  • Phaser3の基礎的な扱い方を知っていること。
    (なお、Phaser3Arcade物理エンジンの基礎についてはこちらの別記事にて解説しています。)
  • TypeScriptが分かること。

作成するゲームの内容

  • 2D横スクロールアクションゲームを作ります。
  • 矢印キーでプレイヤーを操作し、画面はプレイヤーに合わせてスクロールします。
  • プレイヤーが黄色い鍵に接触すると鍵を取得します。
  • プレイヤーが赤い宝石に接触するとポイントが入ります(演出のみ)。
  • すべての鍵を取得し、ゴールの黄色い扉(ゴール)に到着するとゲームクリアです。
    鍵を取りこぼしている場合はゴールできません。

この記事のソースはStackBlitzで確認できます。

StackBlitzの使い方

画面右下の Editor をクリックし、左上のハンバーガーメニューからソースが参照できます。

タイルマップの作り方

Tiledについて

Tiledは、無料で利用できる汎用的なマップエディタです。
2Dの横スクロールアクションのステージをはじめ、
RPGでよく見るようなトップビューのマップなども簡単に作ることができます。

Tiledの公式サイト から最新版をダウンロードし、インストールします。
(本記事の随筆時点ではバージョン1.11.2が最新です。)

アセットの準備

ゲームで使うアセット(資材)は、次のサイトで配布されている Simplified Platformer Pack をもとに、一部加工して利用します。
Open Game Art - Simplified Platformer Pack

アセットを用意したら編集用のフォルダを決め、その中にタイルセット画像を置いて作業していきます。
プロジェクトファイル(tmx)とJSONファイルの保存先もこのフォルダを使って作業を進めます。

タイルマップの作成

ここからTiledを使って具体的なマップ作成の手順を見ていきます。

初期設定

アプリを起動したら、「新しいマップ」をクリックします。
作成するマップの初期設定を入力します。
今回使用するタイルの画像は1つあたり64x64の大きさですので「タイルの大きさ」もその値で設定します。

新しいマップの作成画面

タイルセットの追加

画面右下「新しいタイルセット」ボタンをクリックします。
ダイアログが出ますので、名前を tilesheet にします。
このとき必ず「マップに埋め込み」をONにします。
準備したタイルシート画像ファイルを選択します。
必要に応じて、透過色の設定を行います。
OKを押すと「タイルセットビュー」に新しいタイルセットが表示されます。

メニューから「ファイル名を付けて保存」を選択し、プロジェクトをtmxファイルとしてタイルシート画像と同じフォルダに保存します。
ここではファイル名を tilesmap.tmx とします。

新しいタイルセット

タイルセットの編集

タイル要素ごとに必要なプロパティを登録します。
具体的にはアクションゲームで地面やブロックとなるタイル要素に対し、それ以外のタイルと区別がつくようカスタムプロパティを設定します。

「タイルセットビュー」のスパナのマーク「タイルセットを編集」をクリックします。

タイルセットを編集ボタン

別タブで「タイルセット編集」画面が出ます。
タイルセットにあるタイルのうち「地面やブロックとなるタイル」を選択状態にします。
(shift+クリックやctrl+クリックで複数選択できます)
「プロパティビュー」の左下にある「+」のアイコン「プロパティを追加」ボタンをクリックします。
ダイアログが出ますので、プルダウンをbool、名前にcollidesを指定します。
OKでダイアログを閉じたら、プロパティビューに行が追加されますので、チェックをONにします。
保存して「タイルセット編集」タブを閉じます。

タイルセットを編集画面

レイヤーの編集

レイヤー一覧から「タイルレイヤー」を追加します。
リストにタイルレイヤーが2つある状態にし、それぞれ名前を platform, backgroundとします。

platform はプレイヤーが移動できる領域のマップです。
「スタンプ」や「塗りつぶし」ツールを使ってマップを作成します。
なお、ゲーム内のゴールとして使うので扉のタイルをマップ右端の方に配置します。

background は背景だけのレイヤーです。

タイルレイヤーの編集

続いて、オブジェクトレイヤーを追加して鍵などのオブジェクトを配置します。

タイルレイヤーの追加の手順と同様に、リストから「オブジェクトレイヤー」を選択し追加します。
名前を objectsLayer に変更します。

オブジェクトの配置

プレイヤーの配置

ツールバーから「点を追加」を選びます。
レイヤー objectsLayer が選択されていることを確認し、マップの右端の方をクリックしオブジェクトを配置します。
名前を player に変更します。

オブジェクトレイヤーの編集

宝石・鍵の配置

ツールバーから「タイルを追加」をクリックします。
タイルセットから宝石を選択し、オブジェクトレイヤーの任意の場所に配置します。

タイルを追加 宝石1

プロパティ画面から配置したオブジェクトの名前を gem に変更します。
プロパティ追加ボタンをクリックし、プルダウンをint、名前を point としてカスタムプロパティを追加します。
リストに行が追加されたら、point の行に任意の値を入力します。

タイルを追加 宝石1

ツールバーから「オブジェクトを選択」をクリックし、配置した gem を選択してコピー&ペーストを行い、宝石を複製します。
複製した宝石のpointは好みで変更できます。

宝石の配置と同様の手順で鍵のオブジェクトも配置します。
このとき名前は key とし、カスタムプロパティは不要です。

ゴールの配置

ツールバーから「四角形を追加」をクリックし、ゴールの扉に合わせて、四角形を描画します。
配置した四角形の名前を goal とします。

ここまででマップの作成手順は終了です。

ゴールの追加

JSONファイルのエクスポート

プロジェクトファイルを保存したら、メニューバー>ファイルから「名前をつけてエクスポート」をクリックします。
ファイル名を tilemap.json とし、タイルシートと同じ場所にJSONファイルを出力します。

Phaser3 での利用準備

Phaser3 のプロジェクトフォルダの public に以下のような形で、
tilemap.json とタイルシートの画像 tilesheet.png の2ファイルをコピーして配置します。
tmxファイルはゲーム実行時には不要です。

public
  └─assets
      ├─images
      └─maps
            tilemap.json
            tilesheet.png

コードの解説

タイルマップに関わる個所を中心に解説していきます。
(冒頭にも記した通り、Arcade物理エンジンの基礎については別の記事にて解説しています。)

まず、preload メソッドにおいて、
tilemapTiledJSON メソッドでJSONファイルからタイルマップのデータを読み込みます。
タイルシートの画像は、通常の画像と同じように tilesheet.png をLoadします。
(なお、今回のサンプルではタイルシートの一部をアイテムのスプライトとして使用するため、spritesheetメソッドを使用しています。
this.tilemap.addTilesetImageメソッドにテクスチャを渡すだけの場合は、this.load.imageメソッドで問題ありません。)

this.load.setPath('assets/');
this.load.tilemapTiledJSON('tilemap_key', 'maps/tilemap.json');

this.load.spritesheet('tilesheet_key', 'maps/tilesheet.png', { frameWidth: 64 });
this.load.spritesheet('player', 'images/charasheet.png', { frameWidth: 80, frameHeight: 74 });

続いて create メソッドに移ります。
tilemap メソッドでタイルマップのインスタンスを作成し、
addTilesetImage メソッドで読み込み済みのテクスチャと紐づけます。

なお、今回分かり易いようにタイルマップ、タイルシートのCacheのキー名には末尾に key と付けてあります。

addTilesetImage メソッドに関して、第1引数はタイルマップ内のタイルシート名、
第2引数にタイルシートのテスクチャ名を指定ますが、同名の場合は第2引数は省略できます。

// タイルマップを作る
this.tilemap = this.make.tilemap({ key: 'tilemap_key' });
// タイルマップ内で定義されているタイルシートの名前と、画像のキーを紐づける。
// (なお、第2引数を省略した場合は、第1引数と同じ文字列が使われる。)
this.tileset = this.tilemap.addTilesetImage('tilesheet', 'tilesheet_key') as Phaser.Tilemaps.Tileset;

続いて、レイヤーの設定に移ります。
タイルマップ内で定義したレイヤー名を指定して、それぞれレイヤーオブジェクトを作成します。

// Tilemapから各レイヤーを作成する。
// createLayerの呼び出し順は表示に関係するので背景から作成する。
const background = this.tilemap.createLayer('background', this.tileset, 0, 0); // 背景
this.platform = this.tilemap.createLayer('platform', this.tileset, 0, 0) as Phaser.Tilemaps.TilemapLayer; // プラットフォーム
this.objectsLayer = this.tilemap.getObjectLayer("objectsLayer") as Phaser.Tilemaps.ObjectLayer;           // オブジェクトレイヤー

プラットフォームレイヤーのうち、タイルシートでプロパティに collides を設定したタイルを衝突検知対象にします。

// プラットフォームレイヤーのプロパティから衝突検知対象のタイルを抽出する。
this.platform.setCollisionByProperty({ collides: true });

オブジェクトレイヤーから名前が player である要素を見つけて、その座標にプレイヤーのスプライトを配置します。

// プレイヤー
const playerPos = this.objectsLayer.objects.find(o => o.name === "player") as Phaser.Types.Tilemaps.TiledObject;
this.player = this.physics.add.sprite(playerPos.x, playerPos.y, 'player')
    .setOrigin(0.5, 1).setBounce(0.1).setBodySize(54, 66);
// プレイヤーの移動範囲を画面の中だけとする。
this.player.setCollideWorldBounds(true);

ここからは宝石と鍵を画面に配置していきます。クラス内の deployItems メソッドに処理をまとめています。

プレイヤーの時と同様に、オブジェクトレイヤーから指定の名前のオブジェクトを抽出します。
注意点として、タイルオブジェクトの原点(座標の基準)はタイル画像の左下なのに対し、
配置するスプライトの原点は中央にしますので、その差を考慮する必要があります。

そのあと、カスタムプロパティの取得を行います。
指定の名前のプロパティ、ここでは宝石のオブジェクトにある point を取得し、その値をスプライトに保持します。
この値はプレイヤーが宝石を取得した時の表示に使われます。

// タイル1個あたりの横幅、高さを取得
const tileSize = { w: this.tileset.tileWidth, h: this.tileset.tileHeight };
// 指定の名前のオブジェクトのみを抽出する
const items = this.objectsLayer.objects.filter(o => o.name === item);
// 抽出したオブジェクトからスプライトを生成する
const sprites = items.map(e => {
    const obj = e as Phaser.Types.Tilemaps.TiledObject;
    // アイテムの作成
    // オブジェクトレイヤーに配置したタイルは原点(Origin)が左下であるため、配置の際はx,yの値に注意。
    const sprite = this.physics.add.sprite(
        obj.x + tileSize.w / 2,
        obj.y - tileSize.h / 2,
        spritesheet, obj.gid - 1
    ).setOrigin(0.5);// tweenで動かす関係でspriteのOriginを0.5にする。
    sprite.setName(obj.name);
    sprite.setData('gid', obj.gid);

    // オブジェクトからカスタムプロパティを取得しスプライトのデータとして保持する。
    const customProps = obj.properties as CustomProp[];
    if (propNames && customProps?.length > 0) {
        propNames.forEach(name => {
            const gotElm = customProps.find(f => f.name === name);
            if (gotElm) {
                sprite.setData(name, gotElm.value);
            }
        })
    }
    return sprite;
});

ゴールの設置をします。
基本的にはこれまでのタイルオブジェクトと同じですが、マップに描画した四角形の高さと幅を保持しているのでその値を使って四角形の配置をします。

// ゴール
const goalPos = this.objectsLayer.objects.find(o => o.name === "goal") as Phaser.Types.Tilemaps.TiledObject;
const goalRect = this.add.rectangle(goalPos.x, goalPos.y, goalPos.width, goalPos.height).setOrigin(0);
const goalSprite = this.physics.add.existing(goalRect);
const goalSpriteBody = goalSprite.body as Phaser.Physics.Arcade.Body;

最後にカメラ関係の処理について。

今回のゲームではカメラがプレイヤーに追跡するようにしますので、以下ようにコードを記述します。

// カメラ関連
this.cameras.main.setBounds(0, 0, this.tilemap.widthInPixels, this.tilemap.heightInPixels);
// プレイヤーにカメラを追跡させる
this.cameras.main.startFollow(this.player, true, 1, 1, 0, 300);

Discussion