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

[Siv3D AdventCalendar 2015](http://qiita.com/advent-calendar/2015/siv3d) の２日目の記事です．

#概要
Siv3D に付属されている HamFramework の ham::SceneManager はシーン遷移をサポートするクラスです．以下のようなソースコードでタイトル，ゲーム，結果のシーンを切り替え，さらにシーン間で任意のデータを共有することができます．
![図2.png](https://qiita-image-store.s3.amazonaws.com/0/34295/82cc1411-6951-6e99-25bd-c0cd81964832.png)



```cpp: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つくろー！さんのサイト](http://marupeke296.com/GDEV_No7_StateImpliment2.html)でもシーン遷移の手法が解説されています．[新・ゲームプログラミングの館さんのサイト](http://dixq.net/g/sp_06.html)でもクラスの継承を用いたシーン遷移が紹介されています．

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

#基本的な使い方

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

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

void Main()
{
	SceneManager<String> manager;

	while (System::Update())
	{

	}
}

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

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

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

- int 型
- enum class 型

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

##シーンの作成

```cpp: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 関数内でメンバ変数の値の変更はできません．


##シーンの登録

```cpp: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 型で定義していた場合には，

```cpp: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 の中に一々追加する手間がかかることです．

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

```cpp: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 関数を使う必要が無いため，省略しています．）
それぞれのシーンの具体的な処理を記述していきます．今回の処理は，主に２つあります．

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

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

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

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

* updateFadeIn
* updateFadeOut
* drawFadeIn
* drawFadeOut

をオーバーライドしてください．引数には，フェードイン・アウトの経過を 0.0 → 1.0 の double 型が用意されています．
![図1.png](https://qiita-image-store.s3.amazonaws.com/0/34295/73e813c6-3c74-0435-27ce-7c90a0303549.png)

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

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

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

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

![図2.png](https://qiita-image-store.s3.amazonaws.com/0/34295/e342317f-74d3-e29c-329d-339295ebeac2.png)

```cpp: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 を用いて以下のように記述すると楽になります．

```cpp: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();
	}
}

```

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

```cpp: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](https://qiita-image-store.s3.amazonaws.com/0/34295/c50fd5ac-60cc-fb55-2b4b-4c1ea05e60cd.png)

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

```cpp: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 をクラスで管理
1. ゲーム A，B，C で名前が衝突しないよう，名前空間で分割
1. SceneManager を持つクラスを，親の SceneManager の基底クラスから継承
1. ゲーム A，B，C どこに行くかを決定できるメニューのシーンを作成

（ゲーム A，B，C の内容は全く同じソースコードを用いているので，一部省略しています．）

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

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

```cpp: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<void(String)> によって関数オブジェクトを作成
* 共有データを得る get 関数とラムダ式を用いて，関数オブジェクトの中身を具体的に定義
* ラムダ式内では，根本のシーン切り替えの関数を挿入

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

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

* [Siv3D 公式サンプル | シーン管理](https://github.com/Siv3D/Reference-JP/wiki/シーン管理)
* [Siv3D 公式サンプル | シーン切り替え(シルエット)](https://github.com/Siv3D/Reference-JP/wiki/シーン切り替え(シルエット))

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

* [Siv3Dでポーズ画面を簡単に実装する with HamFramework](http://chungames.hateblo.jp/entry/2015/04/01/001347)

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

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