22
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2023-12-12

本記事はディップ Advent Calendar 2023の13日目の記事です。

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

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

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

作ったゲーム

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

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

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

開発環境

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

Nextへのp5の導入にはp5Wrapperを使用しました。

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

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

// ゲームに存在するシーンの一覧
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} />;
}

するとこうなる

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

// 初期位置指定など、シーンで一回しか実行したくない処理がある場合
// 各シーンの初期化処理用のシーンも用意する必要がある
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なるものを作成し、もっとシンプルに管理できる様にしてみます。

実装

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

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

②sceneManagerを作成します。
シーン切り替え関数では

  • 切り替え前シーンの「削除時の関数」の実行
  • シーン切り替え
  • 切り替えたシーンの「初期化時の関数」の実行
    を実行します。
// シーン名とシーンファイル(型)を辞書型で管理
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の「実行中シーン」の更新処理を常に呼び出す様に設定します。
これで1箇所のswitchですべてのシーンの管理を行う必要がなくなりました。


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というライブラリもあるみたいですが、期限までに試すことができませんでした)

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

22
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?