TypeScriptでブロック崩しを作ろう(Excalibur.jsを添えて) 

## 概要

本記事はTypeScriptを使って...というより
[Excalibur.js](https://excaliburjs.com/)を使ってブロック崩しを簡単に作ろう、という記事。
あわよくば、Excalibur.js信者を増やす作戦なのだ...フフフ。

## Excalibur.jsって❓

JavaScriptの2Dゲームエンジン。
**TypeScriptとの親和性が非常に高い。**

同類には[Phaser](https://phaser.io/)とか、[melonJS](https://melonjs.org/)とかがある。

筆者は断然Excalibur.js推し。
Phaserは型情報がよく分からんときがあった。melonJSは使ったことない。
Excalibur.jsは型情報がバッチリ完備されてて、VSCodeでサクサクプログラミングできるのがお気に入り。

なお、 **日本語の情報は皆無** 。っていうか英語でもほぼない。
でも、必要なことは公式の[Doc](https://excaliburjs.com/docs)や[Discussion](https://github.com/excaliburjs/Excalibur/discussions)に載ってるので調べてみよう。
特にDiscussionで質問すると速攻で作者から返信が来るぞ❗Erik氏すげえ❗

## 環境構築

まず、エディタは何でもOK。以下、VSCodeを想定。
[Node.js](https://nodejs.org/)も当然インストール済みだとする。

次。拙作のテンプレがあるのでそれを `clone` する。

```sh
git clone https://github.com/tenpaMk2/excalibur-parcel2-vscode-debuggable-template
```

次。 `clone` したフォルダをVSCodeで開いて、

```sh
npm install
```

する。
これでExcalibur.jsやらTypeScriptやら[Parcel2](https://parceljs.org/)やら必要なものがインストールされる。

Parcel2って何❓って人はググってね。詳しい説明は省くよ(筆者もよくわかってないので...)。

この状態で

```sh
npm run start
```

して、↓のような表示が出れば成功。

```log
> start
> parcel serve ./src/index.html

Server running at http://localhost:1234
✨ Built in 1.54s
```

これで、テンプレ既存のデモゲーム(ゲームじゃないけど)がトランスパイルされて、
おまけに閲覧用のサーバーも立ち上がる。
試しにアクセスすればゲームが動いているのが確認できるはず。
もしくは、適当なtsファイルを開いて、F5でデバッグ開始してもOK。

最後に、テンプレでしか使わないファイルを削除する。
↓を削除。

```
src
├── assets
│   └── roguelikeChar_transparent.png
├── config.ts
├── files.d.ts
├── objects
│   ├── ball.ts
│   └── ground.ts
├── resource.ts
└── scenes
    └── game-scene.ts
```

つまり、 `index.html` と `style.css` と `index.ts` だけ残せばOK。

## 作る

### 最初の一歩

ここからは公式docの[Getting Started](https://excaliburjs.com/docs/getting-started)に沿って進める。
いじるのは `index.ts` のみ。

まずは中身を全部削除して、↓のようにする。

```ts
import { DisplayMode, Engine } from "excalibur";

const game = new Engine({
  width: 800,
  height: 600,
  displayMode: DisplayMode.FitScreen,
  canvasElementId: "game",
});

engine.start();
```

いきなり公式docと違う部分があるけど目を瞑ってほしい。些末なことなので。
ブラウザで `http://127.0.0.1:1234` にアクセスして、青い画面が表示されれば成功。

### 棒をつくる

続いて、ブロック崩しの『棒』部分を作る。
`engine.start()` のあとに↓を続ける。

```ts
const paddle = new Actor({
  x: 150, // 横位置
  y: game.drawHeight - 40, // 縦位置
  width: 200, // 幅
  height: 20, // 高さ
  color: Color.Chartreuse, // 色
  collisionType: CollisionType.Fixed, // 衝突判定タイプ
});
game.add(paddle); // これでゲームに登録される。忘れがちなので注意。

// 棒がマウスの動きに追従するようにする
game.input.pointers.primary.on("move", (evt) => {
  paddle.pos.x = evt.worldPos.x;
});
```

↓のように棒が表示されてれば成功。

![スクリーンショット 2022-07-16 13.48.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214979/86a28a49-1bf2-f365-aa8d-d475a17e4302.png)

なお、↓のクラスはファイル上部でインポートすること。以後も新しいクラスはインポートするのを忘れないように。

- `Actor`
- `Color`
- `CollisionType`

VSCodeで作業してるなら、タイプしたときに補完候補が↓のように表示されるはず。Tabで選択すれば自動で `import` してくれるぞ❗

![スクリーンショット 2022-07-16 13.40.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214979/7659b53d-adab-4ae4-c87f-d456bfe24357.png)

Excalibur.jsでは、ゲーム上に出現するオブジェクトはたいてい `Actor` クラスで実現する。
この `Actor` に位置や幅、高さ、衝突判定タイプなどの情報を与えてゲームに登録する。
で、各イベントごとに動作を制御してやると、ゲームとして動くというシロモノ。

今回も棒の動きの制御には `move` イベントを使う。
これは、マウスが動いたときに発火するイベント。
このイベントのオブジェクトに、マウスの位置が格納されるので、
それをそのまま棒の位置として使ってやればOK。簡単だね❗
なお、マウスが動く限り毎フレームごとにイベントは発火する。

### 球をつくる

```ts
const ball = new Actor({
  x: 100,
  y: 300,
  radius: 10, // 球半径
  color: Color.Red,
  collisionType: CollisionType.Passive,
});
game.add(ball);

// 1秒後に右下へ移動開始。
setTimeout(() => {
  ball.vel = new Vector(100, 100);
}, 1000);
```

`Actor` に `radius` の情報を与えると球になる。
逆に言うと、 `width` と `height` を与えると長方形になる。

球の衝突判定タイプは `Passive` 。
詳細は公式docを見てほしいが、ざっくり↓の認識でOK。

|         特徴         |       タイプ       |
| :------------------: | :----------------: |
| 衝突によって動かない |      `Fixed`       |
|   衝突によって動く   |      `Active`      |
|    衝突を検知する    |     `Passive`      |
|      衝突しない      | `PreventCollision` |

分からなければあれこれ試してみて動きを見てね。

ボールの動きを制御するには `.vel` プロパティを書き換えればOK。
今回は重力も摩擦の設定もしてないので、等速直線運動する。
なお、 **y座標は下向きが正なので注意** 。

### 球が画面端で反射するようにする

```ts

ball.on("postupdate", () => {
  // 球が画面左にぶつかったら反射する
  if (ball.pos.x < ball.width / 2) {
    ball.vel.x = ballSpeed.x;
  }
  // 球が画面右にぶつかったら反射する
  if (game.drawWidth < ball.pos.x + ball.width / 2) {
    ball.vel.x = -ballSpeed.x;
  }

  // 球が画面上にぶつかったら反射する
  if (ball.pos.y < ball.height / 2) {
    ball.vel.y = ballSpeed.y;
  }
});
```

`.on()` でイベントに処理を紐付ける。
`postupdate` イベントは毎フレームの更新処理ごとに呼ばれる。
流れとしては以下↓。

1. `preupdate` イベントの処理
2. `update()` の処理
3. `postupdate` イベントの処理
4. (1フレームごとに)1から繰り返し

`ball.pos.x` は球の横位置。
**横位置は球の中心なので注意** 。
なお、この中心をどこにするかは `anchor` プロパティで制御できる。

画面端の処理を追加したけど、実際に画面端にボールを弾けるようになるのはこのあと。

### ブロックを作る

```ts
const padding = 20;
const xoffset = 65;
const yoffset = 20;
const columns = 5;
const rows = 3;

const brickColors = [Color.Violet, Color.Orange, Color.Yellow];

const brickWidth = game.drawWidth / columns - padding - padding / columns; // px
const brickHeight = 30; // px
const bricks: Actor[] = [];
for (let j = 0; j < rows; j++) {
  for (let i = 0; i < columns; i++) {
    const brick = new Actor({
      x: xoffset + i * (brickWidth + padding) + padding,
      y: yoffset + j * (brickHeight + padding) + padding,
      width: brickWidth,
      height: brickHeight,
      color: brickColors[j % brickColors.length],
      collisionType: CollisionType.Active,
    });
    game.add(brick);
    bricks.push(brick);
  }
}
```

基本的には棒をつくるのと基本は変わらない。
位置や幅の計算がめんどくさいだけ。説明省略。

### 球が弾かれるようにする

```ts
let colliding = false;
ball.on("collisionstart", function (ev) {
  // 衝突開始時の処理

  if (bricks.indexOf(ev.other) > -1) {
    ev.other.kill(); // ブロックにあたっていたら消去する
  }

  // MTV(Minimum Translation Vector)を正規化するらしい。このへんは謎。
  const intersection = ev.contact.mtv.normalize();

  if (!colliding) {
    colliding = true;
    if (Math.abs(intersection.x) > Math.abs(intersection.y)) {
      ball.vel.x *= -1; // MTVを利用して、反射を処理する
    } else {
      ball.vel.y *= -1; // MTVを利用して、反射を処理する
    }
  }
});

ball.on("collisionend", () => {
  // 衝突終了時の処理

  colliding = false;
});
```

衝突時のイベントの処理順も公式docから画像を引用。

![スクリーンショット 2022-07-16 14.51.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214979/b37842f9-1c3f-6623-c6d7-2af02f36212d.png)

- [引用元: Excalibur.js公式Doc](https://excaliburjs.com/docs/collision-events)

`collisionstart` イベントが衝突開始時のイベントで、
`collisionend` イベントが衝突終了時のイベントだね。
これ以外には衝突中に毎フレーム発火し続ける `precollision` とか `postcollision` イベントもある

MTVの説明は...省略させて...。筆者もよくわかってない。
`precollision` イベントオブジェクトには上下左右のどこにぶつかったのかを判定するプロパティがあるので、そっちを使ったほうが良いと思うのだがなあ。
気が向いた人はそっちで書き直してみよう(丸投げ)。

### ゲームオーバーを作る

```ts
ball.on("exitviewport", () => {
  alert("You lose!");
});
```

これだけ。 `exitviewport` イベントは、画面の外に `Actor` が出たら発火するイベント。
画面の上と左右は球を反射するようにしているので、下から出たときだけゲームオーバーになる、という寸法だね。

## できあがり

![スクリーンショット 2022-07-16 15.05.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214979/5ed29425-7266-cd3e-8a10-470e65ddcec7.png)

以上、これでできあがり。
どうだろう。意外と簡単だったんじゃないだろうか?
要は↓を記述さえすればゲームになる❗簡単❗

- どこにどういう `Actor` がいて、
- どういうとき(イベント発火時)にどういう動きをするのか

## Excalibur.jsどう❓

使いやすくない❓いい感じじゃない❓
githubのstarが1000しかないなんて信じられない。皆もっと使おう。

## 宣伝

他にも↓みたいなゲームが作れるよ。皆も作ろう❗筆者も作ったんだからさ。

- [Dino-Runner](https://tenpamk2-blog.netlify.app/apps/excalibur-dino-runner/)
- [Snake](https://tenpamk2-blog.netlify.app/apps/excalibur-snake)

## 最後に

この記事でExcalibur.js信者が増えれば幸い。
ご意見ご質問あればコメントお願いします。