【Day19】「なぞのばしょ」を壁で隔よう【QAC24】


# なぞのばしょとは？

某ゲームで有名な、普通は立ち入れない場所の事です。
そこに行かせないために、マップには壁が必要になりますよね。

今回はコリジョン (`Collision`) という概念を導入して、プレイヤーが壁にぶつかった時に進めないようにします。

# コリジョンとは？

訳すなら「衝突」ですが、ゲームとか 3D モデリングの世界だと「当たり判定」という意味でも使われますよね。

コリジョンの座標を格納するプロパティを作って、Actor が進んだときにその進む先の座標が格納されていれば進めない...みたいな感じなのがいいですよね。

# `colllision`

何にも考えずに`collision`プロパティを作って座標の配列を格納しておきます。

> ここで座標とは`[-actorx, -actory]`のタプルになります。
> (前回のコードだと私が変数の意味を逆転されているので、正負が逆になってしまう)

座標にも触れたので、現在の座標(`Position`Type)を返す getter も作っておきます。

また、コリジョン座標に色を付けてあげましょう。

```typescript

type Position = [x: number, y: number];

export abstract class GameMap {
  ...

  protected abstract collision: Position[];

  /**
   * Actorの実座標[x, y]を返す
   */
  public get actorpos(): Position {
    return [-this.actorx, -this.actory];
  }

  public draw() {
    // Day 10あたりで書いたようなdraw処理
    ...

    // collisionの座標に色を付ける
    for (const [x, y] of this.collision) {
      ctx.fillStyle = "rgba(110, 45, 207, 0.5)";
      ctx.fillRect(x * TileSize, y * TileSize, TileSize, TileSize);
    }
  }

  ...
}
```

また、現在の座標だけではなく、コリジョンには進む先の座標が必要なので、ある特定の向きの座標を返すメソッドも作っておきます。

`collision`に座標を入れていって、プレイヤーが進む先の座標が`collision`に含まれていたら進めないようにします。
それは`main`のところになりますかね。

```typescript
type Direction = "up" | "down" | "left" | "right";

export abstract class GameMap {
  ...

  public getPositionDirectedActor(direction: ActorDirection): Position {
    const position = this.actorpos;
    switch (direction) {
      case "down":
        return [position[0], position[1] + 1];
      case "up":
        return [position[0], position[1] - 1];
      case "left":
        return [position[0] - 1, position[1]];
      case "right":
        return [position[0] + 1, position[1]];
    }
  }

  protected async main() {
    if (this.moveYPriority === "down") {
      // 下方向への移動が優先されているなら...
      if (!(this.moveUpInterval || this.moveDownInterval)) {
        // この処理の中では`interval`を定義するので既に定義されているなら無視

        this.moveDownInterval = setInterval(() => {
          if (!this.actorinterval && this.realactor) { // Actorのアニメーションが動いてないとき
            this.actorindex = 0;
            this.actorinterval = setInteval(() => {
              // Actorのアニメーション処理 (バックグラウンド)
              if (this.realactor) { // TypeCheckのため
                if (this.realactor.down.moves.length === 0) {
                  // 移動用の画像がない場合は止まってる画像を使う
                  this.actortile = this.realactor.down.stand;
                } else {
                  // 移動用の画像を設定
                  this.actortile = this.realactor.down.moves[this.actorindex];
                  this.actorindex++; // 次の画像へ
                  if (this.actorindex >= this.realactor.down.moves.length) {
                    // 画像が最後まで行ったら最初に戻す
                    this.actorindex = 0;
                  }
                }
              }
            }, ((1000 / FrameRate) * 25) / this.realactor.down.moves.length);
            // ↑ 25Frame毎に移動用画像が1週する (25は適当な数字だよ)
          }

          if (!this.collision.some(this.getPositionDirectedActor("down"))) {
            // `collision`の配列の中に進行方向の座標が含まれていない...つまり進める

            this.animatedy -= TileSize / (1000 / FrameRate); // マップを微妙(1タイルには満たない程度)に動かす

            if (this.animatedy <= -TileSize) {
              // マップのズレが1タイル分になったら、ズレを0にして、実座標を1変える
              this.animatedy = 0;
              this.actory--;

              if (!this.isArrowDown) {
                // もしその時、下矢印キーが押されていなかったら、下方向への移動を止める
                clearInterval(this.moveDownInterval);
                this.moveDownInterval = 0;
                clearInterval(this.actorinterval);
                this.actorinterval = 0;
                if (this.isArrowUp) {
                  /// もし、下は押されてないが上は押されているなら`moveYPriority`を`up`に変えることで
                  /// 次の`main`では上方向への移動を行う
                  this.moveYPriority = "up";
                } else {
                  // どちらも押されていない場合は、`moveYPriority`を`none`にして、止まってる画像を使う
                  this.moveYPriority = "none";
                  this.actortile = this.realactor?.down.stand as RealTile;
                }
              }
            } else if (!this.isArrowDown) {
              // 進行方向が壁+下矢印キーが押されていない場合は、下方向への移動を止める
              // (そうじゃないなら下向きのアニメーションが続く)
              clearInterval(this.moveDownInterval);
              this.moveDownInterval = 0;
              clearInterval(this.actorinterval);
              this.actorinterval = 0;
              if (this.isArrowUp) {
                this.moveYPriority = "up";
              } else {
                this.moveYPriority = "none";
                this.actortile = this.realactor?.down.stand as RealTile;
                this.actorDirection = "down";
              }
            }
          }

          requestAnimationFrame(() => {
            // 描画処理
            clearCanvas();
            this.draw();
          });
        }, 1000 / FrameRate);
      }
    } else if (this.moveYPriority === "up") {
      // 上に動かす処理
    }

    ...
  }
  ...
}
```

基本はこんな感じ...？

猫猫さん < 「あ、これ動かないです。」

# 等価とは？

## `==`と`===`

TypeScript においては「等価である」ということが少し複雑です。

```typescript
console.log(0 == "0");
```

これの答えは`true`ですか？ `false`ですか？
答えは`true`です。 数字と文字列の比較ですが、`==`で比較すると型変換が行われて、`0`と`"0"`は等価となります。

しかし、`===`で比較すると型変換が行われないので、`0`と`"0"`は等価ではありません。

```typescript
console.log(0 === "0");
```

つまりこれは`false`です。

なるほど、`===`を使えばいい感じに比較できるという事です。
（`==`は等価演算子、`===`は厳密等価演算子と呼ばれる。）

## 罠？

`collision`は座標の配列(もっと正確に言うと`number`の配列の配列: `[x, y][]`)ですよね。

じゃあ次のように配列(おまけにオブジェクト)の比較はどうなるでしょうか。

```typescript
console.log({ a: 12345 } === { a: 12345 });
console.log([1, 2, 3] === [1, 2, 3]);
```

これらは両方とも`false`です。

わーお...罠ですね。
これは、TypeScript(JavaScript)の仕様で、オブジェクトにおいては等価比較とかは無意味になってしまいます。

すなわち、`collision`のような配列の配列は`===`で比較することが出来ません。

ただ、次のように一度 JSON にしてしまえば比較は出来ます。

```typescript
console.log(JSON.stringify({ a: 12345 }) === JSON.stringify({ a: 12345 }));
console.log(JSON.stringify([1, 2, 3]) === JSON.stringify([1, 2, 3]));
```

これで`true`が返ってきます。

これをうまく活用して座標の確認をしましょう。

```typescript
type StringifiedPosition = `[${number},${number}]`;

export abstract class GameMap {
  ...

  protected abstract collision: StringifiedPosition[];

  public isPositionCollides(position: Position): boolean {
    const stringifiedPosition = JSON.stringify(position) as StringifiedPosition;
    return this.collision.some((pos) => pos === stringifiedPosition);
  }
}
```

> draw の処理も変えましょうね～

# これでいけそうなんだけど！

多分一般プレイだったらこれでいけると思います。

がっ...！
まだ足りない！

確かに上下左右の単純移動においてのコリジョンは機能するが... **斜め移動** はどうなんだっ！？

というわけで課題ですよ。

上下方向の処理に加えて、`moveXPriority`を参照して斜めの座標を取得し、その座標がコリジョンなら進めないようにする処理にしてみましょう。

これで完璧になると思います！ （2 敗）