Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

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 さんの記事です.よろしくお願いいたします.

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした