本記事はディップ 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というライブラリもあるみたいですが、期限までに試すことができませんでした)
どの様なライブラリを使うにせよ、来年はもっとしっかりしたゲームを作りたいですね。
みなさん、良いお年を!