Siv3Dでのシーン遷移をサポートするSceneManagerの使い方とその応用

  • 16
    Like
  • 0
    Comment

Siv3D AdventCalendar 2015 の2日目の記事です.

概要

Siv3D に付属されている HamFramework の ham::SceneManager はシーン遷移をサポートするクラスです.以下のようなソースコードでタイトル,ゲーム,結果のシーンを切り替え,さらにシーン間で任意のデータを共有することができます.
図2.png

Main.cpp
# include <Siv3D.hpp>
# include <HamFramework.hpp>

struct CommonData
{
    int counter = 0;
    Font font{ 50 };
};

using MyApp = SceneManager<String, CommonData>;

class Title : public MyApp::Scene
{
public:

    void update() override
    {
        if (Input::MouseL.clicked)
        {
            ++m_data->counter;
            changeScene(L"Game");
        }           
    }

    void draw() const override
    {
        Window::ClientRect().draw(Palette::Blue);
        m_data->font(L"TITLE:", m_data->counter).drawCenter(Window::Center());
    }
};

class Game : public MyApp::Scene
{
public:

    void update() override
    {
        if (Input::MouseL.clicked)
        {
            ++m_data->counter;
            changeScene(L"Result");
        }
    }

    void draw() const override
    {
        Window::ClientRect().draw(Palette::Red);
        m_data->font(L"GAME:", m_data->counter).drawCenter(Window::Center());
    }
};

class Result : public MyApp::Scene
{
public:

    void update() override
    {
        if (Input::MouseL.clicked)
        {
            ++m_data->counter;
            changeScene(L"Title");
        }
    }

    void draw() const override
    {
        Window::ClientRect().draw(Palette::Green);
        m_data->font(L"RESULT:", m_data->counter).drawCenter(Window::Center());
    }
};

void Main()
{
    MyApp manager;

    manager.add<Title>(L"Title");
    manager.add<Game>(L"Game");
    manager.add<Result>(L"Result");

    while (System::Update())
    {
        manager.updateAndDraw();
    }
}

今回はその SceneManager を用いたシーン遷移の方法とその応用について解説します.

背景

シーン遷移とは,場面の切り替え処理のことです.例えば,タイトル,メニュー,ゲーム,ゲームクリアなどの場面を必要に応じて切り替える処理です.多くのゲームでは,この”場面の切り替え”が必要となるため,シーン遷移は C++ ゲームプログラミングで重要となります.簡単なシーン遷移の手法の一つとして,switch 文で切り替えるものがあります.小規模なゲームであれば,switch 文で切り替える手法で十分でしょう.しかし,大規模になりシーンの数が増えたり,分割して開発する必要が迫られた時には,長い switch 文の構造を必要に応じて書き直さなければなりません.このように switch 文で切り替える手法は時に不便となるため,これまで多くの C++ のシーン遷移の手法が書籍や WEB サイトで紹介されてきました.例えば平山尚さん(株式会社セガ)が出版した「ゲームプログラマになる前に覚えておきたい技術」には,シーン遷移(シーケンス遷移)に関して二章分割かれています.継承を用い階層的に管理する手法が紹介されています.Gameつくろー!さんのサイトでもシーン遷移の手法が解説されています.新・ゲームプログラミングの館さんのサイトでもクラスの継承を用いたシーン遷移が紹介されています.

シーン遷移をサポートする SceneManager

これらのシーン遷移手法を自分で構築するには,ある程度の時間が必要となります.特に自分はゲームプログラミングを学び始めた際にかなり苦しんだ部分でした.ゲームプログラミングの面白い部分により多くの時間をかけ,シーンの遷移にはあまり時間をかけたくないのです.恐らく自分以外の方も同じ経験をしているのではないかと思い,簡単にかつ汎用的に使えるシーン遷移のライブラリ ”SceneManager” を作成し公開しました.Siv3D にはゲーム機能に特化した HamFramework が提供されていますが,SceneManager はその HamFramework の機能の一つです.

基本的な使い方

SceneManager の生成と識別する型の決定

Main.cpp
# include <Siv3D.hpp>
# include <HamFramework.hpp>

void Main()
{
    SceneManager<String> manager;

    while (System::Update())
    {

    }
}

まずシーンを管理するための準備を始めます.はじめの操作として

  1. HamFramework.hpp をインクルード
  2. SceneManager を作成

を行います.HamFramework.hpp のインクルードは Siv3D のデフォルトの設定ですぐに使うことができます.テンプレートには,シーンをどの型で識別するかを決めます.ここでは,文字列により識別できるよう String 型を用いました.String 型以外として

  • int 型
  • enum class 型

等を用いて管理することもできます.

シーンの作成

Main.cpp
# include <Siv3D.hpp>
# include <HamFramework.hpp>

class Title : public SceneManager<String>::Scene
{
public:
    void init() override
    {

    }

    void update() override
    {

    }

    void draw() const override
    {

    }
};

class Game : public SceneManager<String>::Scene
{
public:
    void init() override
    {

    }

    void update() override
    {

    }

    void draw() const override
    {

    }
};

class Result : public SceneManager<String>::Scene
{
public:
    void init() override
    {

    }

    void update() override
    {

    }

    void draw() const override
    {

    }
};

void Main()
{
    SceneManager<String> manager;

    while (System::Update())
    {

    }
}

シーンの基底クラスを継承してシーンを定義します.

  • init 関数
  • update 関数
  • draw 関数

が用意されています.init 関数では,シーンを切り替えた時に一度だけ呼ばれます(省略可).update 関数と draw 関数は毎フレーム,update,draw の順番で呼ばれます.update と draw の主な違いとしては,update が普通のメンバ関数であるのに対し,draw は const メンバ関数であることです.つまり,draw 関数内でメンバ変数の値の変更はできません.

シーンの登録

Main.cpp
//省略

void Main()
{
    SceneManager<String> manager;

    manager.add<Title>(L"Title");
    manager.add<Game>(L"Game");
    manager.add<Result>(L"Result");

    while (System::Update())
    {
        manager.updateAndDraw();
    }
}

SceneManager にシーンを登録しています.シーンの追加には SceneManager の add 関数を利用します.add 関数の使い方として,

  • クラスの名前をテンプレートに挿入
  • シーンの切り替えに用いる名前を引数に挿入

をします.今回は String 型を定義していたため,L"Title",L"Game",L"Result"を入れています.while 文の方には,updateAndDraw 関数により,シーンの処理が開始します.初期設定では,SceneManager に最初に登録したシーンが開始します.もし初期のシーンを変更したい場合には,SceneManager の init 関数の引数にて名前を入れることで設定してください.

String 型ではなく,int 型で定義していた場合は0,1,2,3などの数字を入れます.また,enum class 型で定義していた場合には,

Main.cpp

enum class SceneName
{
    Title,
    Game,
    Result
};

//省略

void Main()
{
    SceneManager<SceneName> manager;

    manager.add<Title>(SceneName::Title);
    manager.add<Game>(SceneName::Game);
    manager.add<Result>(SceneName::Result);

    while (System::Update())
    {
        manager.updateAndDraw();
    }
}

のように列挙体を設定できます.この手法の利点としては,シーンを切り替える時にインテリセンスによりミスが減ることです.逆に欠点はシーンを追加する毎に enum class の中に一々追加する手間がかかることです.

シーンの具体的な処理の記述

Main.cpp
# include <Siv3D.hpp>
# include <HamFramework.hpp>

class Title : public SceneManager<String>::Scene
{
public:

    void update() override
    {
        if (Input::MouseL.clicked)
            changeScene(L"Game");
    }

    void draw() const override
    {
        Window::ClientRect().draw(Palette::Blue);
        font(L"TITLE").drawCenter(Window::Center());
    }

    Font font{ 50 };
};

class Game : public SceneManager<String>::Scene
{
public:

    void update() override
    {
        if (Input::MouseL.clicked)
            changeScene(L"Result");
    }

    void draw() const override
    {
        Window::ClientRect().draw(Palette::Red);
        font(L"GAME").drawCenter(Window::Center());
    }

    Font font{ 50 };
};

class Result : public SceneManager<String>::Scene
{
public:

    void update() override
    {
        if (Input::MouseL.clicked)
            changeScene(L"Title");
    }

    void draw() const override
    {
        Window::ClientRect().draw(Palette::Green);
        font(L"RESULT").drawCenter(Window::Center());
    }

    Font font{ 50 };
};

void Main()
{
    SceneManager<String> manager;

    manager.add<Title>(L"Title");
    manager.add<Game>(L"Game");
    manager.add<Result>(L"Result");

    while (System::Update())
    {
        manager.updateAndDraw();
    }
}

(今回は init 関数を使う必要が無いため,省略しています.)
それぞれのシーンの具体的な処理を記述していきます.今回の処理は,主に2つあります.

  • マウスを左クリックした場合に,シーンを変更
  • 色のついた背景の上に自分のシーン名を描画

シーンの変更には,基底クラスにて定義された changeScene 関数を使います.changeScene 関数の引数に,シーンの追加時に設定した名前を入れることで,そのシーンに切り替えることができます.デフォルトの設定では,シーン切替時に 1000 ms (一秒間)黒色のフェードイン・アウトが行われます.もしフェードの時間や色を変えたい場合には

  • フェード時間[ms]を changeScene の第二引数に挿入
  • フェードの色を SceneManager の setFadeColor の引数に挿入

で変更できます.また,フェードイン・アウトをしている間は update 関数と draw 関数での挙動が異なるます.フェードイン・アウトの最中に別の操作を受け付けないよう draw のみが処理され,update の処理は飛ばされます.もしフェードイン・アウトの最中に処理を行う必要がある場合には,基底クラスにて定義された

  • updateFadeIn
  • updateFadeOut
  • drawFadeIn
  • drawFadeOut

をオーバーライドしてください.引数には,フェードイン・アウトの経過を 0.0 → 1.0 の double 型が用意されています.
図1.png

しかし,今の機能だけだと少し問題があります.具体的には

  1. シーン間でデータを共有できない
  2. 大きなデータをメンバに含んでいた場合に無駄な処理が多い

シーン間のデータの共有ができなければ,結果画面にゲームのスコアを描画する場合などの状況で不便です.また,大きなデータ(Font,Sound,Texture等)をシーンのメンバに含んでいたい場合には,シーン遷移する度に生成と破棄を繰り返してしまいます.そこで SceneManager のもう一つの機能として,データを共有する機能をつけました.

シーン間の共有データの設定

図2.png

Main.cpp
//省略

struct CommonData
{
    int counter = 0;
    Font font{ 50 };
};

//省略

void Main()
{
    SceneManager<String, CommonData> manager;

    manager.add<Title>(L"Title");
    manager.add<Game>(L"Game");
    manager.add<Result>(L"Result");

    while (System::Update())
    {
        manager.updateAndDraw();
    }
}

SceneManager に,どの型のデータを共有するかを設定します.SceneManager 作成時の,テンプレートの第二引数にて設定します.SceneManager のコンストラクタで何も引数を入れない場合では,SceneManager 生成時に内部にて CommonData の生成を始めます.もし予め作成した CommonData を用いたければ,SceneManager のコンストラクタの引数に入れ設定することができます.

また,SceneManager の方の名前が少し長くなり,基底クラスの名前の設定が手間なので,using を用いて以下のように記述すると楽になります.

Main.cpp
//省略

struct CommonData
{
    int counter = 0;
    Font font{ 50 };
};

using MyApp = SceneManager<String, CommonData>;

//省略

void Main()
{
    MyApp manager;

    manager.add<Title>(L"Title");
    manager.add<Game>(L"Game");
    manager.add<Result>(L"Result");

    while (System::Update())
    {
        manager.updateAndDraw();
    }
}

シーン間の共有データ利用

Main.cpp
# include <Siv3D.hpp>
# include <HamFramework.hpp>

struct CommonData
{
    int counter = 0;
    Font font{ 50 };
};

using MyApp = SceneManager<String, CommonData>;

class Title : public MyApp::Scene
{
public:

    void update() override
    {
        if (Input::MouseL.clicked)
        {
            ++m_data->counter;
            changeScene(L"Game");
        }           
    }

    void draw() const override
    {
        Window::ClientRect().draw(Palette::Blue);
        m_data->font(L"TITLE:", m_data->counter).drawCenter(Window::Center());
    }
};

class Game : public MyApp::Scene
{
public:

    void update() override
    {
        if (Input::MouseL.clicked)
        {
            ++m_data->counter;
            changeScene(L"Result");
        }
    }

    void draw() const override
    {
        Window::ClientRect().draw(Palette::Red);
        m_data->font(L"GAME:", m_data->counter).drawCenter(Window::Center());
    }
};

class Result : public MyApp::Scene
{
public:

    void update() override
    {
        if (Input::MouseL.clicked)
        {
            ++m_data->counter;
            changeScene(L"Title");
        }
    }

    void draw() const override
    {
        Window::ClientRect().draw(Palette::Green);
        m_data->font(L"RESULT:", m_data->counter).drawCenter(Window::Center());
    }
};

void Main()
{
    MyApp manager;

    manager.add<Title>(L"Title");
    manager.add<Game>(L"Game");
    manager.add<Result>(L"Result");

    while (System::Update())
    {
        manager.updateAndDraw();
    }
}

シーン間で共有したデータの利用方法を説明します.シーンの基底クラスには m_data と名付けられたポインタが用意されています.この m_data からデータを変更や利用を行います.今回は

  • マウスを左クリックする毎に数字を増やし描画する
  • Font を共有データに入れる

といった処理をしています.Font 型が共有データで管理されることで毎回生成・破棄していた無駄な処理を減らしています.
以上が SceneManager の主な使い方になります.ちょっとしたゲームやプログラムを作るときに,クラスで分けて開発したい場合には便利です.また,過去のソースコードなどを利用する際にも,少し手を加えるだけですぐに適用できます.

応用 階層的なシーンの管理

分担して開発する場合や,ゲームの規模が大きくなる場合には,階層的にシーンを管理したくなる事があります.例えば,

  • 三人でそれぞれ SceneManager を使い三つのゲーム A,B,C を開発
  • それぞれのゲーム A,B,C を一つのゲームに統合

といった状況です.ゲーム A 内からゲーム B へ移動する時はどのようにすればよいでしょうか.これまで書いたソースコードは極力書き換えたくはありません.

図3.png

SceneManager による SceneManager を持つクラスの管理

Main.cpp
# include <Siv3D.hpp>
# include <HamFramework.hpp>

struct CommonData
{
    Font font{ 50 };
};

using MyBaseApp = SceneManager < String, CommonData > ;

namespace GameA
{
    struct CommonData
    {
        int counter = 0;
        Font font{ 50 };
    };

    using MyApp = SceneManager < String, CommonData > ;

    class Title : public MyApp::Scene
    {
    public:

        void update() override
        {
            if (Input::MouseL.clicked)
            {
                ++m_data->counter;
                changeScene(L"Game");
            }
        }

        void draw() const override
        {
            Window::ClientRect().draw(Palette::Blue);
            m_data->font(L"TITLE:", m_data->counter).drawCenter(Window::Center());
        }
    };

    class Game : public MyApp::Scene
    {
    public:

        void update() override
        {
            if (Input::MouseL.clicked)
            {
                ++m_data->counter;
                changeScene(L"Result");
            }
        }

        void draw() const override
        {
            Window::ClientRect().draw(Palette::Red);
            m_data->font(L"GAME:", m_data->counter).drawCenter(Window::Center());
        }
    };

    class Result : public MyApp::Scene
    {
    public:

        void update() override
        {
            if (Input::MouseL.clicked)
            {
                ++m_data->counter;
                changeScene(L"Title");
            }
        }

        void draw() const override
        {
            Window::ClientRect().draw(Palette::Green);
            m_data->font(L"RESULT:", m_data->counter).drawCenter(Window::Center());
        }
    };

    class GameSystem : public MyBaseApp::Scene
    {
    public:

        void init() override
        {
            manager.add<Title>(L"Title");
            manager.add<Game>(L"Game");
            manager.add<Result>(L"Result");
        }

        void update() override
        {
            manager.update();
        }

        void draw() const override
        {
            manager.draw();
        }

        MyApp manager;
    };
}

namespace GameB
{
    //省略(GameAと同じ)
}

namespace GameC
{
    //省略(GameAと同じ)
}

class GameMenu : public MyBaseApp::Scene
{
public:

    void update() override
    {
        if (Input::KeyA.clicked)
            changeScene(L"GameA");

        if (Input::KeyB.clicked)
            changeScene(L"GameB");

        if (Input::KeyC.clicked)
            changeScene(L"GameC");
    }

    void draw() const override
    {
        m_data->font(L"KeyA→ GameA\nKeyB→ GameB\nKeyC→ GameC").drawCenter(Window::Center());
    }

};

void Main()
{
    MyBaseApp manager;

    manager.add<GameMenu>(L"Menu");
    manager.add<GameA::GameSystem>(L"GameA");
    manager.add<GameB::GameSystem>(L"GameB");
    manager.add<GameC::GameSystem>(L"GameC");

    while (System::Update())
    {
        manager.updateAndDraw();
    }
}

階層的に SceneManager を使う方法を紹介します.今回は例として,

  1. これまで Main 関数内で管理していた SceneManager をクラスで管理
  2. ゲーム A,B,C で名前が衝突しないよう,名前空間で分割
  3. SceneManager を持つクラスを,親の SceneManager の基底クラスから継承
  4. ゲーム A,B,C どこに行くかを決定できるメニューのシーンを作成

(ゲーム A,B,C の内容は全く同じソースコードを用いているので,一部省略しています.)

ところで,ゲーム A での処理中(例えば,ゲーム A での Result クラス内)にメニューへ移動したい場合はどうすればよいでしょうか.ゲーム A のシーンがアクセスできる範囲は,ゲーム A で設定した共有データのみなので,根本のシーンそれ自体を変更できないように思われます.それを解決する手法として,SceneManager のメンバ関数 changeScene を下の階層の共有データに渡し,下の階層のシーンから使えるようにする手法です.

シーン切り替えの関数を下の階層から使用できるようにする手法

Main.cpp
//省略

namespace GameA
{
    struct CommonData
    {
        int counter = 0;
        Font font{ 50 };
        std::function<void(String)> changeBaseScene;
    };

//省略

    class Result : public MyApp::Scene
    {
    public:

        void update() override
        {
            if (Input::MouseL.clicked)
            {
                ++m_data->counter;
                changeScene(L"Title");
            }

            if (Input::MouseR.clicked)
                m_data->changeBaseScene(L"Menu");
        }

        void draw() const override
        {
            Window::ClientRect().draw(Palette::Green);
            m_data->font(L"RESULT:", m_data->counter).drawCenter(Window::Center());
        }
    };

    class GameSystem : public MyBaseApp::Scene
    {
    public:

        void init() override
        {
            manager.get()->changeBaseScene = [&](String state){ changeScene(state); };

            manager.add<Title>(L"Title");
            manager.add<Game>(L"Game");
            manager.add<Result>(L"Result");
        }

        void update() override
        {
            manager.update();
        }

        void draw() const override
        {
            manager.draw();
        }

        MyApp manager;
    };
}

//省略

シーンの切り替えの関数を下の階層から使えるようにしています.今回は,ゲーム A の結果画面において,右クリックをするとメニューのシーンに切り替えるようにしています.ここで着目するのは,

  • ゲームAのなかの共有データに std::function によって関数オブジェクトを作成
  • 共有データを得る get 関数とラムダ式を用いて,関数オブジェクトの中身を具体的に定義
  • ラムダ式内では,根本のシーン切り替えの関数を挿入

していることです.これによりゲーム A のシーンからメニュー画面に移動するような仕組みを入れることができます.

参考になるサンプル

今回紹介したソースコード以外のサンプルが見たい方は,Reputeless さんが書いたサンプルを見ると参考になります.

また,chunchunmorning さんによる SceneManager でのポーズ実装方法の解説があります.

まとめ

SceneManager の使用方法とその応用について解説しました.シーン遷移はゲームプログラミングとC++を学んだ際に非常に苦戦した場所です.SceneManager と Siv3D の利用により,より多くの人がゲームプログラミングとC++を楽しめることを願っています.

明日は @Reputeless さんの記事です.よろしくお願いいたします.