Siv3D AdventCalendar 2015 の2日目の記事です.
#概要
Siv3D に付属されている HamFramework の ham::SceneManager はシーン遷移をサポートするクラスです.以下のようなソースコードでタイトル,ゲーム,結果のシーンを切り替え,さらにシーン間で任意のデータを共有することができます.
# 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 の生成と識別する型の決定
# include <Siv3D.hpp>
# include <HamFramework.hpp>
void Main()
{
SceneManager<String> manager;
while (System::Update())
{
}
}
まずシーンを管理するための準備を始めます.はじめの操作として
- HamFramework.hpp をインクルード
- SceneManager を作成
を行います.HamFramework.hpp のインクルードは Siv3D のデフォルトの設定ですぐに使うことができます.テンプレートには,シーンをどの型で識別するかを決めます.ここでは,文字列により識別できるよう String 型を用いました.String 型以外として
- int 型
- enum class 型
等を用いて管理することもできます.
##シーンの作成
# 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 関数内でメンバ変数の値の変更はできません.
##シーンの登録
//省略
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 型で定義していた場合には,
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 の中に一々追加する手間がかかることです.
##シーンの具体的な処理の記述
# 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 型が用意されています.
しかし,今の機能だけだと少し問題があります.具体的には
- シーン間でデータを共有できない
- 大きなデータをメンバに含んでいた場合に無駄な処理が多い
シーン間のデータの共有ができなければ,結果画面にゲームのスコアを描画する場合などの状況で不便です.また,大きなデータ(Font,Sound,Texture等)をシーンのメンバに含んでいたい場合には,シーン遷移する度に生成と破棄を繰り返してしまいます.そこで SceneManager のもう一つの機能として,データを共有する機能をつけました.
##シーン間の共有データの設定
//省略
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 を用いて以下のように記述すると楽になります.
//省略
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();
}
}
##シーン間の共有データ利用
# 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 を持つクラスの管理
# 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 を使う方法を紹介します.今回は例として,
- これまで Main 関数内で管理していた SceneManager をクラスで管理
- ゲーム A,B,C で名前が衝突しないよう,名前空間で分割
- SceneManager を持つクラスを,親の SceneManager の基底クラスから継承
- ゲーム A,B,C どこに行くかを決定できるメニューのシーンを作成
(ゲーム A,B,C の内容は全く同じソースコードを用いているので,一部省略しています.)
ところで,ゲーム A での処理中(例えば,ゲーム A での Result クラス内)にメニューへ移動したい場合はどうすればよいでしょうか.ゲーム A のシーンがアクセスできる範囲は,ゲーム A で設定した共有データのみなので,根本のシーンそれ自体を変更できないように思われます.それを解決する手法として,SceneManager のメンバ関数 changeScene を下の階層の共有データに渡し,下の階層のシーンから使えるようにする手法です.
##シーン切り替えの関数を下の階層から使用できるようにする手法
//省略
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 さんの記事です.よろしくお願いいたします.