【Day10】タイルをマップに埋め込ませましょう【QAC24】


# マップにタイル（の情報）を埋め込む

前回の記事でクラスについて触れていきましたが、「次回説明する」と言って省略したアクセス修飾子について触れていきます。

## アクセス修飾子

前回の記事で`public`とか`private`とか書いていましたが、これはアクセス修飾子といって、クラスのメンバーに対してアクセス制限をかけるものです。

これはどういうことかというと、コードを見てもらいましょう。

```typescript
class Person {
  private privateData: string = "ふっふっふ...人間は私が作り上げたのだよ";
  public name: string = "アダム";
  protected family: string[] = ["イブ", "ソーン"];
  public favorite: string = "リンゴ";
  protected protectedData: string = "これが人類にだけ教えてあげる...";
}

class Tanaka extends Person {
  public name: string = "太郎";
  protected family: string[] = ["次郎", "三郎"];
  public favorite: string = "はちみつ";
  private myNumber: number = 123456789;
}

class Yamada extends Person {
  public name: string = "花子";
  protected family: string[] = ["花女", "花男"];
  public favorite: string = "カレー";
  private myNumber: number = 987654321;
}
```

このように`Person`クラスとそれを継承した`Tanaka`クラス、`Yamada`クラスを作成しました。

まず、アクセス修飾子には`private`、`protected`、`public`の 3 つがあります。

- `private`：そのクラス内でのみアクセス可能
- `protected`：そのクラスとそのサブクラス内でのみアクセス可能
- `public`：どこからでもアクセス可能

今回、他人に知られてはいけない情報として、`privateData`と`myNumber`を`private`で宣言しています。

```typescript
const adam = new Person();
const taro = new Tanaka();
const hanako = new Yamada();

console.log(adam.privateData); // エラー
console.log(taro.myNumber); // エラー

console.log(taro.privateData); // エラー
```

このように、`private`で宣言されたメンバーには、そのクラス外からアクセスすることができません。
アクセスできるのはそのクラス内なので、それぞれのクラスに`showMyNumber`メソッドを追加してみましょう。

```typescript
...

class Tanaka extends Person {
  public name: string = "太郎";
  protected family: string[] = ["次郎", "三郎"];
  public favorite: string = "はちみつ";
  private myNumber: number = 123456789;

  public showMyNumber() {
    console.log(this.myNumber);
  }
}

...

console.log(taro.showMyNumber()); // 123456789
```

で、その`public`とは、どこからでもアクセスできるということです。

```typescript
console.log(adam.name); // アダム
console.log(taro.name); // 太郎
console.log(hanako.name); // 花子
```

`protected`は、そのクラスとそのサブクラス内でのみアクセス可能ということなので、`family`については、`Person`クラスとそのサブクラスである`Tanaka`クラス、`Yamada`クラスでアクセス可能です。

```typescript
class Person {
  private privateData: string = "ふっふっふ...人間は私が作り上げたのだよ";
  public name: string = "アダム";
  protected family: string[] = ["イブ", "ソーン"];
  public favorite: string = "リンゴ";

  public showFamily() {
    console.log(this.family);
  }

  public showProtectedData() {
    console.log(this.protectedData);
  }
}

console.log(adam.family); // エラー
console.log(taro.family); // エラー
console.log(hanako.family); // エラー

console.log(adam.showFamily()); // ["イブ", "ソーン"]
console.log(taro.showFamily()); // ["次郎", "三郎"]
console.log(hanako.showFamily()); // ["花女", "花男"]

console.log(adam.protectedData); // エラー
console.log(taro.protectedData); // エラー

console.log(adam.showProtectedData()); // これが人類にだけ教えてあげる...
console.log(taro.showProtectedData()); // これが人類にだけ教えてあげる...
```

このように、外からはアクセスできないが、そのクラス(継承も含む)内であればアクセスできるということです。

...というよくわからない説明がきっとサバイバルガイドだと分かりやすいと思うので貼っておきます。

https://typescriptbook.jp/reference/object-oriented/class/access-modifiers

これらのアクセス修飾子をうまく使い分けることで、プログラムの安全性をいい感じに高められます。

# タイルをマップに埋め込む

という訳で埋め込むめに必要な情報は何でしょうか...

まずは`tile.ts`内の`TileSize`、`FakeTile`、`RealTile`、`fakeTileToRealTile`を全て`export`してから、`map.ts`にインポートしていきます。

```typescript:src/lib/map.ts
import { TileSize, FakeTile, RealTile, fakeTileToRealTile } from "./tile";
```

マップ上にタイルを描画するために必要な情報は`FakeTile`(タイルマップへの画像パスと、そのタイルマップ内でのタイルを示す座標)に加えて、マップのどこに描画するかです。
この 2 の情報をまとめたオブジェクト型`MapTile`を作成しましょう。

```typescript
type MapTile = {
  x: number;
  y: number;
  tile: FakeTile;
};
```

では、`GameMap`クラスに`tiles`プロパティを追加しましょう。

```typescript
abstract class GameMap {
  protected tiles: MapTile[][] = [];
}
```

今回は`MapTile[][]`として、2 次元配列でタイルを管理します。
理由は、マップタイルをレイヤー構造で管理するためです。（後で必要になったときに便利。）

まだ、この`MapTile`は`FakeTile`で、描画はこのままではできません。
そこで、同様に`RealTile`を含んだ`DrawableTile`型を作成しましょう。

```typescript
type DrawableTile = {
  x: number;
  y: number;
  tile: RealTile;
};
```

そして、`GameMap`内で`MapTile[][]`を`DrawableTile[][]`に変換するメソッドを追加しましょう。

```typescript
abstract class GameMap {
  protected tiles: MapTile[][] = [];
  protected realtiles: DrawableTile[][] = [];

  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,
          };
        });
      }
    }
  }
}
```

こんな感じでしょうか？
`for`ループを使って、`MapTile[][]`を`DrawableTile[][]`に変換しています。
`if (!this.realtiles[l]) this.realtiles[l] = [];`は、`realtiles`の現在のレイヤーが存在しない場合に、新しく配列を作成しています。
そうでないと、存在しない要素にアクセスしようとしてエラーが発生してしまいます。

また、`fakeTileToRealTile(tile.tile).then((realTile) => { ... }`として、`fakeTileToRealTile`関数を`then`で実行しています。
これは、`await`を使わないで`async`関数を実行する方法の 1 つで、その`async`関数が終わったら`then`内の処理を実行するというものです。

# 描画を試すためにメイン処理を書いてみる

今まで手つかずだった`index.html`と`main.ts`を書いていきます。

```html:index.html
<!doctype html>
<html lang="ja-JP">
  <head>
    <meta charset="UTF-8" />
    <link rel="stylesheet" href="/src/styles.css" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>qac24-sample</title>
    <script type="module" src="/src/main.ts" defer></script>
  </head>

  <body>
    <!-- メインのゲーム画面を表示するCanvas -->
    <div id="main">
      <canvas id="game" width="960" height="960">
        HTML5 Canvasがサポートされていません。<br>
        OSのアップグレードをご確認ください。
      </canvas>
    </div>
  </body>
</html>
```

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

let game: Game;

const main = async () => {
  game = new Game();
};

window.addEventListener("DOMContentLoaded", () => {
  main();
});
```

出来上がったのがこちらです。

更に、なんか適当なマップを作ってみましょう。

`src`に`maps`フォルダを作成し、その中に`sample.ts`を作成します。

```typescript:src/maps/sample.ts
import { GameMap } from "../lib/map";
```

あ、`GameMap`の export を忘れていましたね。

```typescript:src/lib/map.ts
export abstract class GameMap {
  ...
}
```

`GameMap`を継承した`SampleMap`クラスを作成しましょう。
継承は`extends`を使います。
なお、継承の際のルールとして、`abstract`なメソッドやプロパティがある場合は、それらを実装する必要があります。今回はないですね。

```typescript:src/maps/sample.ts
import { GameMap } from "../lib/map";

export class SampleMap extends GameMap {
  protected tiles = [[
    {
      x: 0,
      y: 0,
      tile: {
        path: "/src/assets/rpg/Graphics/Tilesets/001-Grassland01.png",
        x: 1,
        y: 3,
        width: 1,
        height: 1,
      },
    },
    {
      x: 1,
      y: 0,
      tile: {
        path: "/src/assets/rpg/Graphics/Tilesets/001-Grassland01.png",
        x: 1,
        y: 3,
        width: 1,
        height: 1,
      },
    },
    {
      x: 0,
      y: 1,
      tile: {
        path: "/src/assets/rpg/Graphics/Tilesets/001-Grassland01.png",
        x: 1,
        y: 3,
        width: 1,
        height: 1,
      },
    },
    {
      x: 1,
      y: 1,
      tile: {
        path: "/src/assets/rpg/Graphics/Tilesets/001-Grassland01.png",
        x: 1,
        y: 3,
        width: 1,
        height: 1,
      },
    }
  ]]
}
```

これで、`SampleMap`という 4 マスしかないマップが作成されました。

※画像パスについては、お使いの環境に合わせてください。
　今回は Day 7 で解説した通り、RPG Maker XP の RTP を使用しています。

描画するための`draw`メソッドを`GameMap`に追加しましょう。

```typescript:src/lib/map.ts
export abstract class GameMap {
  ...

  public draw() {
    const canvas = document.getElementById("game") as HTMLCanvasElement | null;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    if (!ctx) return;

    ctx.clearRect(0, 0, canvas.width, canvas.height);

    this.realtiles.forEach((tiles) => {
      tiles.forEach((tile) => {
        tile.tile.draw(
          tile.x * TileSize,
          tile.y * TileSize,
          ctx
        )
      });
    });
  }

  ...
}
```

これで、`SampleMap`の`draw`メソッドを呼び出すことで、マップが描画されるようになりました。

```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);
  game.map?.generateMapTiles();
  game.map?.draw();
};

window.addEventListener("DOMContentLoaded", () => {
  main();
});
```

なんかしらん`?`が出てきましたね。

先ほどの`game.ts`の定義内で、`map`プロパティを`GameMap | null`としていました。
そのため、`map`が`null`の場合もあります。
その場合、`map`のメソッドを呼び出すと`null`から存在しないものを呼び出しているとなってエラーが発生してしまいます。

そのため、`?`をつけることで、`map`が`null`の場合はそのメソッドを呼び出さないようにしています。

そして、現在では`generateMapTiles`が`protected`になっているので、とりあえずお試しするためだけに`public`に変更しておきましょう。

```typescript:src/lib/map.ts
export abstract class GameMap {
  ...

  public generateMapTiles() {
    ...
  }

  ...
}
```

では、`pnpm tauri dev`を実行して、確認してみましょう。

...

あれ？
画面が真っ白ですよ？

# まとめ

現在まで適切にかけていれば、エラー無く動作すると思います。
しかしながら、エラーはないものの、描画されているはずのタイルが描画されていません。

これは、画像の読み込みが完了していないためです。
いやいや、`generateMapTiles`で`fakeTileToRealTile`を`then`で実行しているじゃないかと思うかもしれませんが...これは特殊な処理なのです。

という訳で、次回は非同期処理の話をさらに深めていきます。