旧SEによる日曜プログラミング日記

趣味は作曲、自分で一から作りたがりのユーザIT子会社の旧SE(かつてはプログラマー)によるリハビリのための日曜プログラミング日記です。

Phaser3+typescriptでSRPGを開発してみる #005 Tiledの利用

前回、2D MAPを描いたのですが、背景の黒が出てきたり困ってたところ、インターネットで情報を集めると、Tiledというソフトウェアが良さそうで、かつMacも使えそう!ということなので、今日は使ってみたいと思います。

マップ作成(2) Tiledの活用

筆者の試行環境

たまに、書いておこうと思います。

macOS X10.14 (MacBookPro)

Tiledのインストール

Tiledは下記からダウンロードできます。 フリーで使えますが、寄付を募っておられます。

www.mapeditor.org

Macの場合は解凍すると、Tiled.appが出てくるので、任意のフォルダで実行するだけです。

Tiledの使い方

(0) 日本語モード

なんと日本語モードがあります。英語に触れる必要がないと信じて内資の会社に就職した私には、大変ありがたいです。 設定方法は、下図の通りです。

f:id:mtmusic34:20210614230638p:plain
Tiledの環境設定

(1) タイルセット読込

まずはタイルセットを読み込みます。アイコンが羅列されているものですね。

メニューバーの【ファイル】→【新規】→【新しいタイルセット】を起動して、ファイルパスを【参照】ボタンを押下してファイルを選択します。 タイルセットの【名前】はファイル名に応じて、自動的に入ってくれます。

f:id:mtmusic34:20210614231250p:plain
タイルセット読み込み
こんな感じで読み込まれます。

尚、ずらして画像を読みたい場合、新規で読み込むときにオフセットを調整すれば良いようです。読み込み後でも、画像ファイルの再指定ボタンを押下すると、新規読込時のポップアップが起動するので、変更可能です。

f:id:mtmusic34:20210614232248p:plain
タイルセットをずらして読み込む

(2) マップの作成

続いて、同じく上部メニューバーの【ファイル】→【新規】→【新しいマップ】をクリックすると、マップが作れます。

f:id:mtmusic34:20210614231520p:plainf:id:mtmusic34:20210614231712p:plain
マップ作成

白紙の画面が出てきます。 固定にしてしまうと幅・高さはグレーになっていて変えられないので、新規作成時に注意が必要そうですね。無限で作った方が良いかもです。

(3) 描画

あとは、ペタペタ貼り付けるだけです。

ちなみにレイヤーが指定できます!

f:id:mtmusic34:20210615224241p:plain
Layer1

まずは草原で塗りつぶしたあと、レイヤーを新規で追加(Layer2)。 そして、下を透過するようなチップを選ぶと、ご覧の通り、透過できます!

f:id:mtmusic34:20210615224452p:plain
Layer2

かなり便利です。

Phaserへの取込

ファイル構成

タイルセットの画像ファイルと、マップのJSONファイルを下記に配置して試行してみます。

<root>
 |- index.html
 |- images
 |    |- map
 |         |- tileset_demo.png
 |- props
 |    |- game
 |         |- map_demo.json
 |- src
 |    |- index.ts
 |    |- scene
 |           |- Scenes.ts
 |           |- GameScene.ts
 |- dist
      |- index.js

JSONを自動生成

さて、問題はこれをどのようにPhaserへ取り込むのか?です。

まず互換性のあるファイルとして出力するには?ですが、やはりJSONでの書き出しが必要ですよね。

日頃からJSONで管理している人はJSONその物を直接、Phasre3で読み込むことができます。もし、tsxで管理している場合には、メニューバーの【ファイル】ー【名前をつけてエクスポート】を選択して、ファイル種別として【JSONマップファイル (*.json)】を指定して出力してみてください。

但し、Phaser3では、Externalオプションに対応しておらず、Embed TilelayerをオフにしてJSON出力しないと

External tilesets unsupported. Use Embed Tileset and re-export
Uncaught TypeError: Cannot read property '2' of undefined
AssignTileProperties @ 【ルートディレクトリ】/node_modules/phaser/dist/phaser.js:133637:48

というエラーが出ます。多少、わかりにくいアイコンなのですが、タイルセットのWindow下部の左から2番目のアイコンをオフにしてください。

f:id:mtmusic34:20210617234102p:plain
External tilesetsの無効化
すると、いきなりタブが2つになりますが、気にしないで大丈夫です。

ファイル保存形式

ちなみに、Tiledでは、tsxという拡張子がデフォルトなのですが、これは単にXMLというだけです。 ですが、この拡張子、React等ではプログラムとして認識されてしまい、トランスパイルに影響が出ることがあります。

ということでJSONでデータ保存する方が楽そうです。

自動生成されたjson

自動生成されたJSONは下記の通りです。

map_demo.json

{ "compressionlevel":-1,
 "height":5,
 "infinite":false,
 "layers":[
        {
         "data":[4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4],
         "height":5,
         "id":1,
         "name":"Layer1",
         "opacity":1,
         "type":"tilelayer",
         "visible":true,
         "width":10,
         "x":0,
         "y":0
        }, 
        {
         "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 28, 0, 0, 29, 14, 30, 0, 0, 0, 43, 44, 0, 0, 45, 14, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
         "height":5,
         "id":2,
         "name":"Layer2",
         "opacity":1,
         "type":"tilelayer",
         "visible":true,
         "width":10,
         "x":0,
         "y":0
        }],
 "nextlayerid":3,
 "nextobjectid":1,
 "orientation":"orthogonal",
 "renderorder":"right-down",
 "tiledversion":"1.7.0",
 "tileheight":32,
 "tilesets":[
        {
         "columns":16,
         "firstgid":1,
         "image":"..\/..\/images\/map\/tileset_demo.png",
         "imageheight":384,
         "imagewidth":512,
         "margin":0,
         "name":"tileset_demo",
         "spacing":0,
         "tilecount":192,
         "tileheight":32,
         "tilewidth":32
        }],
 "tilewidth":32,
 "type":"map",
 "version":"1.6",
 "width":10
}

長くて見づらくてすみませんが、この通り、mapのJSONファイルが出力されました。

jsonファイルの解析

ポイントは下記の通りです。

  • Layer1とLayer2が見つけられるでしょうか。

  • tilesetsの中身が展開されているでしょうか?(Externalモードにしていると、ここがtsxファイルを参照するだけの記述になっているはずです)

その他、いくつか気になることがあったので、触れさせていただきます。

data

前回記事(#004)では、左上を0としたのですが、今回は0は空白(タイルを指定していないセル)で、1から始まっており、前回3だったところが4になってそうです。

これは、jsonファイルの下部にあるtileset.firstgridとリンクしてそうです。

tilesets

そのタイルセット(複数形)ですが、ここでタイルセットを複数定義できます。

タイルセットの数だけ、上部メニューの【ファイル】ー【新規】ー【新しいタイルセット】を選択して読み込み、全てのタイルセットの編集画面で、タイルセットのWindow下部の左から2番目のアイコン を押下して、埋め込みタイルセットをオフにしてください。

typescript側の書き方

ようやくtypescriptでの書き方ということで、このJSONファイルを読み込んでみます。

GameScene.ts

export default class GameScene extends Phaser.Scene {

    // タイルセットの1セルあたりの解像度(px)
    public static CELL_PX = {
        WIDTH : 32,
        HEIGHT : 32
    };

    // マップの開始位置(Offset) 左上が(0,0)
    private static MAP_POSITION = {
        X :  20,
        Y :  40
    };

    // タイルセットの画像ファイルのキー
    // タイルセット名(map_demo.jsonの中のtilesets.name属性を指定)
    private static TILESET_KEY_NAME = "tileset_demo"  
     // Phaser3の中のタイルセットを指すキーフレーズ(任意の文言)   
    private static TILESET_KEY_PLAIN = "PLAIN_MAP";    

    // マップJSONファイルのキー
    // Phaser3の中のマップJSONファイルを指すキーフレーズ(任意の文言)
    private static MAP_JSON_FILE_KEY = "MAP_JSON_FILE_KEY"; 

    preload() {
        // tileset画像ファイルをロード(キー・ファイルパス)
        this.load.image(GameScene.TILESET_KEY_PLAIN, "/images/map/tileset_demo.png");

        // mapファイルのロード
        this.load.tilemapTiledJSON(GameScene.MAP_JSON_FILE_KEY, "/props/game/map_demo.json");
    }

    create() {          

        // タイルマップの作成
        var tilemap:Phaser.Tilemaps.Tilemap  = this.add.tilemap(GameScene.MAP_JSON_FILE_KEY);
        // タイルセットをリンク付け
        let planeTiles0:Phaser.Tilemaps.Tileset = tilemap.addTilesetImage(
                 GameScene.TILESET_KEY_NAME,    // 第1引数は マップのjsonファイル中のtilesets.name属性を指定
                 GameScene.TILESET_KEY_PLAIN);    // 第2引数はPhaser3内のキーワード
        // レイヤーを作成
        let mapGroundLayer0:Phaser.Tilemaps.TilemapLayer = tilemap.createLayer(
            "Layer1",           // Tiledで設定したレイヤー名
            planeTiles0,     // 定義したタイルセット
            20,40               // 第3引数・第4引数は、左上の座標を(0,0)とした時の配置位置(x,y) 
            );
        let mapGroundLayer1:Phaser.Tilemaps.TilemapLayer = tilemap.createLayer(
            "Layer2", 
            planeTiles0,
            20,40);
    };
}

今回のポイントです。

  • タイルセットと画像のリンクづけは、addTilesetImageメソッドを使用する

  • addTilesetImageの第1引数は マップのjsonファイル中のtilesets[].name属性、すなわちTiledで設定した名前を指定する

  • レイヤーはレイヤーの数だけ、createLayerを呼ぶ。その時の第1引数(レイヤーID)はマップのJSONファイル中のlayers[].name属性、すなわちTiledで設定したレイヤー名を指定する

キーがJSONname属性と合致している必要があるので注意です。合っていないとInvalid Tilemap Layer ID: 【レイヤーID】 【ルートDir】が出ます。

尚、今回は読み込むtilesetを一つにしていますが、複数のtilesetを読み込ませたい場合もPhaser3で実装可能で、createLayerメソッドの第2引数を配列で渡します。

let seaTiles:Phaser.Tilemaps.Tileset = tilemap.addTilesetImage("WorldMap-A1","TILESET_SEA");
let planeTiles:Phaser.Tilemaps.Tileset = tilemap.addTilesetImage("WorldMap-A2", "TILESET_PLAIN");

let layerBackground:Phaser.Tilemaps.TilemapLayer = tilemap.createLayer(
        "Background", 
        [seaTiles, planeTiles],                       // Tilesetを順番に読込(配列形式で複数指定)
        20, 40);                                              // Offsetを指定

完成

実行すると、無事マップが表示されました。

f:id:mtmusic34:20210617231312p:plain
ついに完成

まとめ

いかがでしたか? 捕まりそうなポイントもかなり網羅したつもりです。 これで、#004での疑問も解決し、ようやく2DのSRPGのゲーム画面ぽくなってきました。

Tiledがあるとかなり楽になることが伝われば幸いです。

次回は、ユニットを表示します。

mtmusic34.hatenablog.com

過去の日記

タイトル 記載内容
#001 プロローグ Phaser3とは
#002 環境設定 インストール・ビルド環境設定
#003 はじめてのPhaser3 初回稼働・キャンバス表示
#004 マップ作成 タイルセットの読込、画面手動作成、Sceneクラスのライフサイクル