【Day18】良い加減主人公を配置しよう【QAC24】


# 主人公とは？

ここでは、画面の真ん中で動いてるあのキャラクターの事を指します。

# 主人公の配置

主人公もマップなどと同じように、タイルセットに画像があります。
同じようにタイルを持ってくればよさそうです。

`map.ts`の`tiles`を模倣して`actor`を作りましょう。
また、その`actor`の型も定義してしまいましょう。

## Actor に必要なものとは？

型を作るときはその型の構造を考えておくのが便利というかなんというか...

画面に配置されてる主人公としては...まぁ上下左右分の画像は必要ですよね。
後は...止まってる時と動いてるときは画像違いますよね。
なんなら、動いてるときはアニメーションしてるべきですよね。

ということで、以下のような型を作ってみましょう。

```typescript
type ActorTiles = {
  stand: FakeTile;
  moves: FakeTile[];
};

type Actor = {
  up: ActorTiles;
  down: ActorTiles;
  left: ActorTiles;
  right: ActorTiles;
};
```

マップ用のタイルと Actor 用のタイルは同じなので`FakeTile`を流用しています。
`Actor`に上下左右それぞれ、止まった時用と動いてる時用のタイル情報を持たせています。

また、マップタイルの時と同様に`Drawable`なものも定義しておきましょう。

```typescript
type DrawableActorTiles = {
  stand: RealTile;
  moves: RealTile[];
};

type DrawableActor = {
  up: DrawableActorTiles;
  down: DrawableActorTiles;
  left: DrawableActorTiles;
  right: DrawableActorTiles;
};
```

（もしかしてジェネリクス使えばもっと楽にかけた？）

これは`GameMap`の`realactor`として定義しておきましょう。
(ただし、タイルが生成されるまでは`realactor`は`null`なので、`DrawableActor | null`となる。)

# 描画をする

Day11 あたりに`generateMapTiles`みたいなのをなんか説明したような気がするのですが、それの Actor 版として`generateActorTiles`を作りましょう。(長いので紹介なし)

それらを纏めた`generateTiles`でも作りましょうか。
といっても、それらを実行するだけですが。

```typescript
export abstract class GameMap {
  ...

  protected async generateTiles() {
    await Promise.all([
      this.generateMapTiles(),
      this.generateActorTiles(),
    ]);
  }

  ...
}
```

んで...なんですが、`realactor`には全部の向きの情報が入ってますよね。
画面に描画するのはそのうちの 1 方向だけですよね。

もっと言えば、止まってるときのやつか動いてるときのやつのどちらかしか描画しませんよね。

というわけで、`DrawableActorTiles`のさらに中、描画を行う`RealActor`を型に取った`actortile`とかでも作りましょうか。
これはベクタではないので、常にタイルが 1 つだけになります。

という訳でこの`actortile`に入れる処理...すなわち、「矢印キーを押したら主人公を動かす」の紹介になりますﾊﾟﾁﾊﾟﾁﾊﾟﾁﾊﾟﾁ

# 矢印キーを押したらマップを動かすんだってば（デジャヴ）

まず先に免責事項となっちゃいますが、このコードはあくまで私が無限思考に陥ってマズいって思ってひねり出したコードなので、もう...それはそれはなコードです。
（ある程度は）動いてますが、適時調節してください。私は後で適当に直すが、君は今のうちにやっておくんだよ！

まず、矢印キーを押したときに`move[XY]Priority`と`isArrow(Down|Up|Left|Right)`を変える処理はかいたと思いますが、離したときなんもしてないのでそこは書きましょう。

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

  protected keyup(e: KeyboardEvent) {
    if (e.code === "ArrowDown") {
      this.isArrowDown = false;
    } else if (e.code === "ArrowUp") {
      this.isArrowUp = false;
    } else if (e.code === "ArrowLeft") {
      this.isArrowLeft = false;
    } else if (e.code === "ArrowRight") {
      this.isArrowRight = false;
    }
  }

  ...
}
```

次に、`main`メソッドの中で動かす処理を書きましょう。
長いので「下に動く処理（マップを上に動かす処理）」だけ書きますね...

他の方向の処理は同じように書けるのであなたのプログラミングセンスに任せます。

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

  private actorinterval: number = 0; // Actorのアニメーション
  private actorindex: number = 0; // Actorのアニメーション画像のindex

  /// マップが動いているpx量 (マップと実座標のズレと言えば分かりやすいか？)
  /// (実際のActorの座標は`actor[xy]`だが、マップがどれだけアニメーションしているかの情報も必要)
  private animatedx: number = 0;
  private animatedy: number = 0;

  // マップを動かすアニメーション
  private moveDownInterval: number = 0;
  ...

  protected actorx: number = 0;
  protected actory: number = 0;

  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は適当な数字だよ)
          }

          // マップを動かす処理

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

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

    if (this.moveXPriority === "left") {
      // 左に動かす処理
    } else if (this.moveXPriority === "right") {
      // 右に動かす処理
    }
  }

  ...
}
```

ほら、ｺﾞﾁｬｺﾞﾁｬやろ？
無駄なコードがありそうですがそれは一旦気にせず(もし見抜いたらそこは消そう)、これで多分動く...多分...
動かないなら自分で直してみてね。私もやったんだからｻ...

上下方向と左右方向の移動を1つの`if-else`にまとめず分けたのは、もし上矢印キーと右矢印キーを同時押しした時、上方向の移動のみになってしまうため、軸違いは分けた方がいいと思ったからです。

てかインデントって大事だよね？
ここまでのネストする方が悪いんだけど...

# まとめ
ちなむと、私のプロジェクトの`main`には次回やるテーマのコードが配合されているので、上のコードはそれの抜粋です。
（だから動くか不明なのです。）

次回は移動処理において大事なやつをやります。
が、正直まだ未完成なので、私は知恵だけ与えて皆さんに考えてもらうことにします...

覚悟しておいてほしいのは、次回のテーマを進めると、さっきのｺﾞﾁｬｺﾞﾁｬコードが更にｺﾞﾁｬｺﾞﾁｬします。
ごめんな。