【Day7】落ち穂と素材は拾った方がいい。【QAC24】


# ミレー

前回はサボって申し訳ない。
今回もサボります。

# タイルとは？

RPG のマップは、タイルという小さな画像が何枚も連続して配置されることで描画されます。
草原であれば、草のタイル画像を何枚も並べて描画します。

# タイルセット

んでそのタイル画像が集まった画像がタイルセットと呼ばれています。
基本はこのタイルセットを素材として用意することで、マップを作成します。
マップだけではなく、スマホゲームなんかでも使われていましたね。

そして、このタイルセット素材なのですが、フリー素材として提供されていることもあります。
調べてみると、様々な方が Booth などで提供されています。

# 3 分タイルセットクッキング

というわけで 3 分でタイルセットを用意しましょう ♪

まずは RPG Maker XP のインストールフォルダを開きます（？？？？？）

## 突然の RPG Maker XP？

RPG Maker XP というのは、RPG Maker (RPG ツクール) シリーズの作品のひとつで、少し前に Steam で無料配布されていました。
無料素材を探すのが面倒なので、今回はここから素材を拝借して使います。

ちなみに、RPG Maker の素材については、RPG Maker 以外でも使えるということが規約に書かれています
詳しくは規約を確認してくださいね。

それでは、RPG Maker XP などがない人は、無料素材を探してみてください。
素材の種別は特に問いません。
ただ、今回はタイルサイズは 32x32 とさせていただきます。

今回は`src/assets/rpg`の中に RPG Maker XP の RTP を置いておきました。

## タイルを管理するコード

今、`main.ts`がある場所に`lib`というフォルダを作りましょう。
その中に`tile.ts`を作成します。

```typescript
const TileSize = 32;
```

まずはタイルサイズを定義しておきましょう。

先の記事で Canvas に画像を描画する方法を紹介しましたが、その`Image`クラス(正確には`HTMLImageElement`)をタイルに保管します。
...そんなオブジェクト...の型を定義しましょう。

```typescript
type Tile = {
  image: HTMLImageElement;
  x: number;
  y: number;
  width: number;
  height: number;
};
```

このオブジェクトは、タイルセットの画像と、そのうちの対象となるタイルの特定の座標から特定のサイズを保管するものです。
これだけだとちょっとまだ頼りないので、さらに Canvas に描画する関数も追加しましょう。

Canvas に描画するために必要なものは、Canvas の Context と、描画位置、描画する画像、その画像のうちどの部分を描画するかの情報です。

```typescript
const drawTile = (
  tile: Tile,
  x: number,
  y: number,
  ctx: CanvasRenderingContext2D,
  width?: number,
  height?: number
) => {
  ctx.drawImage(
    tile.image,
    tile.x,
    tile.y,
    tile.width,
    tile.height,
    x,
    y,
    width,
    height
  );
};
```

まぁ安直に書いたらこうなりますよね。
これを Tile オブジェクトに追加しましょう。

`width`と`height`なのですが、これはキャンバスに描画する際のサイズです。
通常、画像サイズと同じで良いので、省略できるようにします。
（引き伸ばしなどをしなければならない場合は別）
省略できる引数は`?`を付けることで実現できます。

そのために、「タイルセットへの画像パス」、「タイルの位置・サイズ」から Tile オブジェクトを生成する関数を作ります。

```typescript
const getTile = (
  path: string,
  x: number,
  y: number,
  width: number,
  height: number
): Tile => {
  const image = new Image();
  image.src = imagePath;
  return { image, x, y, width, height };
};
```

これでは、画像の読み込みが終わる前にオブジェクトを返しており、実際に描画が出来るかどうかの保証がありません。
そこで`image.decode()`を使います。

これは、画像の読み込みが終わるまで待機するメソッドです。

```typescript
const getTile = (
  path: string,
  x: number,
  y: number,
  width: number,
  height: number
): Tile => {
  const image = new Image();
  image.src = imagePath;
  image.decode();
  return { image, x, y, width, height };
};
```

と、一般に考えたらこうなりますが、`decode()`というのは非同期処理なのです。

## 非同期処理！？

2 年前のアドベントカレンダーではマジで軽くしか触れませんでしたが、それは私が Python の非同期処理に対しての知見が浅かったからです。
TypeScript の非同期処理は、Python よりも比較的簡単です。

まず概要として、非同期処理というのは主に I/O 処理などで、どうしても待たなければならない処理がある場合に使用されます。

例えば、あるファイルをダウンロードしていある間は、I/O が処理を行って、CPU は完了まで待機しているような状態です。
その間に、CPU に他の仕事をさせてあげれば、効率が良くなるよね～というものが非同期処理の考え方...です！多分。

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

非同期処理に対応した関数を非同期で使うには、`await`というキーワードを使います。
ただ、`await`を使うには、そのスコープが`async`である必要があります。
そのために`getTile`も`async`関数に変更しました。
（その`async`関数が非同期処理に対応した関数なので、これを実行するにはさらに`async`内で実行しなけらばならず...の無限ループなのです）

また、`async`関数の返り値は`Promise<T>`型になります。
は？T って何？ってなると思いますが、これはジェネリクスという考え方で...今回は`Promise<Tile>`が、`Tile`型を返す`Promise`であることを覚えておいてください。

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

...こんなもんで分かったかな？

## Tile に今の関数をうめこもう

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

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

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

まず、`Tile`に`draw`メソッドを追加しました。
ここには実装は書けず、関数の型のみを書くことが出来ます。

実際の`getTile`の中身ですが、まず`tileInfo`というオブジェクトを作ります。
これは、`Tile`オブジェクトに含まれる`x`、`y`、`width`、`height`をまとめたものです。
これを`...tileInfo`で展開して`Tile`オブジェクトに追加します。
`Tile`は`image`、`x`、`y`、`width`、`height`、`draw`を持つオブジェクトになります。

と、このようにオブジェクトを作成することで、色々な処理を行うことが出来ます。
これは、オブジェクト指向の考え方に少しだけ近いようなもの...ですかね多分。

`class`を使わなかった理由は、なんとなくたくさんの数のクラスをインスタンス化するとボトルネックになるかなと思ったからです。

次回以降は、この`Tile`オブジェクトをさらに拡張していき、より使いやすいものにしていきます。

# あとがき

うわ～計画狂っちゃったよ！！
この記事でこんなに書くとは思わなかった！！
明日の分のネタ無くなっちゃったよ！！