【Day11】Promiseを使ってタイルを正しく描画しよう【QAC24】


# なぜ正確に描画されなかった？

前回まで、`Game`と`GameMap`を作り上げて、画面描画が出来そうな感じでした。

しかし、実際に`pnpm tauri dev`で実行したウィンドウにはタイルは描画されていませんでした。

なぜなのでしょうか。

## 画像の読み込みが終わってない？

今回の肝は`GameMap`クラスの`generateMapTiles`メソッドです。

```typescript
abstract class GameMap {
  ...

  protected generateMapTiles() {
    for (let l = 0; l < this.tiles.length; l++) {
      if (!this.realtiles[l]) this.realtiles[l] = [];
      for (let t = 0; t < this.tiles[l].length; t++) {
        const tile = this.tiles[l][t];
        fakeTileToRealTile(tile.tile).then((realTile) => {
          this.realtiles[l][t] = {
            x: tile.x,
            y: tile.y,
            tile: realTile,
          };
        });
      }
    }
  }

  ...
}
```

このメソッドで`MapTile`から`DrawableTile`に変換しています。
答えは単純で、`fakeTileToRealTile`関数が非同期処理を行っているためです。

いやいや、`then`を使って処理を行うから動くんでしょ？って思われるかもしれません。

## 非同期処理の「投げっぱなし」とは？

Day 7 で以下のように書いております。

> `async`関数を`await`を付けずにそのまま実行すると、本当の値(今回なら`Tile`)ではなく`Promise`が返ってきます。
> また、`await`を付けないと、処理完了まで止まることはなく、どんどん次の処理へと進んでいってしまいます。

これは、`then`だろうと同じです。
この、`async`関数なのに`await`を使わないせいで、処理が止まらずに次の処理へと進んでしまうことを「非同期処理の投げっぱなし」と言います。

```typescript
fakeTileToRealTile(tile.tile).then((realTile) => {
  this.realtiles[l][t] = {
    x: tile.x,
    y: tile.y,
    tile: realTile,
  };
});
```

このコードは、`fakeTileToRealTile`関数が終わったら`then`内の処理を実行するということです。
この時、`realTile`は`Promise<RealTile>`ではなく、`RealTile`になります。これは、`then`を使ったために`Promise`が解決されているからです。
しかしながら、「終わったら`then`内の処理をしてね」ということが書いてあるだけで、それを理解した`for`ループは終わるのは待たずに次のループへと進みます。

最終的にこの関数では、`then`内の処理が全て終わることはなく、ループが終わり、`generateMapTiles`メソッドが終了してしまいます。
そのため、`this.realtiles`は`generateMapTiles`メソッドが終了したときには不完全な場合があります。

その不完全な状態で`draw`を行ったらどうなるかはわかりますよね～...

では、どうすればいいのでしょうか？

# `Promise`を使ってみよう

`Promise`は、非同期処理を行うためのクラスです。
今までの`Promise<T>`と同じ`Promise`です。
このクラスを使うことで、非同期処理をより高度に扱うことが出来ます。

では、もう早速答えを指し示してしまいましょう。

```typescript
abstract class GameMap {
  ...

  protected generateMapTiles() {
    let loadable: number = 0;
    let loaded: number = 0;

    for (let l = 0; l < this.tiles.length; l++) {
      if (!this.realtiles[l]) this.realtiles[l] = [];
      for (let t = 0; t < this.tiles[l].length; t++) {
        const tile = this.tiles[l][t];
        loadable++;
        fakeTileToRealTile(tile.tile).then((realTile) => {
          this.realtiles[l][t] = {
            x: tile.x,
            y: tile.y,
            tile: realTile,
          };
          loaded++;
        });
      }
    }

    return new Promise((resolve) => {
      const interval = setInterval(() => {
        if (loaded === loadable) {
          clearInterval(interval);
          resolve();
        }
      }, 100);
    });
  }

  ...
}
```

まず、読み込まれるべきタイルの数を`loadable`という変数で管理します。
そして、`then`内で読み込みが終わったら`loaded`を増やします。
つまり、`loadable`と`loaded`が一致したら全てのタイルが読み込まれたということです。

`return new Promise`とありますが、これは`Promise`インスタンスを返すということです。
`Promise`は、`resolve`と`reject`を引数に取るコールバック関数を引数に取ります。
そのうち`reject`は省略可能です。

```typescript:コールバック関数の中身
const interval = setInterval(() => {
  if (loaded === loadable) {
    clearInterval(interval);
    resolve();
  }
}, 100);
```

この`resolve`と`reject`は、指定したコールバック関数の中で呼び出される関数になります。
そのうち、`resolve`を呼び出すと、処理を解決したことにし、`Promise`を完了させます。
`reject`は、処理が何らかの理由で失敗してしまったことにして、`Promise`を失敗させます。(呼び出し側でエラーとなる)

今回は、`setInterval`という、インターバルで処理を行うものを使って、100msごとに確認しています。
`loaded`と`loadable`が一致したら、`clearInterval`でインターバルの処理を終了させて（もうこれ以降はインターバル処理が行われなくなるものであって、即座にすべてのインターバル処理が停止するようなものではない。）、`resolve`を呼び出して`Promise`を完了させます。

「`Promise`が完了」すると、この`generateMapTiles`を呼び出した側でいうと、`await generateMapTiles()`が終了し、次の処理へと進むことが出来ます。

# じゃあそれを反映させてメインを書き直そう

もう一度、お試しのためだけに`generateMapTiles`を`public`にしましょう。

```typescript:src/main.ts
import { Game } from "./lib/game";
import { SampleMap } from "./maps/sample";

let game: Game;

const main = async () => {
  game = new Game();
  game.changeMap(SampleMap);
  await game.map?.generateMapTiles();
  game.map?.draw();
};

window.addEventListener("DOMContentLoaded", () => {
  main();
});
```
ちなみに、このコードにおける`main`関数も`async`関数になります。
`window.addEventListener("DOMContentLoaded", () => { ... })`の中で`main`関数を呼び出していますが、`await`は付けず、投げっぱなしで処理しています。
投げっぱなしでも問題がないのは、それ以降に処理がないからです。

これで、タイルが正しく描画されるはずです。

```shell
$ pnpm tauri dev
```

# まとめ

少し高度な非同期処理を行うために、`Promise`を使ってみました。

ちなみになんですが、「そもそも`fakeTileToRealTile`を`await`で呼び出せば、うまくいくのでは？」とお思いでしょうか。
確かに、以下のコードでも描画は出来ます。

```typescript
abstract class GameMap {
  ...

  protected async generateMapTiles() {
    for (let l = 0; l < this.tiles.length; l++) {
      if (!this.realtiles[l]) this.realtiles[l] = [];
      for (let t = 0; t < this.tiles[l].length; t++) {
        const tile = this.tiles[l][t];
        this.realtiles[l][t] = {
          x: tile.x,
          y: tile.y,
          tile: await fakeTileToRealTile(tile.tile);,
        };
      }
    }
  }

  ...
}
```

一方で、これでは`fakeTileToRealTile`の処理が終わるまで次の処理に進みません。
つまり、1タイルごとに画像を読み込み、処理を進めていくことになります。

今回の`Promise`を使ったコードであれば、全てのタイルの読み込み処理を一度に行うため、並列的に画像読み込み処理が行われます。
そのため、より素早くタイルを読み込むことが出来るのです。多分。
(あんまりI/Oには詳しくないのだ...)