Phaser 3 で複数画像をまとめた atlas を使うために spritesmith を利用したメモ

# 1. 目的・選択肢について

Steam のように、事前に全てのアセットを配布してから起動する形式では、ロード時間はそれほど問題にならない。
しかし、Web 上でアップロードする場合などでは、画像数が多い場合にロード時間が長くなるため、画像を1つのファイルにまとめたい(スプライトシート化・アトラス化したい)ケースがある。

本稿は、Phaser で扱うための画像を、無料で使える [spritesmith](https://www.npmjs.com/package/spritesmith) で作成した時のメモ。

## 1.1. 有償

- [TexturePacker](https://www.codeandweb.com/texturepacker)
  - 高機能とされるが、5千円から1.2万円程度の費用が必要になる

## 1.2. 無償

- [spritesmith](https://www.npmjs.com/package/spritesmith)
  - 比較的よく利用されている。最新バージョンは [2024/10/23 の 3.5.1](https://www.npmjs.com/package/spritesmith/v/3.5.1?activeTab=versions)
  - [spritesmith-cli](https://www.npmjs.com/package/spritesmith-cli)
    - 本来はこの CLI を利用したいが、最新バージョンが [2016/04/11 の 1.1.2](https://www.npmjs.com/package/spritesmith-cli?activeTab=versions) と古く、依存する `spritesmith` のバージョンも 1.3.2 と古い(`spritesmith` は 2.x、3.x で大きな仕様変更があった)。そのため、今回は `spritesmith-cli` ではなく `spritesmith` を直接利用する
- npm 上には `spritesheet` キーワードで検索すると[他にも多くのパッケージが存在する](https://www.npmjs.com/search?q=keywords:spritesheet)が、本稿では検証していない
- Unity では、[スプライトアトラス](https://docs.unity3d.com/ja/2023.2/Manual/sprite-atlas.html)機能がアセットパイプラインに統合されているため、外部ツールは不要に見えるが今回は調べない

# 2. `spritesmith` を使って Phaser 用の atlas を作成する

## 2.1. Phaser のコード側で必要な変更

| `Scene` 内のタイミング例 | 画像を直接使う                                                                | atlas を使う                                           |
| ------------------------ | ----------------------------------------------------------------------------- | ------------------------------------------------------ |
| `preload`                | `this.load.image('aaa', 'aaa.png');  this.load.image('bbb', 'bbb.png');  ...` | `this.load.atlas('atlas', 'atlas.png', 'atlas.json');` |
| `create`                 | `this.add.image(x, y, 'aaa');`                                                | `this.add.image(x, y, 'atlas', 'aaa');`                |

画像ファイルを atlas 画像 + atlas JSON ファイルに変換した後、 `this.load.atlas` で読み込んだシートの各画像へは、フレーム名を用いてアクセスする。
従来テクスチャキーのみで指定していた箇所は、テクスチャキー(例: `atlas`)とフレーム名(例: `aaa`)の両方で指定するように、コード全体を修正する必要がある。


## 2.2. Phaser 用の atlas ファイルフォーマット

`this.load.atlas` には画像とそれに対応する JSON ファイルが必要であるが、[Phaser の公式ドキュメント](https://docs.phaser.io/api-documentation/class/loader-filetypes-atlasjsonfile)では、「[TexturePacker で生成すること](https://www.codeandweb.com/texturepacker/tutorials/how-to-create-sprite-sheets-for-phaser3?source=photonstorm)」が推奨されている程度で、JSON もフォーマットに関する詳細な仕様の記述は少ない。

公式ドキュメントには詳細がないものの、Phaser は実行可能なサンプルが豊富に提供されており、atlas のサンプルも存在する。[atlas の実行サンプル内に記載がある JSON](https://phaser.io/examples/v3.85.0/loader/texture-atlas-json/view/get-atlas-meta-data) を確認すると、その構造は比較的単純であることがわかる。


## 2.3. `spritesmith` を使った atlas 画像、atlas JSON の作成

`this.load.atlas` で読み込むための `atlas.png` と `atlas.json` を `spritesmith` を利用して生成する。(`spritesmith` の `result` に含まれる `result.coordinates` がメインの情報なので、それを [atlas の実行サンプル内に記載がある JSON](https://phaser.io/examples/v3.85.0/loader/texture-atlas-json/view/get-atlas-meta-data) を参考に atlas JSON のプロパティにマッピングする)

```js:generate_atlas.cjs
#!/usr/bin/env node
// generate_atlas.cjs (CommonJS スクリプト)

// require() を使用して必要な CommonJS モジュールをインポート
const Spritesmith = require('spritesmith');
const fs = require('fs');
const path = require('path');
const glob = require('glob'); // ファイル検索用

// --- 設定 ---
// 入力ディレクトリと出力ディレクトリのパスを設定
const inputGlob = 'public/assets/**/*.png'; // スクリプトと同じ階層にある 'public/assets' 下の *.png を取得
const outputDir = 'public/assets/atlas'; // スクリプトと同じ階層の 'public/assets/atlas' ディレクトリを想定
const outputImg = `${outputDir}/atlas.png`; // 出力するアトラス画像ファイルパス
const outputJson = `${outputDir}/atlas.json`; // 出力するJSONメタデータファイルパス
const padding = 1; // 各スプライト間のパディング (ピクセル)

console.log('--- Spritesmith を使用してテクスチャアトラスを生成 (CommonJS スクリプト) ---');
console.log(`入力ディレクトリ: ${inputGlob}`);
console.log(`出力ディレクトリ: ${outputDir}`);

// 出力ディレクトリが存在しない場合は作成
if (!fs.existsSync(outputDir)) {
    console.log(`出力ディレクトリを作成中: ${outputDir}`);
    fs.mkdirSync(outputDir, { recursive: true }); // recursive: true で親ディレクトリもまとめて作成
}

// glob.sync() を使用して入力ディレクトリ以下の .png ファイルを再帰的に検索
const files = glob.sync(inputGlob, {
    ignore: ['public/assets/cursor/**', 'public/assets/atlas/**', '**/sprite_*.png']
});

if (files.length === 0) {
    console.warn(`警告: '${inputDir}' ディレクトリまたはそのサブディレクトリに画像ファイルが見つかりませんでした。アトラス生成をスキップします。`);
    process.exit(0); // ファイルが見つからなければ正常終了
}

console.log(`${files.length} 個の画像ファイルが見つかりました:`);

// Spritesmith の実行
Spritesmith.run({
    src: files, // 入力ファイルリスト
    padding: padding, // パディング設定
    imgOpts: { format: 'png' }, // 出力画像形式
    engine: 'pixelsmith' // 使用するエンジンを明示的に指定する
}, function handleResult(err, result) { // 実行結果を処理するコールバック関数
    if (err) {
        console.error('Spritesmith 実行中にエラーが発生しました:', err);
        process.exit(1); // エラーコードで終了
        return; // コールバック内の後続処理を停止
    }
    console.log('Spritesmith 実行成功.');
    // アトラス画像をファイルに書き出す (result.image は Buffer オブジェクト)
    fs.writeFileSync(outputImg, result.image);
    console.log(`アトラス画像を生成しました: ${outputImg}`);
    // アトラスJSON情報をファイルに書き出す (result に以下の値が無い場合、spritesmith のバージョンが怪しい)
    if (result && result.coordinates && result.properties) {
        const frames = [];
        for (const filepath in result.coordinates) {
            if (result.coordinates.hasOwnProperty(filepath)) {
                const sprite = result.coordinates[filepath];
                const frameName = path.basename(filepath).replace(/\.\w+$/, '');
                frames.push({
                    filename: frameName,
                    frame: { x: sprite.x, y: sprite.y, w: sprite.width, h: sprite.height },
                    rotated: false,
                    trimmed: false,
                    spriteSourceSize: { x: 0, y: 0, w: sprite.width, h: sprite.height },
                    sourceSize: { w: sprite.width, h: sprite.height },
                    pivot: { x: 0.5, y: 0.5 }
                });
            }
        }
        const atlasJson = {
            frames: frames,
            meta: {
                app: 'spritesmith',
                image: path.basename(outputImg),
                format: 'RGBA8888', // 決め打ち
                size: { w: result.properties.width, h: result.properties.height },
                version: '1.0', // 決め打ち
                scale: '1' // 決め打ち
            }
        };

        fs.writeFileSync(outputJson, JSON.stringify(atlasJson, null, 2));
        console.log(`メタデータJSONを生成しました: ${outputJson}`);
    } else {
        console.error('エラー: result.coordinates または result.properties が存在しません。');
    }
    console.log('テクスチャアトラス生成スクリプト完了.');
    process.exit(0); // 正常終了
});
```

上記のスクリプトを保存した後、必要なライブラリを入れて実行↓

```shell
npm -D install spritesmith glob
node generate_atlas.cjs
```

することで、スクリプトから見て `public/assets/atlas` 下に `atlas.png` と `atlas.json` を作成することができた。

`result.coordinates` から Phaser の atlas json への変換は、おそらく [spritesheet-templates](https://www.npmjs.com/package/spritesheet-templates) などの形でどこかに実装が既にありそうだが、試せていない。Phaser 用の JSON に関しては単純な内容なので今のところ問題ない気がする。

### 2.3.1 JSON Array と JSON Hash の違い

Phaser 3 の 2025/05 現在最新の 3.90.x 系では [公式ドキュメント](https://docs.phaser.io/api-documentation/class/loader-loaderplugin#atlas) に以下のように記載があるとおり、

> Phaser expects the atlas data to be provided in a JSON file, using either the JSON Hash or JSON Array format.

`this.load.atlas` で読み込む atlas json のフォーマットは JSON Hash と JSON Array どちらでも良いという記載があり、ややこしい。本当にどちらの形式でも動くのか試してみたところ、たしかにどちらでも動いた。どちらがメインで使われるべきフォーマットなのか等も調べてみたが、詳しい状況はわからなかった (どちらのフォーマットも Phaser 発ではなく、歴史的によく使われているフォーマットの模様)。[Phaser のサンプル上では](https://phaser.io/examples/v3.85.0/loader/texture-atlas-json/view/get-atlas-meta-data
) JSON Array を利用しているため、上記スクリプトは JSON Array で出力している。

### 2.3.2 JSON Array サンプル (Hash と違うところだけ)

`.frames` は配列になり、フレーム情報と同じオブジェクト内に `filename` キーが入り、その値としてフレーム名が入る。

```json
{
  "frames": [
    {
      "filename": "aaa",
      "frame": {
        "x": 0,
        "y": 2163,
        "w": 400,
        "h": 300
      },
      ...
    },
  ],
  "meta": {
    ...
  }
```

### 2.3.3 JSON Hash サンプル (Array と違うところだけ)

`.frames` はオブジェクトになり、フレーム名をキーとしてそこに各フレームの情報が紐づく。

```json
{
  "frames": {
    "aaa": {
      "frame": {
        "x": 0,
        "y": 2163,
        "w": 400,
        "h": 300
      },
      ...
    },
  },
  "meta": {
    ...
  }
```

# 3. まとめ

`spritesmith` が無料で使えて良さそう。