Edited at
Siv3DDay 2

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

More than 1 year has passed since last update.

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


概要

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


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. シーン間でデータを共有できない

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

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


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


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 へ移動する時はどのようにすればよいでしょうか.これまで書いたソースコードは極力書き換えたくはありません.


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