【ハンズオン】Phaser.js / TSでゲームを作りながら心からプログラミングを楽しみたい！

## はじめに
普段、ゲーム開発とは関係のないWeb開発のバックエンドを担当しております。
たまには敢えて業務に関連しない趣味全振りの「自分が楽しむ」為のプログラミングをやってみたいと思い、Phaser.jsに触れてみました。

「技術」は課題解決の「手段」であるという話をよく耳にしますが、
確かにその通りではあると思いつつもプログラミングをただの仕事道具としてではなく、そもそも自分がエンジニアを目指したきっかけである「ものづくりの楽しさ」を思い出したいというのが触ってみた一番のきっかけです。
(技術は手段だとは思いますが、手段でしかないとは思いたくないです。)

※語弊の無いように言っておくと楽しいという文脈では普段の開発も変わらず、やはり楽しいなぁとは思います。言い換えると何も責任の無い開発で遊んでみたという話です。

※加えてエンジニアは生成AIでは表現できない、人ならではの細部へのこだわりが必要だとも思います。どんな形でもいいので書く事の大切さを忘れないでいたいと思いつつ精進していきたいです。


以降に記述するTSのコードは慣れてない部分もありまともに活用できてないとは思いますが、記事を通してプログラミングそのものをライトに楽しむきっかけになればと思っております。

:::note info
　本記事では公式ドキュメントのチュートリアル内容を解説しながら実装する記事になります。
　既にチュートリアルが済んでる方には参考にならないかもしれませんのでご了承ください。
:::

## 本記事のゴール
下記のようなゲームを実装します。
ゲームの内容としては、マップ上に散らばる星を獲得してスコアを稼ぎ、
途中出現する爆弾に被弾するとGAME OVERになるシンプルなゲームです。

 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490435/f749d683-b16b-da18-2bab-58f3bada93aa.png)

 また、Phaser.jsに関連する周辺知識も踏まえつつ執筆させていただきますので
 少し長い記事になってしまいましたが温かくご覧いただけると幸いです。
 そんなのいいから早く本題へ！という方は[こちら](#本題チュートリアルを通して動くものを作ってみる)からご覧ください。

公開するほどのものでもないかもしれませんが、一応リポジトリも公開しております。

 https://github.com/CreareWorks/phaser-ts


## そもそもゲーム開発エンジンってなに？
ゲームを効率的に作成するためのツールセットやライブラリの事を指します。
ゲーム開発エンジンはグラフィックスレンダリング、物理シミュレーション、オーディオ管理、ユーザー入力処理など、ゲームを作成するために必要な基本機能が含まれます。

ゲーム開発エンジンの主な機能
- グラフィックスレンダリング: 2Dや3Dの画像を描画する機能。
- 物理エンジン: 重力や衝突など、現実世界の物理法則をシミュレートする機能。
- サウンド管理: 音楽や効果音を再生、管理する機能。
- 入力処理: キーボード、マウス、タッチなどのユーザー入力を処理する機能。
- アニメーション: キャラクターやオブジェクトの動きを制御する機能。

有名なゲーム開発エンジンには、Unity、Unreal Engine、Godot などがあり、開発者がゲームのロジックに集中できるように、複雑な技術的課題を簡単に扱えるように設計されています。
phaser.jsもゲーム開発エンジンの一種です。

## Phaser.js とは
Phaser.js は、HTML5 ゲームフレームワークの一つであり、2D ゲームの作成を簡単にするための豊富な機能を提供しています。
ブラウザ上で動作するため、インストールなしで手軽にプレイ可能なゲームを作成できます。

日本語の記事は比較的少なそうではありますが、
公式サイトのドキュメントはかなり充実しており、チュートリアルを通せば簡単な実装は十分できるようになります。
長くても3hほどあればチュートリアルは理解できるかと思います。

## 導入：OOPについて
Phaser.jsの話に入る前にPhaser.js本体のパラダイムについて先に触れたいと思います。
オブジェクト指向プログラミング (OOP) は、ソフトウェア開発におけるパラダイムの一つで、データと関連する操作をオブジェクトとしてモデル化する手法です。
OOPの基本概念には、クラスとオブジェクト、継承、ポリモーフィズム、カプセル化があります。

### OOP の基本概念
- クラスとオブジェクト: クラスはオブジェクトの設計図であり、オブジェクトはクラスから生成される実体です。
- 継承: 新しいクラスが既存のクラスの機能を引き継ぎ、拡張できる仕組みです。
- ポリモーフィズム: 同じインターフェースを使って異なる型のオブジェクトを操作できる能力です。
- カプセル化: データと対象を操作するメソッドを一つにまとめ、外部からのアクセスを制限することです。

### Phaser.jsでOOPの基本概念を追ってみる
Phaser.jsは OOPパラダイムに則って設計されています。
以下で詳細に解説していきます。

#### クラスとオブジェクト
Phaser.jsでは、シーンやスプライトなどのゲーム要素はクラスとして定義されます。
例えば、カスタムシーンを作成する際にはクラスを定義し、そのクラスを Phaser.Sceneから継承します。

``` typescript
class ExampleScene extends Phaser.Scene {
  constructor() {
    super({ key: 'myScene' });
  }

  preload() {
    // リソースの読み込み
  }

  create() {
    // 初期設定
  }

  update() {
    // フレームごとの更新処理
  }
}
```

#### 継承

上記の例では、ExampleScene クラスが Phaser.Sceneクラスを継承しています。
継承する事により、Phaser.Sceneのすべての機能をExampleSceneで利用できます。

#### カプセル化

クラス内のプロパティやメソッドをプライベートにすることで、外部からの不正なアクセスを防ぎます。
TypeScriptでは、private キーワードを使ってカプセル化を実現できます。

``` typescript
class ExampleScene extends Phaser.Scene {
  private score: number = 0;

  constructor() {
    super({ key: 'ExampleScene' });
  }

  private updateScore(value: number) {
    this.score += value;
  }
}
```

このようにPhaser.jsはOOPの原則に従って設計されており、ゲーム開発においてもこれらの概念を活用することで、コードの再利用性や保守性を高めることができます。
こちらの概念、前提を踏まえた上でいよいよ本番に入りたいと思います。

## 本題：チュートリアルを通して動くものを作ってみる
サンプルコードでは、細かく何をしているものなのかコメントを残しているので参考にしていいただけると幸いです。

### 環境設定

まず、プロジェクトの環境構築を行います。
今回のプロジェクトでは以下の環境を使用します。

```
VITE v5.3.1
Typescript Version 5.3.3
```

### プロジェクトのセットアップ

① Viteプロジェクトの作成

```bash
npm create vite@latest my-phaser-game --template vanilla-ts
cd my-phaser-game
npm install
```

② 必要なパッケージ(phaser.js)のインストール

```bash
npm install phaser
```

### ゲームの基本設定

`main.ts` ファイルを作成し、以下のように記述します。

```typescript
import Phaser from "phaser";
import ScrollScene from "./scenes/scrollScene";

// ゲームの基本設定を指定
const config: Phaser.Types.Core.GameConfig = {
  type: Phaser.AUTO, // レンダリングタイプを指定(CANVAS, WEBGL, AUTOがある)
  width: 800, // ゲームの幅を指定
  height: 600, // ゲームの高さを指定
  physics: { // 物理エンジンの設定
    default: 'arcade', // 使用する物理エンジンを指定
    arcade: {
        gravity: { y: 300 }, // 重力の方向と強さを指定
        debug: false // デバッグモード
    }
  },
  input: {
    keyboard: true // ここでキーボード入力を有効にする
  },
  scene: ScrollScene, // 後に作成するScrollScene というカスタムシーンを指定
};



// Phaser起動させるプロセスを開始
new Phaser.Game(config);
```

### シーンの作成

次に、`ScrollScene.ts` ファイルを作成し、以下のコードを記述します。
一旦、完成コードを記述しています。
各メソッドの詳細な説明は後述でさせていただきます。

```typescript
import Phaser from "phaser";

class ScrollScene extends Phaser.Scene {
  // 初期化
  private _cursorKeys?: Phaser.Types.Input.Keyboard.CursorKeys;
  private _player?: Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;
  private _stars?: Phaser.Physics.Arcade.Group;
  private _bombs?: Phaser.Physics.Arcade.Group;
  private _score: number = 0;
  private _scoreText?: Phaser.GameObjects.Text;

  constructor() {
    super({ key: 'scrollScene' });
  }

  // ゲーム素材を読み込む(最初に読み込んでおく必要がある)
  preload() : void {
    // 素材リソースのベースとなるURLを指定
    this.load.setBaseURL("https://labs.phaser.io");

    /**
     * (例) https://labs.phaser.io/assets/skies/space3.pngを指定している状態
     * 第一引数には、読み込んだ素材を識別するためのキーを指定
     * 第二引数に対象の画像を指定
     * localにphaserをinstallしている場合は、baseURLを指定しなくても良い
     */
    this.load.image('sky', 'assets/skies/space3.png');
    this.load.image('ground', 'assets/sprites/platform.png');
    this.load.image('star', 'assets/demoscene/star.png');
    this.load.image('bomb', 'assets/sprites/bullet.png');
    // マップデータ(スプライトシート)を読み込む
    this.load.spritesheet('dude', // キー
        'assets/sprites/dude.png', // 画像を指定
        { frameWidth: 32, frameHeight: 48 } // フレームの幅と高さを指定
    );

  }

  // ゲーム画面の初期化
  create() : void {
    // 背景画像の追加
    // 画像の表示位置は、x: 400, y: 300
    // preloadで読み込んだsky画像を指定
    // オブジェクトが表示される順序は、下記で作成している順序と一致する。
    // 背景画像にしたい画像は一番最初に指定する。
    this.add.image(400, 300, 'sky');

    // 「静的」な物理オブジェクトを作成するための設定, オブジェクトは動かない、壁や地面などに使う
    const platforms = this.physics.add.staticGroup();

    // 画面内の緑の地面を作成
    // setScale(2)で、画像のサイズを2倍に拡大, スケールを変更した時は物理エンジンを更新させる必要がある。
    platforms.create(400, 568, 'ground').setScale(2).refreshBody();
    platforms.create(600, 400, 'ground');
    platforms.create(50, 250, 'ground');
    platforms.create(750, 220, 'ground');

    // プレイヤーを作成、初期配置
    this._player = this.physics.add.sprite(100, 450, 'dude');
    this._player.setBounce(0.2); // 地面に着地した際のバウンド値
    this._player.setCollideWorldBounds(true); // 画面の外に出ないようにする

    // プレイヤーにアニメーションを定義(あくまでアニメーションの定義であてtこれで動くわけではない)
    this.anims.create({
        key: 'left', // 左向きのアニメーション
        frames: this.anims.generateFrameNumbers('dude', { start: 0, end: 3 }), // 0~3のフレームを指定(0~3が左向きで歩行する画像)
        frameRate: 10,
        repeat: -1 // 繰り返し再生
    });   
    this.anims.create({
        key: 'turn',
        frames: [ { key: 'dude', frame: 4 } ],
        frameRate: 20
    });   
    this.anims.create({
        key: 'right',
        frames: this.anims.generateFrameNumbers('dude', { start: 5, end: 8 }),
        frameRate: 10,
        repeat: -1
    });

    // 地面オブジェクト、プレイヤーオブジェクトの衝突判定を行うcolliderオブジェクトを作成する
    this.physics.add.collider(this._player, platforms);

    // プレイヤーを操作するためのキーボードマネージャ反映
    if (this.input.keyboard) {
      this._cursorKeys = this.input.keyboard.createCursorKeys();
    } else {
      console.error('Keyboard input not initialized');
    }

    // マップに配置する「動的」な星オブジェクトを作成
    this._stars = this.physics.add.group({
      key: 'star',
      repeat: 11,
      setXY: { x: 12, y: 0, stepX: 70 }
    });
    // 
    this._stars.children.iterate((child: Phaser.GameObjects.GameObject) => {
      // 作成された12個の各星オブジェクトに0.4~0.8のバウンド値を設定
      child.setBounceY(Phaser.Math.FloatBetween(0.4, 0.8));
    });
    // 地面オブジェクト、星オブジェクトの衝突判定を行うcolliderオブジェクトを作成する
    this.physics.add.collider(this._stars, platforms);

    // プレイヤーが星オブジェクトに触れた際の処理
    this.physics.add.overlap(this._player, this._stars, this.collectStar, null, this);

    // スコアをカウントさせるための初期値定義
    this._scoreText = this.add.text(10, 10, 'score: 0', { fontSize: '32px', fill: '#FFFFFF' });

    // ボムオブジェクトを作成
    this._bombs = this.physics.add.group();
    this.physics.add.collider(this._bombs, platforms);
    this.physics.add.collider(this._player, this._bombs, this.hitBomb, null, this);
  }

  // update ゲーム画面の更新 操作など(画面を表示するだけなので今回は使わない)
  update() : void {
    // プレイヤーやカーソルキーが初期化されていない場合は、何もせずに

リターン
    if (!this._player || !this._cursorKeys) {
      return;
    }

    // キーボードの入力に応じてプレイヤーの移動制御
    if (this._cursorKeys.left.isDown)
    {
        this._player.setVelocityX(-160);
        this._player.anims.play('left', true);
    }
    else if (this._cursorKeys.right.isDown)
    {
        this._player.setVelocityX(160);
        this._player.anims.play('right', true);
    }
    else
    {
        this._player.setVelocityX(0);
        this._player.anims.play('turn');
    }
    
    if (this._cursorKeys.up.isDown && this._player.body.touching.down)
    {
        this._player.setVelocityY(-300);
    }
  }

  /**
   * プレイヤーが星オブジェクトに触れた際の処理
   * @param player 
   * @param star 
   */
  private collectStar (
    player: Phaser.Types.Physics.Arcade.SpriteWithDynamicBody, 
    star: Phaser.GameObjects.GameObject
  ) : void {
      star.disableBody(true, true);

      this._score += 10;
      this._scoreText.setText('Score: ' + this._score);

      if (this._stars.countActive(true) === 0)
      {
          this._stars.children.iterate(function (child: Phaser.GameObjects.GameObject) {
              child.enableBody(true, child.x, 0, true, true);
          });
    
          let x = (player.x < 400) ? Phaser.Math.Between(400, 800) : Phaser.Math.Between(0, 400);
    
          let bomb = this._bombs.create(x, 16, 'bomb');
          bomb.setBounce(1);
          bomb.setCollideWorldBounds(true);
          bomb.setVelocity(Phaser.Math.Between(-200, 200), 20);
    
      }
  }

  /**
   * プレイヤーが爆弾オブジェクトに触れた際の処理
   * @param player 
   * @param bomb 
   */
  private hitBomb (
    player: Phaser.Types.Physics.Arcade.SpriteWithDynamicBody, 
    bomb: Phaser.Physics.Arcade.Group
  ) : void {
      this.physics.pause();

      player.setTint(0xff0000);

      player.anims.play('turn');
      
      this.add.text(200, 250, 'GAME OVER', { fontSize: '80px', fill: '#ff0000' });
  }
}

export default ScrollScene;
```

### 各メソッドの詳細な説明

#### preload()
`preload()` メソッドは、ゲーム開始前に必要な素材を読み込むためのメソッドです。
ここでは、画像やスプライトシートなどを指定します。

```typescript
preload() : void {
  this.load.setBaseURL("https://labs.phaser.io");

  this.load.image('sky', 'assets/skies/space3.png');
  this.load.image('ground', 'assets/sprites/platform.png');
  this.load.image('star', 'assets/demoscene/star.png');
  this.load.image('bomb', 'assets/sprites/bullet.png');
  this.load.spritesheet('dude', 'assets/sprites/dude.png', { frameWidth: 32, frameHeight: 48 });
}
```
##### コードの中身を解説
- `setBaseURL` で素材のベースURLを指定します。
  - `this.load.setBaseURL("https://labs.phaser.io")` というコードにより、すべての素材の基本パスを設定します。このURLは、次に読み込むリソースのベースパスになります。
- `load.image` で画像を読み込み、キーを指定します。
  - 例えば、`this.load.image('sky', 'assets/skies/space3.png')` では、キー 'sky' を用いて、背景画像を読み込みます。このキーを使って、後で画像を参照できます。
- `load.spritesheet` では、スプライトシートのフレームサイズも指定します。
  - `this.load.spritesheet('dude', 'assets/sprites/dude.png', { frameWidth: 32, frameHeight: 48 })` では、32x48 ピクセルの各フレームを持つスプライトシートを読み込みます。
  - この設定により、アニメーションに使用できる複数のフレームが含まれます。

#### create()
`create()` メソッドは、ゲームの初期設定を行うためのメソッドです。
背景やプレイヤー、プラットフォームなどを設定します。

```typescript
create() : void {
  this.add.image(400, 300, 'sky');

  const platforms = this.physics.add.staticGroup();
  platforms.create(400, 568, 'ground').setScale(2).refreshBody();
  platforms.create(600, 400, 'ground');
  platforms.create(50, 250, 'ground');
  platforms.create(750, 220, 'ground');

  this._player = this.physics.add.sprite(100, 450, 'dude');
  this._player.setBounce(0.2);
  this._player.setCollideWorldBounds(true);

  this.anims.create({
    key: 'left',
    frames: this.anims.generateFrameNumbers('dude', { start: 0, end: 3 }),
    frameRate: 10,
    repeat: -1
  });
  this.anims.create({
    key: 'turn',
    frames: [ { key: 'dude', frame: 4 } ],
    frameRate: 20
  });
  this.anims.create({
    key: 'right',
    frames: this.anims.generateFrameNumbers('dude', { start: 5, end: 8 }),
    frameRate: 10,
    repeat: -1
  });

  this.physics.add.collider(this._player, platforms);

  if (this.input.keyboard) {
    this._cursorKeys = this.input.keyboard.createCursorKeys();
  } else {
    console.error('Keyboard input not initialized');
  }

  this._stars = this.physics.add.group({
    key: 'star',
    repeat: 11,
    setXY: { x: 12, y: 0, stepX: 70 }
  });
  this._stars.children.iterate((child: Phaser.GameObjects.GameObject) => {
    child.setBounceY(Phaser.Math.FloatBetween(0.4, 0.8));
  });
  this.physics.add.collider(this._stars, platforms);
  this.physics.add.overlap(this._player, this._stars, this.collectStar, null, this);

  this._scoreText = this.add.text(10, 10, 'score: 0', { fontSize: '32px', fill: '#FFFFFF' });

  this._bombs = this.physics.add.group();
  this.physics.add.collider(this._bombs, platforms);
  this.physics.add.collider(this._player, this._bombs, this.hitBomb, null, this);
}
```
##### コードの中身を解説
###### 背景画像の設定
- `add.image` で背景画像を追加しています。
  - `this.add.image(400, 300, 'sky')` というコードにより、キー 'sky' で読み込まれた背景画像を (400, 300) の位置に追加します。

###### 静的なオブジェクトの設定
- `physics.add.staticGroup` で静的なプラットフォームを作成するためのメソッドです。
  - `const platforms = this.physics.add.staticGroup()` では、このグループに追加されたオブジェクトは動かず、固定された位置に設置されます。
  - `platforms.create(400, 568, 'ground').setScale(2).refreshBody()` では、地面のプラットフォームを作成し、2倍のサイズにスケールアップしてリフレッシュします。
リフレッシュすることで、物理エンジンに新しいサイズを認識させます。

###### プレイヤーオブジェクトに対する設定
- `physics.add.sprite` でプレイヤーを作成し、バウンド(重量の影響, 地面に着地したときのふるまい)や衝突設定を行います。
  - `this._player = this.physics.add.sprite(100, 450, 'dude')` では、キー 'dude' を使ってプレイヤーキャラクターを作成し、初期位置 (100, 450) に配置します。
  - `this._player.setBounce(0.2)` では、キャラクターが地面に衝突した際のバウンド値を設定します。
  - `this._player.setCollideWorldBounds(true)` では、キャラクターがゲーム画面の端を超えないようにします。

###### プレイヤー操作時のアニメーション設定
- `anims.create` でプレイヤーのアニメーションを設定します。
  - 左,右,静止状態の各アニメーションを設定します。
例えば、`key: 'left'` では、左向きに歩くアニメーションを設定し、0~3 のフレームを使用します。
  - 下記画像のようなスプライトシートからフレームを指定しています。
例えば、0~3のフレームを指定した場合は「⬅️」を押下している時に、左から1~4番目の画像を繰り返し表示させる事で左に歩いているように見せます。
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490435/294330c8-421d-4f81-b410-aa3c5cecfe05.png)

###### キーボード入力を有効にする
 - `this._cursorKeys = this.input.keyboard.createCursorKeys()` というコード
により、カーソルキー入力を有効にします。
これで、プレイヤーの移動をキーボードで制御できます。

###### シーン内で表示させる星と爆弾のふるまいを設定する
- `physics.add.group` で星と爆弾のグループを作成し、それぞれの設定を行います。
  - 星オブジェクトは動的なグループとして設定され、各星はバウンド値を持ちます。
爆弾オブジェクトも同様に設定しています。

#### update()
`update()` メソッドは、フレームごとに呼び出され、ゲームのロジックやプレイヤーの入力処理を行います。
こちらのメソッドで実際に操作した時のふるまいを定義しています。

```typescript
update() : void {
  if (!this._player || !this._cursorKeys) {
    return;
  }

  if (this._cursorKeys.left.isDown)
  {
    this._player.setVelocityX(-160);
    this._player.anims.play('left', true);
  }
  else if (this._cursorKeys.right.isDown)
  {
    this._player.setVelocityX(160);
    this._player.anims.play('right', true);
  }
  else
  {
    this._player.setVelocityX(0);
    this._player.anims.play('turn');
  }
  
  if (this._cursorKeys.up.isDown && this._player.body.touching.down)
  {
    this._player.setVelocityY(-300);
  }
}
```
##### コードの中身を解説
###### キー入力に応じてプレイヤーの移動とアニメーションを制御する。
  - 左矢印キーが押された場合、`this._player.setVelocityX(-160)` により、プレイヤーは左に移動し、`this._player.anims.play('left', true)` により、左向きのアニメーションが再生されます。
  - 右矢印キーが押された場合も同様に、プレイヤーは右に移動し、右向きのアニメーションが再生されます。
  - どちらのキーも押されていない場合、プレイヤーは静止し、`this._player.anims.play('turn')` により、静止アニメーションが再生されます。
- 上矢印キーが押された時にプレイヤーがジャンプします。
  - `this._cursorKeys.up.isDown && this._player.body.touching.down` という条件で、プレイヤーが地面に触れている状態で上矢印キーが押された場合にジャンプさせます。
この条件を満たすと、`this._player.setVelocityY(-300)` により、プレイヤーは上にジャンプします。

#### プレイヤーが星に触れた時の処理を実装する
プレイヤーが星に触れた時の処理です。
星を獲得しつつスコアを更新し、全ての星を獲得したら新しい星を配置させる処理になります。

```typescript
private collectStar (
  player: Phaser.Types.Physics.Arcade.SpriteWithDynamicBody, 
  star: Phaser.GameObjects.GameObject
) : void {
  star.disableBody(true, true);

  this._score += 10;
  this._scoreText.setText('Score: ' + this._score);

  if (this._stars.countActive(true) === 0)
  {
    this._stars.children.iterate(function (child: Phaser.GameObjects.GameObject) {
      child.enableBody(true, child.x, 0, true, true);
    });

    let x = (player.x < 400) ? Phaser.Math.Between(400, 800) : Phaser.Math.Between(0, 400);

    let bomb = this._bombs.create(x, 16, 'bomb');
    bomb.setBounce(1);
    bomb.setCollideWorldBounds(true);
    bomb.setVelocity(Phaser.Math.Between(-200, 200), 20);
  }
}
```
##### コードの中身を解説
###### 星に触れると星を獲得し、スコアを更新(加算)させる
  - `star.disableBody(true, true)` により、星の物理ボディを無効化し、表示を消します。
プレイヤー視点、星を獲得した状態です。
  - `this._score += 10` でスコアを10点増やし、`this._scoreText.setText('Score: ' + this._score)` でスコアテキストを更新します。
- すべての星が獲得できたら、新しい星を配置し爆弾を作成します。
  - `if (this._stars.countActive(true) === 0)` の条件で、すべての星が獲得できたかチェックしています。
  - `this._stars.children.iterate` 内のコードで、すべての星のボディを再有効化し、元の位置に出現させます。
  - プレイヤーの位置に応じて、爆弾の初期位置をランダムに設定し、`this._bombs.create(x, 16, 'bomb')` で爆弾を作成します。`bomb.setBounce(1)` で爆弾にバウンド値を設定し、`bomb.setCollideWorldBounds(true)` で画面端で跳ね返るようにします。
`bomb.setVelocity(Phaser.Math.Between(-200, 200), 20)` でランダムな速度を設定します。

#### プレイヤーが爆弾に触れた時の処理を実装する
ゲームを停止し、プレイヤーを赤色にさせてから"GAME OVER"というテキストを表示させます。

```typescript
private hitBomb (
  player: Phaser.Types.Physics.Arcade.SpriteWithDynamicBody, 
  bomb: Phaser.Physics.Arcade.Group
) : void {
  this.physics.pause();

  player.setTint(0xff0000);

  player.anims.play('turn');
  
  this.add.text(200, 250, 'GAME OVER', { fontSize: '80px', fill: '#ff0000' });
}
```
##### コードの中身を解説
  - `player.setTint(0xff0000)` により、プレイヤーを赤色に表示させます。
  - `player.anims.play('turn')` により、プレイヤーの静止アニメーションを再生させます。
  - `this.add.text(200, 250, 'GAME OVER', { fontSize: '80px', fill: '#ff0000' })` で "GAME OVER" テキストを表示します。

## 完成
問題なく実装できていれば下記のようなものが出来上がります。

![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490435/39f94197-66fa-5fbd-1e08-4891fe03de97.gif)

ここまでお疲れ様でした。
拙い解説でここまでお付き合いいただきありがとうございました。

## tips
### デバッグ方法

Phaser.js には、リソースのロード状況をデバッグするための機能が用意されています。
以下のコードを `preload` メソッドに追加することで、ロードが成功したかどうかを確認できます。

```typescript
this.load.on('filecomplete', function (key) {
  console.log('File complete: ' + key);
});
this.load.on('loaderror', function (file) {
  console.log('Load error: ' + file.key);
});
```
私が実装した時、公式ドキュメントに記載されている画像素材なのになぜか読み込まない時があり苦戦しました。
上述の方法でデバッグする事で、devToolで確認する事ができます。
スクショのようにどのリソースが取得できていないか判断できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490435/4a0f9507-9086-8de1-9135-3e609d939399.png)

## 参考リンク
- [Phaser.js 公式サイト](https://phaser.io)
- [Phaser.js チュートリアル](https://phaser.io/tutorials/making-your-first-phaser-3-game/part1)
- [Phaser.js 素材ライブラリ](https://labs.phaser.io/index.html?dir=&q=)
ゲームで使用する素材はこちらのURLから取得できます。

## まとめ
Phaser.js公式のチュートリアルを通じて、Phaser.js を使ったシンプルなゲームの作成方法を学びました。
ぜひ、自分自身でコードを修正したり、機能を追加したりして、オリジナルのゲームを作成してみてください！

また、普段仕事でプログラムを触っているとどうしても課題解決、要望解決をする為の仕事道具になりがちですが、敢えて仕事とは全く関係のない分野に触れる事によってプログラミングそのものにワクワクする気持ちを思い出しました。
生成AIが流行っている今、敢えて書く！というのも悪くないと思います。
(誤解の無いよう、生成AI自体は素晴らしいものだと思いますし、エンジニアとして必要な知見だとも思っています。ただエンジニアなので生成AIが無いと何も書けないような状態にはなりたくないと個人としての想いのお話でした。)
初心を思い出すのは大事だと感じました。

:::note info
この記事を読んでいただいた誰か一人でも良きプログラミングライフのきっかけになれば嬉しいです。
:::