【Day13】ExtendsなMapでEditorをCreateしよう【QAC24】


# マップエディター！

ゲーム制作にはエディターは欠かせないと思います。
RPG ゲームにおいては、マップエディターが特に重要です。

Day 10 で設定した`GameMap.tiles`にはマップのタイルデータが入っていますが、これを編集するためのエディターを作成していきます。

# まずは新規マップ！

`GameMap`を継承した`EditorMapMain`クラスでも作成しましょう。

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

export class EditorMapMain extends GameMap {
  ...
}
```

多分適切な拡張機能を入れていれば、`abstract`なメソッドやプロパティの中で、現在の`EditorMapMain`に実装されていないものがエラーとして出てくると思います。
そこらへんは適当に空の配列とか空データを入れておきましょう。

# タイルを選択する機能

マップエディターは、イメージとしてはタイルを絵のように描いていくようなものです。多分。
そこで今回私は、モードセレクタを作成して、それぞれのモードに応じた描画やイベントを実装していきました。

詳細については、省きますので自分で好きな方法で実装してみてください。

## マウスの位置を取得する

TypeScript において、マウスの位置 (ブラウザ上のマウスの位置) を取得するために、まずは`MouseEvent`を取る、マウス系イベント(`mousemove`, `mousedown`, `mouseup`)を登録します。
とりあえず`mousemove`だけ登録してみましょう。

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

  protected mousemove(e: MouseEvent) {
    console.log(e.clientX, e.clientY);
  }

  ...
}
```

Day 12 で説明したように、コンストラクタにイベントのバインド処理を追加しておいてください。

`MouseEvent`の`clientX`と`clientY`は、ブラウザ上のマウスの位置を取得することができます。
実際に動かしてみましょう。

これでブラウザ上におけるマウスの位置を取得できます。
マップエディターにこれを搭載するには、マウスの位置をマップ上の位置に変換する必要があります。

マウスの座標は、ブラウザの左上 (ビューポートの左上) が原点ですが、それをマップの左上が頂点になるように変換するには、簡単な計算をする必要があります。

`マップ上の座標 = (ブラウザ上のマウスの座標 - キャンバスエレメントの位置) * (キャンバスの仮想サイズ / キャンバスの実サイズ)`

実際にコードとして書き起こすのであれば、以下のようになります。

```typescript
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) * (canvas.width / rect.width);
const y = (e.clientY - rect.top) * (canvas.height / rect.height);
```

`getBoundingClientRect`というのは、エレメントの実際の位置やサイズを取得するメソッドです。
`canvas.width`と`rect.width`は、それぞれキャンバスの仮想サイズと実サイズです。
仮想サイズというのは Day 4 なんかで指定した`<canvas>`の`width`と`height`のことです。
実サイズというのは、ブラウザ上で表示されているキャンバスのサイズです。

これによって、マウスの位置をマップ上の位置に変換することができます。

> `(e.clientX - rect.left)`だけでも、キャンバスの左上からの相対位置を取得できます。
> が、これだけだと単位が合わなくなり、マップ上の右下が 300 なはずなのに、ブラウザの大きさによって 300 を超えたり、300 を下回ったりしてしまいます。

## 単位変換

マップ上におけるマウスの座標を px 単位で取得することが出来ました。
が、このゲームはタイル単位でマップを描画しているので、マウスの座標をタイル単位に変換しておくと大変便利だと思います。

マウスの座標をタイル単位に変換するには、以下のように計算します。

```typescript
const tileX = Math.floor(x / TileSize);
const tileY = Math.floor(y / TileSize);
```

まぁ簡単ですね。

`Math.floor`は小数点以下を切り捨てる関数です。
単位はタイルなので整数である必要があります。
そこで、`1.5`タイルという数値が出てきたとき、左端が`0`タイル(少数もつけるのであれば`0.0～0.9`)、その 1 つ右が`1`(`1.0～1.9`)タイルとなっているので、切り捨てを行います。

## それぞれを纏めてあげましょう

今回私は`dom.ts`に置いておきましたが、まぁ分かればどこでもいいと思います。

いい機会ですので、`canvas.ts`とかに Canvas を取得する関数、コンテキストを取得する関数でも置いておいても便利かもしれません。

```typescript:/src/lib/canvas.ts
import { TileSize } from "./tile";

/**
 * Canvasのサイズを取得する
 * @param tiles Canvasに描画される予定のタイル数 (1辺のタイル数)
 * @returns Canvasのサイズ (px)
 */
export const getCanvasSize = (tiles: number = 33) => {
  return TileSize * tiles;
};

/**
 * Canvas要素を取得する
 * @returns Canvas要素
 */
export const getCanvas = () => {
  const canvas = document.getElementById("game") as HTMLCanvasElement | null;
  if (!canvas) {
    throw new Error("Canvas not found");
  }
  return canvas;
};

/**
 * Canvasの2Dコンテキストを取得する
 * @returns Canvasの2Dコンテキスト
 */
export const getContext = () => {
  const canvas = getCanvas();
  const ctx = canvas.getContext("2d");
  if (!ctx) {
    throw new Error("Canvas context not found");
  }
  return ctx;
};

/**
 * Canvasの描画をクリアする
 * @param tiles Canvasに描画される予定のタイル数 (1辺のタイル数)
 */
export const clearCanvas = (tiles: number | null = null) => {
  const canvasSize = tiles ? getCanvasSize(tiles) : getCanvasSize();
  const ctx = getContext();
  ctx.clearRect(0, 0, canvasSize, canvasSize);
};
```

ここで`tiles`とかいう`number`型の引数が出てきているのですが、これは 1 辺のタイル数を指定する引数です。
通常は使わないんですけど...なんかどうしても 1 辺のタイルサイズを変更したいな～...って時があるかもしれないので。

```typescript:/src/lib/dom.ts
import { getCanvasSize, getCanvas } from "./canvas";
import { TileSize } from "./tile";

/**
 * Canvas上での相対的なマウス座標を取得する
 * @param e EventListenerで取得したMouseEvent
 * @returns マウス座標
 */
export const getMousePosition = (
  e: MouseEvent,
  canvasTiles: number | null = null
) => {
  const canvasSize = canvasTiles ? getCanvasSize(canvasTiles) : getCanvasSize();
  const rect = getCanvas().getBoundingClientRect();
  const x = (e.clientX - rect.left) * (canvasSize / rect.width);
  const y = (e.clientY - rect.top) * (canvasSize / rect.height);
  const isInside = x >= 0 && y >= 0 && x < canvasSize && y < canvasSize;
  return { x, y, isInside };
};

/**
 * マウス座標からCanvas上の座標を取得する
 * @param x マウスのX座標
 * @param y マウスのY座標
 * @returns Canvas上の座標 (px)
 */
export const getCanvasPosition = (x: number, y: number) => {
  const canvasX = Math.floor(x / TileSize);
  const canvasY = Math.floor(y / TileSize);
  return { x: canvasX, y: canvasY };
};
```

イベントを渡すことで、マップ上の座標に変換してくれる関数`getMousePosition`と、その座標をタイル単位に変換してくれる関数`getCanvasPosition`を作成しました。

`getMousePosition`の返り値には、`isInside`というプロパティがあります。
これは、マウスがキャンバスの中にあるかどうかを示すフラグです。

マウスがキャンバス外にある場合、`x`や`y`はマイナスの値になるのですが、これをそのまま扱ってしまわないように...(「キャンバス外の場合は処理を行わない」など応用できるように)

# 選択できそうな感じにしてみましょう

これらを応用して、マウスが動いた時に該当のタイル座標を光らせてみましょう。

```typescript:/src/maps/editorMap.main.ts
export class EditorMapMain extends GameMap {
  ...

  protected mousemove(e: MouseEvent) {
    const { x, y, isInside } = getMousePosition(e);
    if (!isInside) return;

    const { x: canvasX, y: canvasY } = getCanvasPosition(x, y);
    console.log(canvasX, canvasY);

    clearCanvas();
    this.draw();
    this.ctx.fillStyle = "rgba(255, 0, 0, 0.5)";
    this.ctx.fillRect(canvasX * TileSize, canvasY * TileSize, TileSize, TileSize);
  }

  ...
}
```

これでどうでしょうか。
マウスが動いた時に、そのタイルを光らせることができるようになりました。

んで、実際に座標も取得できたし、タイルを光らせることもできたし...まさにお絵かきソフトが出来上がってきたような感じですね。
ところでみなさん、絵の具を使うときに必要なもの...あと何が足りませんか？

というのを次回の次回にやるために、次回は少し休憩がてら`class`の機能についてお話していきましょう。