p5.jsでシーン遷移のあるゲームを作りたい！

本記事は[ディップ Advent Calendar 2023](https://qiita.com/advent-calendar/2023/dip-dev)の13日目の記事です。

初めまして！
今年4月に入社したエージェント開発課の岡本です。
主に「ナースではたらこ」や「介護ではたらこ」のバックエンド部分の実装を行っています。

12月といったらクリスマスプレゼント、クリスマスプレゼントと言ったらゲーム！
ということで簡単なブラウザゲームを作ってみました。

その時に悩んだ「シーン管理の方法」を記事に起こしてみようと思います。

# 作ったゲーム

上から落ちてくるボールを落とさない様にバーで跳ね返す簡単なゲームを作りました。

https://next-p5-game.vercel.app/

このゲームには以下のシーンがあります。

- タイトル画面
  - タップするとゲームが始まる
- ゲーム画面
  - ボールを落としたらリザルト画面に移動する
- リザルト画面
  - 最終的なスコアが表示され、タップするとタイトル画面に戻る



# 開発環境

p5.js製のゲームはJSファイル単体で作られることが多いですが、興味本位からNext.jsで作ってみることにします。
（課でNext.jsを使用しており、触る機会が欲しかったので…）

Nextへのp5の導入には[p5Wrapper](https://github.com/P5-wrapper/next)を使用しました。

https://github.com/P5-wrapper/next

# シーンの切り替えをどう実装するか？

p5.jsには基本的な関数として`setup()`と`draw()`が存在します。
このうちの`draw()`は毎フレーム実行されており、ゲームの状態に応じて実行する関数を変える形でシーンを実装できます。
シーン状態を管理する変数と、それに応じて実行関数を変えるswitch文を実装する方法が一番シンプルだと思います。

```TypeScript
// ゲームに存在するシーンの一覧
enum GameScene {
  "A",
  "B",
  "C"
}

// 実行中のシーンを管理する変数
let mode: GameScene;

const sketch: Sketch = (p5: P5CanvasInstance) => {
  p5.setup = () => {
    ...
  };

  p5.draw = () => {
    // モードの状態に応じて実行するシーンを変更する
    switch (mode) {
      case GameScene.A:
        title.init(p5);
        break;
      case GameScene.B:
        title.update(p5);
        break;
      case GameScene.C:
        play.init(p5);
        break;
    }
  };
};

export default function Page() {
  return <NextReactP5Wrapper sketch={sketch} />;
}

```

# するとこうなる

上のやり方で実装するとこうなります。
（ゲームの処理面は~~酷いコードになっているので~~割愛します）

```TypeScript
// 初期位置指定など、シーンで一回しか実行したくない処理がある場合
// 各シーンの初期化処理用のシーンも用意する必要がある
enum GameScene {
  "Title",
  "PlayInit",
  "Play",
  "Result",
}

let title = {
  init: (p5: P5CanvasInstance) => {
    mode = GameScene.Title;
  },
  update: (p5: P5CanvasInstance) => {
    //
    // タイトル画面の描画処理
    //
    p5.mouseClicked = () => {
      mode = GameScene.PlayInit;
    };
  },
};

const play = {
  init: (p5: P5CanvasInstance) => {
    ball = new Ball(screenManager);
    bar = new Bar(screenManager);
    score = 0;
    mode = GameScene.Play;
  },
  update: (p5: P5CanvasInstance) => {
    //
    // ゲーム画面の描画・判定・スコア計算処理
    //
  },
};

const result = {
  init: (p5: P5CanvasInstance) => {
    mode = GameScene.Result;
  },
  update: (p5: P5CanvasInstance) => {
    //
    // ゲームオーバー・スコア表示処理
    //
    p5.mouseClicked = () => {
      mode = GameScene.TitleInit;
    };
  },
};

const sketch: Sketch = (p5) => {
  p5.setup = () => {
    p5.createCanvas(window.innerWidth, window.innerHeight, p5.WEBGL);
  };

  p5.draw = () => {
    switch (mode) {
      case GameScene.Title:
        title.update(p5);
        break;
      case GameScene.PlayInit:
        play.init(p5);
        break;
      case GameScene.Play:
        play.update(p5);
        break;
      case GameScene.Result:
        result.update(p5);
        break;
    }
  };
};

export default function Page() {
  return <NextReactP5Wrapper sketch={sketch} />;
}
```


…ものすごく力技な実装になってしまいました。
特に`p5.draw`内のswitch文が気に入りません。
`sceneManager`なるものを作成し、もっとシンプルに管理できる様にしてみます。

# 実装

①シーンファイルのベースを作成します。
どのシーンにも「初期化時の関数」「更新時の関数」「削除時の関数」があることを明示します。

```TypeScript
export class scene {
    constructor(protected sceneManager: SceneManager) {
    }
  // シーン切り替え時のみ実行する
  init(p5:P5CanvasInstance){}
  
  // 実行中常に実行する
  update(p5:P5CanvasInstance){}
  
  // シーン削除時のみ実行する
  destroy(){}
}
```

②sceneManagerを作成します。
シーン切り替え関数では
- 切り替え前シーンの「削除時の関数」の実行
- シーン切り替え
- 切り替えたシーンの「初期化時の関数」の実行
を実行します。

```TypeScript
// シーン名とシーンファイル（型）を辞書型で管理
export const SceneList: { [key: string]: typeof Scene } = {
  Title: TitleScene,
  Play: PlayScene,
  Result: ResultScene,
};

export class SceneManager {
  p5: P5CanvasInstance;
  
  // 実行中のシーンを保管
  scene: Scene;

  // シーンを跨いで使用する共有の情報はここで管理
  score = 0;

  constructor(
    ...
  ) {
　　　　　　　　//
    // 画面サイズの設定等の初期化処理を実行
    //

    this.scene = new SceneList['Title'](this);
  }

  // シーンの切り替え処理
  sceneChange(sceneName: string) {
    this.scene.destroy();
    this.scene = new SceneList[sceneName](this);
    this.scene.init(this.p5);
  }
}
```

③sceneManagerの「実行中シーン」の更新処理を常に呼び出す様に設定します。
これで１箇所のswitchですべてのシーンの管理を行う必要がなくなりました。

```TypeScript

let sceneManager: SceneManager;

const sketch: Sketch = (p5) => {
  p5.setup = () => {
    // sceneManagerで画面サイズなど、共通して使用する設定を管理しておくと使いやすそう
    // 細かい設定は今回の内容と関係ないので割愛
    sceneManager = new SceneManager(
      p5,
      9 / 16,
      window.innerWidth,
      window.innerHeight,
      "https://fonts.gstatic.com/ea/notosansjapanese/v6/NotoSansJP-Bold.otf"
    );

    p5.createCanvas(sceneManager.width, sceneManager.height, p5.WEBGL);
  };

  p5.draw = () => {
    // シーンを増やすたびにswitch文を書く必要がなくなった
    sceneManager.scene.update(p5);
  };
};

export default function Page() {
  return <NextReactP5Wrapper sketch={sketch} />;
}
```

# おわりに

p5.jsを使用したゲームの多くは単体ファイルで作成されており、「状態管理が必要なゲームの制作には向いてないのかな？」と思いましたが、意外といける様です。
一方、物理演算や音まわりなど、まだまだ試せてないこともあるので…どうだろう…
（P5.playというライブラリもあるみたいですが、期限までに試すことができませんでした）

どの様なライブラリを使うにせよ、来年はもっとしっかりしたゲームを作りたいですね。
みなさん、良いお年を！