チャプター 03 / 06

3. シーンと状態を管理する

MitiruEngine におけるシーン構成と状態管理。SceneRouter / StateMachine / Timer / Cooldown / Sequence を組み合わせて、ジャンルを問わない update ループを書く方法。

シーン (タイトル → メニュー → 戦闘 → リザルト…) の切り替えと、シーン内の細かい状態 (idle → attacking → cooldown) は、ジャンルを問わずゲームの背骨です。MitiruEngine はこれを C++ 側で素直に持つことを推奨しています。

  • シーン遷移mitiru::scene::SceneRouter でスタック型に積む / 差し替える
  • シーン内のフェーズ管理mitiru::fsm::StateMachine<EnumT> で enum 駆動の状態機械を持つ
  • 時間絡みの遷移Timer / Cooldown / Sequence で待つ

JSON で宣言する rule に逃がしてもいいですが、シンプルな C++ で書くほうが読みやすいというのがエンジンの方針です。

シーン遷移とフェーズ状態機械の例 外側にシーンルータ (Title → Menu → Battle → Result)、内側に Battle 内フェーズ (Idle → Windup → Strike → Recover) という 2 階建ての状態機械。

外側: SceneRouter

Title push

Menu replace

Battle replace

Result push

内側: Battle 内 StateMachine<AttackPhase>

Idle wait input

Windup 0.2s

Strike hitbox on

Recover → Idle

図: 外側のシーンと内側のフェーズは、両方とも C++ の素直な enum + 関数で書ける。

シーンルーター

#include <mitiru/scene/SceneRouter.hpp>

class TitleScene : public mitiru::scene::IScene {
public:
    void update(float dt) override { /* ... */ }
    void draw(mitiru::Screen& s) override { /* ... */ }
};

class BattleScene : public mitiru::scene::IScene { /* ... */ };

mitiru::scene::SceneRouter router;
router.push(std::make_unique<TitleScene>());

// プレイヤーが「始める」を選んだら
router.replace(std::make_unique<BattleScene>());

// ポーズメニューを上に積む
router.push(std::make_unique<PauseScene>());

// ポーズを閉じる
router.pop();

RPG のシーン階層 (世界 → 街 → 店) も、RTS の (ロビー → マッチ → リザルト) も、シューティングの (タイトル → ステージ → ゲームオーバー) も、すべて同じ push/pop/replace で書けます。

ステートマシン (シーン内のフェーズ)

#include <mitiru/fsm/StateMachine.hpp>

enum class AttackPhase { Idle, Windup, Strike, Recover };
mitiru::fsm::StateMachine<AttackPhase> sm{AttackPhase::Idle};

sm.transition(AttackPhase::Idle, AttackPhase::Windup);
// 0.2s 経過後...
sm.transition(AttackPhase::Windup, AttackPhase::Strike);
// hitbox を有効にして、その後...
sm.transition(AttackPhase::Strike, AttackPhase::Recover);

アクションのコンボ、シミュレーションの建築フェーズ、カードゲームのターン進行、どれにも同じパターンが効きます。

タイマー / クールダウン / シーケンス

mitiru::time::Cooldown fireCooldown{0.25f};   // 4 連射 / 秒
if (mapper.wasPressed(Action::Fire) && fireCooldown.ready()) {
    spawnBullet();
    fireCooldown.trigger();
}
fireCooldown.update(dt);

mitiru::time::Sequence intro;
intro.then([&]{ showLogo(); }, 1.0f)
     .then([&]{ showTitle(); }, 0.6f)
     .then([&]{ enableInput(); }, 0.0f);
intro.update(dt);

CEF 側にステートを通知する

シーン状態を HUD や UI に出すとき、JS にロジックを持たせず「C++ が決めた現在値を push して、HTML が表示する」だけにします。

engine.cefContext().executeJavaScript(
    "Mitiru.state.set('scene', '" + currentSceneName + "');");
<script>
  Mitiru.state.subscribe('scene', name => {
    document.getElementById('scene-label').textContent = name;
  });
</script>

関連 API

もっと深く知る