Akashic Engine でローディングシーンのカスタマイズ

この文章は [Akashic Engine Advent Calendar 2019][akashic-advcal] 20 日目のエントリです。

## アセットとローディングシーン

[Akashic Engine][ae] ではゲームの開始時やシーンの生成時に、必要なアセットを読み込みます。アセット読み込みの間ゲームは進められないので、ローディングシーンが表示されます。

まあ使われている方には釈迦に説法かと思いますが、おなじみのこの画面です。

![screen (13).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540286/b5a816c1-1f9f-5a3b-2862-c4525a0500ca.png)

あまり公式にアピールされていませんが、実はこのロード画面はある程度カスタマイズできるので、その方法をご紹介します。

## グローバルアセットとデフォルトローディングシーン

カスタマイズの観点から言うと、ローディングシーンは二種類存在します。 **デフォルトローディングシーン** とそうでない普通の **ローディングシーン** です。

<dl>
  <dt>デフォルトローディングシーン</dt>
  <dd>ゲーム開始時に、グローバルアセット読み込みに使われるローディングシーン。ほとんどカスタマイズ不可</dd>
  <dt>(非デフォルトの) ローディングシーン</dt>
  <dd>シーンのアセット読み込み時に使われるローディングシーン。カスタマイズ可能</dd>
</dl>

グローバルアセットとは、 game.json のアセット定義で `"global": true` と指定されたアセットです。ゲーム開始時に必ず読み込まれ、以降ずっと読み込みなしで `g.game.assets` からアクセスできます。game.json を特に手で編集していない (`akashic scan asset` だけしている) 場合、スクリプトアセットだけグローバルアセットにされていますが、画像でも音声でも任意のアセットをグローバルとして指定することができます (参考: [game.json の仕様][game-json-spec])。(ただしなんでもかんでもグローバルにすると、ゲーム開始時のロード時間が伸びていくので、汎用性の高いものだけグローバルにするのがおすすめです)

このグローバルアセットの読み込みに使われるのがデフォルトローディングシーンです。これはほぼカスタマイズできません。なぜなら、肝心のカスタマイズするスクリプトアセットすらまだ読み込まれていない段階で使われるためです。カスタマイズしようがない。

ただし一つだけ、デフォルトローディングシーンを「非表示にすること」は可能です。game.json で `"defaultLoadingScene"` プロパティに `"none"` を指定すると、通常デフォルトローディングシーンが表示される状況で何も描画されなくなります。

```json
{
  "width": 640,
  "height": 360,
  "fps": 60,
  "assets": {
    "foo": "...",
    "...": "..."
  },
  "defaultLoadingScene": "none"
}
```

ただしこれは、ゲームのプレイヤーにも何が起きているかわからなくなってしまうので、あまりおすすめしません。ゲームが起動できていないのか読み込みが止まったのかも区別できないからです。あくまで何か特殊な状況を想定したオプションと考えるべきでしょう。

## ローディングシーン

他方、非デフォルトのローディングシーンは、割と自由にカスタマイズが効きます。通常のシーン (`g.Scene`) 同様に画像を表示したり、ユーザの操作を受け付けたりできます。

例として、ここでは「画面右下で円形のインジケータがなんとなく回るロード画面」を作ってみます。こういうの。回っている様子は各自で感じとってください。

![スクリーンショット 2019-12-20 22.46.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540286/ed1cade4-b704-6bcd-048b-c48f261d6e86.png)

まず画像が必要です。楽するために最初から円が 8 個並んでいる画像を作りました。45 度ずつ円を配置しているので、45 度ずつ回転していけばそれだけでアニメーションしている風に見えるやつです。

![load_icon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540286/f6a1242f-763c-6aaf-d30b-8cde4dde4542.png)

これを `load_icon.png` という名前で image/ において `akashic scan asset` すると、game.json に次のような記述が加わります。

```json
    "load_icon": {
      "type": "image",
      "width": 40,
      "height": 40,
      "global": true,
      "path": "image/load_icon.png"
    }
```

一行 (`"global": true,`) を加えて前述のグローバルアセットにしておきます。

```json
    "load_icon": {
      "type": "image",
      "global": true,
      "width": 40,
      "height": 40,
      "path": "image/load_icon.png"
    }
```

というのも、ローディングシーン内で使うアセットがグローバルアセットでない場合、「**ローディングシーン用のアセットを読み込むためのローディングシーン**」が必要になってしまうからです。(Akashic Engine はその場合でも (デフォルトローディングシーンを使って) 動作しますが、やや本末転倒でしょう)

ともあれこれで `game.assets["load_icon"]` でこのアセットがいつでも利用できるようになりました。

```javascript
var loadIconAsset = g.game.assets["load_icon"];
```

ローディングシーンは、 `g.game.loadingScene` に代入された `g.LoadingScene` クラスのインスタンスです。`g.LoadingScene` は通常のシーン (`g.Scene`) と同様にゲーム開発者が new できます。

```javascript
var loadingScene = new g.LoadingScene({ game: g.game });
```
あとは普通のシーンと同じように `loaded` トリガーで処理を記述して、 `g.game.loadingScene` に代入すれば完成です。

```javascript
loadingScene.loaded.add(function () {
  // 画面右下にアイコンを表示
  var loadIcon = new g.Sprite({
    scene: loadingScene,
    src: loadIconAsset,
    x: g.game.width - loadIconAsset.width - 10,
    y: g.game.height - loadIconAsset.height - 10,
  });

  // 毎フレーム 45 度回転させてアニメーションする
  loadIcon.update.add(function () {
    loadIcon.angle += 45;
    loadIcon.modified();
  });

  loadingScene.append(loadIcon);
})

// ローディングシーンとして設定
g.game.loadingScene = loadingScene;
```

この処理全体としては、ゲーム実行開始直後に一度実行しておくだけです。ほぼ普通のシーンと変わらないので、まあ難しいところはないかと思います。

## より発展的なオプションと注意点

その他ありそうなローディング中の演出についてざざっと補足です。

#### ユーザ操作を受け付ける

可能です。いえ、普通のシーンと全く同じ形で `pointDown` トリガーなどが利用できるので、まあ当然できるというだけですが……。ローディングの間ユーザ操作でちょっとしたエフェクトを出すような演出が作れます。

#### 読み込み進捗を表すプログレスバーを作る

可能です。というかデフォルトローディングシーンがやっていますね。

ファイルサイズ・読み込みサイズの情報は取れないので「残り何ファイルか」という数値になってしまいますが、以下のメソッドで常時参照できます。

```javascript
loadingScene.getTargetWaitingAssetsCount()
```

これを `loaded` で一度呼び出して全体数を把握しておき、あとは適宜呼び出して残りの数で進捗を把握できるので、表示に反映させてください。関係するものとして `targetAssetLoaded` トリガーがあります。これはアセットが一つ読み込まれる度に fire されます。

```javascript
loadingScene.targetAssetLoaded.add(function () {
  // ...
});
```

#### ロード終了後に何か演出を挟む

可能です。通常ローディングシーンは、必要なアセットがすべて読み込まれた瞬間に終了して次の(非ローディング)シーンに遷移してしまいます。が、 `g.LoadingScene` を new する時に `explicitEnd: true` を指定するとこの動作がなくなります。

```javascript
var loadingScene = new g.LoadingScene({
  game: g.game,
  explicitEnd: true
});
```

この場合、ローディング完了後に明示的に `loadingScene.end()` が呼び出されるまでそのローディングシーンが続行されます。必然的に上述の残りアセット数把握が必須になることに注意してください。

これを使うと「ローディング完了後画面がフェードアウトしてから次のシーンに行く」「ローディング完了後ユーザクリックを待って進む」などが実現できます。あとカスタマイズした **ローディングシーンのデバッグに便利** です。勝手に終わらないので。

#### 前のシーンの描画内容の上にかぶさるローディングシーン

不可能です。残念ながら Akashic Engine は「現在のシーン」しか描画してくれないので、半透明の `g.FilledRect` などを置いても直前のシーンの描画内容は何も出ません。クロスフェードとかも実現したいんですが方法がないのが現状です。

#### 注意点: targetReset トリガー

ここまで書いて来なかったのですが、実は **ローディングシーンは何度も使いまわされます** 。

大抵使い捨ての通常のシーンと異なり、ロードの度に使われるので、凝ったアニメーションなどをしていると「前回のロード画面の最後の状態」でロード画面が始まってしまうことになります。最初のコード例で挙げた「アイコンが回転するだけのロード画面」のようなケースであれば、それでも問題ないのですが、処理によってはそうもいかないでしょう。

このため `g.LoadingScene` には `targetReset` トリガーがあります。

このトリガーは読み込み待ちのシーンが変わる度に (つまり新たにロード画面に入る度に) 一度 fire されるので、ローディングシーンの内部状態をリセットするのに利用できます。

```javascript
loadingScene.targetReset.add(function () {
  // ローディングシーンの状態リセット処理
  // ...
});
```

## まとめ

ローディングシーンとそのカスタマイズについてご紹介しました。

読み込み時間なんて短いに越したことはないのですが、長いなら長いなりに演出することもできるので、やたら素材の重くなってしまったゲームなどで活用いただければと思います。

[akashic-advcal]: https://qiita.com/advent-calendar/2019/akashic-engine
[ae]: https://akashic-games.github.io/
[game-json-spec]: https://akashic-games.github.io/guide/game-json.html