【Day8】タイルを描画してみよう【QAC24】


# そのタイル、あなたならどうやってデータとして保管する？

前回の記事で Tile Type を作成しました。

```typescript
type Tile = {
  image: HTMLImageElement;
  x: number;
  y: number;
  width: number;
  height: number;
  draw: (
    x: number,
    y: number,
    ctx: CanvasRenderingContext2D,
    width?: number,
    height?: number,
  ) => void;
};
```

ゲームのマップには、このタイル情報をどこかに保存する必要があります。
最終的にはマップデータにこのタイルデータを埋め込んで、さらにこれらを TypeScript のファイルとして（多分）保存しておくことになるのですが、この`HTMLImageElement`や`draw`関数をゴロゴロと保存するのはどうなのでしょうか？
特に`HTMLImageElement`に関してはそもそも DOM なので保存することは出来ないでしょう。

ただ、`getTile`関数があるのでそれを活用するための新しい型を作っていきましょう。

# `シリアライズ`とは？

TypeScript 含め、様々なプログラミング言語において JSON (`JavaScript Object Notation`) というデータ形式が対応しています。
JSON とは、JavaScript のオブジェクトをほぼそのまま文字列として表現したもので、多くの言語にてデータを保管する目的で使用されています。
他にも、WebAPI で使用されていて......兎にも角にも！
データを文字列としてあらわすときに JSON は非常によく使われています！

TypeScript において、データを JSON 文字列に変換する関数の`JSON.stringify`がデフォルトで用意されており、何も考えずに使うことが出来ます。

この時、JSON 化できない関数などのデータを含んでいると、`JSON.stringify`は不正確なデータを返します。（JSON 化されたものを元に戻そうとしても元のデータと一致しない）
これと逆で、JSON 化してからデータの状態に戻しても元のデータと一致するようなデータを`シリアライズ`可能なデータと言います。

TypeScript において、何を基準にシリアライズ可能になっているかというと、そのデータの型がすべてを決めています。
(Rust では`Serialize`トレイトを実装しているかどうかで判断されます)

# 新しい Tile Type を作ろう

`getTile`関数を活用できるような、新しいシリアライズ可能な型を作っていきましょう。

そのため、これまでの`Tile`型の名前を`RealTile` (描画できちゃう Real な Tile) と改名して、その対となる`FakeTile` (描画できない Fake な Tile) を作りましょう。

```typescript
type FakeTile = {
  path: string;
  x: number;
  y: number;
  width: number;
  height: number;
};

type RealTile = {
  image: HTMLImageElement;
  x: number;
  y: number;
  width: number;
  height: number;
  draw: (
    x: number,
    y: number,
    ctx: CanvasRenderingContext2D,
    width?: number,
    height?: number,
  ) => void;
};
```

`FakeTile`は`RealTile`と違い、`image`の代わりに`path`を持っています。
これは、`getTile`に渡すための情報を持っているからですね～

これでもいいのですが、`x`、`y`、`width`、`height`は`RealTile`と`FakeTile`で共通しているので、これらを共通化して新しい型を作りましょう。

```typescript
type AbstractTile = {
  x: number;
  y: number;
  width: number;
  height: number;
};

type FakeTile = {
  path: string;
} & AbstractTile;

type RealTile = {
  image: HTMLImageElement;
  draw: (
    x: number,
    y: number,
    ctx: CanvasRenderingContext2D,
    width?: number,
    height?: number,
  ) => void;
} & AbstractTile;
```

このように、共通部分を`AbstractTile` (抽象的な Tile) として定義し、`FakeTile`と`RealTile`でそれを継承することで、コードの重複を減らすことが出来ます。

(本来の Abstract のプログラミング的な意味とは少し離れている気もしますが、本物のプログラミング的な Abstract についても別の記事で紹介していきます。)

# 相互に変換可能にしてあげよう

現在、`getTile`関数は`path`、`x`、`y`、`width`、`height`を引数に取り、`RealTile` (正確には`Promise<RealTile>`) を返しています。
これは、`FakeTile`を`RealTile`に変換する関数のように見えますが、それでは`FakeTile`を引数にとることが通常望ましいと考えます。

そのため、`FakeTile`から`RealTile`に変換する関数を作りましょう。

```typescript
const fakeToRealTile = async (fakeTile: FakeTile): Promise<RealTile> => {
  return await getTile(
    fakeTile.path,
    fakeTile.x,
    fakeTile.y,
    fakeTile.width,
    fakeTile.height
  );
};
```

なんかショートカットみたい。

また、その逆として`RealTile`から`FakeTile`に変換するものも作りたいですね。
`RealTile`はシリアライズ出来ないことを事前に考えているので、`RealTile`の中に`FakeTile`に変換する関数を入れちゃいましょう。

そのためには、`RealTile`に`getFakeTile`関数の定義を追加し、`getTile`の中を修正しましょう。

```typescript
type RealTile = {
  image: HTMLImageElement;
  draw: (
    x: number,
    y: number,
    ctx: CanvasRenderingContext2D,
    width?: number,
    height?: number,
  ) => void;
  getFakeTile: () => FakeTile;
} & AbstractTile;

...

const getTile = async (
  path: string,
  x: number,
  y: number,
  width: number,
  height: number
): Promise<RealTile> => {
  const image = new Image();
  image.src = path;

  const tileInfo = {
    x,
    y,
    width,
    height,
  };

  await image.decode();

  return {
    image,
    ...tileInfo,
    draw: (
      x: number,
      y: number,
      ctx: CanvasRenderingContext2D,
      width: number = tileInfo.width,
      height: number = tileInfo.height,
    ) => {
      ctx.drawImage(
        image,
        tileInfo.x,
        tileInfo.y,
        tileInfo.width,
        tileInfo.height,
        x,
        y,
        width,
        height
      );
    },
    getFakeTile: () => {
      return {
        path,
        ...tileInfo,
      };
    },
  };
};
```

`getTile`の下の方に`getFakeTile`関数を追加し、`FakeTile`を返すようにしました。

# 仕上げ！

タイルサイズが32であることはすでに決まっていることなので、x座標やy座標、サイズの単位をpxからタイルにした方が後々扱いやすくなると思うので、途中に計算過程を含ませましょう。

```typescript
const getTile = async (
  path: string,
  x: number,
  y: number,
  width: number,
  height: number
): Promise<RealTile> => {
  const image = new Image();
  image.src = imagePath;

  const tileInfo = {
    x: x * TileSize,
    y: y * TileSize,
    width: width * TileSize,
    height: height * TileSize,
  };

  await image.decode();

  return {
    image,
    ...tileInfo,
    draw: (
      x: number,
      y: number,
      ctx: CanvasRenderingContext2D,
      width: number = tileInfo.width,
      height: number = tileInfo.height,
    ) => {
      ctx.drawImage(
        image,
        tileInfo.x,
        tileInfo.y,
        tileInfo.width,
        tileInfo.height,
        x,
        y,
        width,
        height
      );
    },
    getFakeTile: () => {
      return {
        path,
        ...tileInfo,
      };
    },
  };
};
```

`getTile`の引数の単位をタイルとして、中で`TileSize`を掛け算することで、px単位に変換しています。

`draw`の内部では計算処理が行われておらず、px単位になっているのには理由があります。
タイルは32x32ですが、実際に描画される際にはアニメーションなどが行われます。
32px進むまでに線形補間が行われるため、px毎に描画されることになります。
そのため、`draw`関数の引数はpx単位で受け取るようにしています。


# おわりに
私のレポジトリの`tile.ts`とほぼ同じ内容になりました。
これで、タイル部分は完璧ですかね。

次回からマップについて作り始めていきたいと思います。