SceneManager
🎮 SceneManagerとは?
ゲームは実は 画面の集合です。
タイトル
↓
ゲーム選択
↓
ミニゲーム
↓
リザルト
↓
称号画面
これらを Scene(シーン) と呼びます。
🧠 SceneManagerの役割
SceneManagerは
今どの画面か?
を管理します。
currentScene = TitleScene
そして毎フレーム
update()
draw()
を呼びます。
🎮 完成形のイメージ
SceneManager
↓
TitleScene
GameSelectScene
MiniGameScene
ResultScene
CollectionScene
🧩 ① Sceneの基本クラス
まず すべての画面の共通クラスを作ります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Scene{ constructor(manager){ this.manager = manager; } start(){} update(){} draw(ctx){} } |
🧠 ② SceneManager
これがゲームの司令塔です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
class SceneManager{ constructor(canvas){ this.canvas = canvas; this.ctx = canvas.getContext("2d"); this.currentScene = null; } change(scene){ this.currentScene = scene; if(this.currentScene.start){ this.currentScene.start(); } } update(){ if(this.currentScene){ this.currentScene.update(); } } draw(){ if(this.currentScene){ this.currentScene.draw(this.ctx); } } } |
🎮 ③ メインループ
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const canvas = document.getElementById("game"); const manager = new SceneManager(canvas); function gameLoop(){ manager.update(); manager.draw(); requestAnimationFrame(gameLoop); } gameLoop(); |
🎮 ④ タイトル画面
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
class TitleScene extends Scene{ update(){ if(mouse.clicked){ this.manager.change( new GameSelectScene(this.manager) ); } } draw(ctx){ ctx.fillStyle="black"; ctx.fillRect(0,0,800,600); ctx.fillStyle="white"; ctx.font="40px sans-serif"; ctx.fillText("MY MINI GAME",250,250); ctx.font="20px sans-serif"; ctx.fillText("CLICK TO START",300,320); } } |
🎮 ⑤ ゲーム選択画面
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
class GameSelectScene extends Scene{ update(){ if(mouse.clicked){ this.manager.change( new MiniGameScene(this.manager) ); } } draw(ctx){ ctx.fillStyle="darkblue"; ctx.fillRect(0,0,800,600); ctx.fillStyle="white"; ctx.font="30px sans-serif"; ctx.fillText("SELECT GAME",300,200); ctx.fillText("CLICK GAME START",260,300); } } |
🎮 ⑥ ミニゲーム画面
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
class MiniGameScene extends Scene{ start(){ this.score = 0; } update(){ if(mouse.clicked){ this.score++; } if(this.score >= 10){ this.manager.change( new ResultScene(this.manager,this.score) ); } } draw(ctx){ ctx.fillStyle="green"; ctx.fillRect(0,0,800,600); ctx.fillStyle="white"; ctx.font="30px sans-serif"; ctx.fillText("CLICK!",350,200); ctx.fillText("SCORE:"+this.score,330,300); } } |
🎮 ⑦ リザルト画面
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
class ResultScene extends Scene{ constructor(manager,score){ super(manager); this.score = score; } update(){ if(mouse.clicked){ this.manager.change( new TitleScene(this.manager) ); } } draw(ctx){ ctx.fillStyle="purple"; ctx.fillRect(0,0,800,600); ctx.fillStyle="white"; ctx.font="40px sans-serif"; ctx.fillText("RESULT",320,200); ctx.fillText("SCORE:"+this.score,300,300); } } |
🎮 ⑧ 最初のシーン
|
1 2 3 |
manager.change( new TitleScene(manager) ); |
🚀 これで完成するゲーム構造
TitleScene
↓
GameSelectScene
↓
MiniGameScene
↓
ResultScene
↓
TitleScene
関連記事(jsでボタンをクラスでまとめる)
Webゲーム定番パターン
① 基本思想
JSを 役割ごとに分離します。
state = 状態管理
ui = 画面操作
api = サーバー通信
actions = ボタン処理
構造
janken-game.js
中身
Game
├ state
├ ui
├ api
└ actions
② 全体の基本構造
const Game = { state: {},
ui: {},
api: {},
actions: {}};
この Gameオブジェクトに全部入れるのがポイントです。
理由
グローバル汚染を防ぐ
③ state(ゲーム状態)
ここは データ保存専用
Game.state = { playerId: null,
playerName: "",
selectedHand: null,
attackTarget: null,
round: null};
重要ルール
stateはデータだけ
DOM触らない
④ UIモジュール
画面更新は全部ここ。
Game.ui = { show(el){
document.getElementById(el).style.display = "block";
}, hide(el){
document.getElementById(el).style.display = "none";
}, setText(id,text){
document.getElementById(id).textContent = text;
}};
例えば
Game.ui.setText("player-name", "なまえ");
⑤ APIモジュール
サーバー通信はここだけ。
Game.api = { async register(name){ const res = await fetch("/api/register",{
method:"POST",
body:JSON.stringify({name})
}); return res.json(); }, async attack(data){ const res = await fetch("/api/attack",{
method:"POST",
body:JSON.stringify(data)
}); return res.json(); }};
メリット
API変更がここだけで済む
⑥ actions(ボタン処理)
ここが ゲームロジック
Game.actions = { async register(){ const name =
document.getElementById("display-name").value; const res = await Game.api.register(name); Game.state.playerId = res.id; Game.ui.setText("player-name", name); Game.ui.hide("register-box");
Game.ui.show("game-box"); }};
⑦ イベント委任
クリック管理は1か所だけ。
document
.getElementById("janken-app")
.addEventListener("click",(e)=>{ const action = e.target.dataset.action; if(!action) return; if(Game.actions[action]){
Game.actions[action](e);
}});
⑧ HTMLはこう変える
<button data-action="register">登録する</button><button data-action="revive">復活する</button><button data-action="startSet">カードをセット</button><button data-action="startAttack">カードに挑む</button>
JSは触らなくてOK。
⑨ 手ボタン
<button data-hand="guu">✊</button>
<button data-hand="choki">✌️</button>
<button data-hand="paa">✋</button>
JS
if(e.target.dataset.hand){ Game.state.selectedHand =
e.target.dataset.hand;}
⑩ とあるパターンのゲームでの完成構造
JSファイル
janken-game.js
構造
Game
├ state
│ ├ player
│ ├ round
│ └ selectedHand
│
├ ui
│ ├ show
│ ├ hide
│ ├ updateProfile
│ └ updateRanking
│
├ api
│ ├ register
│ ├ revive
│ ├ setCard
│ └ attack
│
└ actions
├ register
├ revive
├ startSet
├ submitCard
└ attack
スクロールゲームでのオブジェクト管理パターン
代表的な手法
1. オブジェクトプール(Object Pool)パターン
最もよく使われる手法です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 事前に固定個数を確保しておき、使い回す const pool = Array.from({ length: 20 }, () => ({ active: false, x: 0, y: 0 })); function spawnFromPool(x, y) { const obj = pool.find(o => !o.active); if (!obj) return; // プール枯渇時はスキップ obj.active = true; obj.x = x; obj.y = y; } function update() { pool.forEach(obj => { if (!obj.active) return; obj.x += obj.vx; if (isOffScreen(obj)) obj.active = false; // 返却(削除しない) }); } |
メリット: push/filter による配列の生成・GC負荷ゼロ。60fps維持に有利。
2. チャンクベースのスポーン
横スクロールゲームで多用されます。
|
1 2 |
[画面外右側 = 生成ゾーン] [画面内 = 更新ゾーン] [画面外左側 = 削除ゾーン] ←── cameraX ──────────────────────────────────→ |
- プレイヤーが進んだ分だけ「前方一定距離」にオブジェクトを生成
- 「最後のオブジェクトからの距離」ではなく「cameraX + 画面幅 + マージン」を閾値にする
3. イベント駆動スポーン
|
1 2 3 |
毎フレーム: if (最前方オブジェクトX < player.x + 生成閾値) → 追加生成 if (オブジェクトX < camera.x - 削除マージン) → 削除 |
シンプルですが、生成条件と削除条件が独立していることが重要です。
コメント