はじめに
DxLibレベルのライブラリを前提としたシーン遷移を実装したので紹介する。
注意点
この記事にはポインタを直接扱う部分が出てくるが、スマートポインタや自作クラスを利用することで容易に回避できるので流してもらいたい。(スマポを使わない理由は最後かくかも)
この記事で紹介する内容のほとんどはSiv3Dでより使いやすいものが実装されておりわざわざ自分で実装する必要がない事がほとんどなので、使えればいいという人はOpenSiv3Dもあわせてそちらのほうをみてもらうといいだろう。
Visual Studio2019で開発しているのでその周りのコメントが多い。
方針
・使う上での懸念事項は少なめに
・細かな変更による再コンパイルを最小限に
アイデア
クラスの継承を利用したポリモーフィズムを利用して現在のシーンを更新描画する。
シーンの遷移は更新を行うupdate関数の返り値をみて行う。
基底クラス
各シーンの基底となるクラスを作成する。
今回はクラス名をSceneとした。
update関数の返り値については後述するので流してもらいたい。
#pragma once
class Scene {
protected:
//コンストラクタ
Scene() {}
public:
//コピー禁止
Scene(const Scene &) = delete;
//コピー禁止
Scene &operator=(const Scene &) = delete;
//仮想関数化したデストラクタ
virtual ~Scene() = default;
public:
virtual Scene *update() = 0;
virtual void draw() const = 0;
};
各シーンはこれらを継承して作成する。
Titleクラスを作ってみる。
#pragma once
#include"Scene.h"
class Title : public Scene {
public:
Title();
virtual ~Title();
virtual Scene *update() override;
virtual void draw() const override;
};
遷移を行う
update関数の返り値を利用して遷移を実現する。
具体的には、遷移を行わないフレームではthisポインタを返し、遷移を行いたいフレームでは遷移先の実体を生成しそのポインタを返すというものだ。
つまり、TitleからGameへの遷移を行いたいときはTitle::update内で
return new Game();
のようなものを実行すればよい。
上の書き方は後ほど変更するのであまり気にしないでもらいたい。
シーン管理クラス
シーンを管理するクラスを作成する。
今回はクラス名をSceneManagerとした。
#pragma once
class Scene;
class SceneManager {
public:
SceneManager();
SceneManager(const SceneManager &) = delete;
SceneManager &operator=(const SceneManager &) = delete;
~SceneManager();
private:
Scene *mScene;
public:
void update();
void draw() const;
};
前節の遷移の仕方を参考にupdate関数等を実装する。
#include"SceneManager.h"
#include"Title.h"
SceneManager::SceneManager() :
mScene(Title())
{}
SceneManager::~SceneManager() {
delete mScene;
}
void SceneManager::update() {
Scene *p = mScene->update();
if(p != mScene) {
delete mScene;
mScene = p;
}
}
void SceneManager::draw() const {
mScene->draw();
}
あとは実体を生成した後メインループ内でupdate関数とdraw関数を順番に実行するだけでよい。
draw関数の必要性は後にわかる。
値の共有
値を共有する方法はいくつかあるが、今回は共有するデータをまとめたクラスを使って実現する。
今回はクラス名をCommonDataとする。
class CommonData {
public:
CommonData();
//...
};
データが膨大になったときのことを考えてポインタを渡すことになるが、問題はそれをどこで渡すかだ。
update関数の引数にする手もあるが、後ほど紹介することの都合上これは避けてコンストラクタで渡すことにする。
実体の管理はSceneManagerで行い、コンストラクタでそのポインタを各シーンに渡す。
class CommonData;
class SceneManager {
//...
private:
CommonData *const mCommonData;
};
SceneManager::SceneManager() :
mCommonData(new CommonData())
//, ...
{}
SceneManager::~SceneManager() {
delete mCommonData;
}
すべてのシーンに必要なのに毎回書いていては面倒なので、基底クラスのprotectedメンバに追加し基底クラスのコンストラクタで初期化しよう。
class CommonData;
class Scene {
//...
protected:
Scene(CommonData *const commonData) :
mCommonData(commonData)
{}
//...
protected:
CommonData *const mCommonData;
};
あとは各シーンで必要になったときにCommonData.hppをインクルードして自分の持っているポインタから値を変更すればよい。
シーンクラスの生成方法の変更
各シーンのupdate関数は、シーンの遷移を実行するときに次のシーンの実体を生成する必要があった。
言い換えれば、次のシーンのクラスの定義が書かれたヘッダーをインクルードする必要があるのだ。
せっかくポリモーフィズムを利用して各シーンを独立に扱おうとしているのに、次のシーンの内容が変更されただけで再コンパイルをくらうのは納得いかない。
そこでテンプレートの特殊化を利用して各シーンを完全に独立に扱う。
具体的にコードで見ていこう。
まず、Sceneに次のようなテンプレート関数の宣言を追加する。
class Scene {
//...
protected:
template<typename T>
Scene *makeScene();
};
次に、各シーンのcppファイルのどこかで関数の定義を追加する。
template<>
Scene *Scene::makeScene<Title>() {
return new Title(commonData);
}
すべてのシーンで関数の定義を書くことで、遷移先のクラスの宣言のみで遷移を実行できる。
//...
class Game; //遷移先のクラスの宣言
Scene *Title::update() {
//...
return makeScene<Game>(); //遷移を実行
}
しかしこうすると最初SceneManager内でシーンを生成できない。
そこでSceneManagerもSceneを継承する。
こうしない方法もあるが、後の事情でこうしておく。
class SceneManager : public Scene {
//...
};
実は、こうすることで各シーンのクラスが書かれたヘッダーファイルは必要なくなり、すべてcppファイルに書くことができる。
ヘッダーファイルを用意するかどうかは完全に個人の趣味になるので自由にしてもらって構わない。
もしかしたらコンストラクタのインライン展開的なものが変わるかもしれない。
ここまでのまとめ
ここまでの内容である程度小規模なゲームでは問題なく利用できるものになっている。
各ファイルの内容を以下においておく。
#pragma once
class CommonData;
class Scene {
protected:
//コンストラクタ
Scene(CommonData *const commonData) :
commonData(commonData)
{}
public:
//コピー禁止
Scene(const Scene &) = delete;
//コピー禁止
Scene &operator=(const Scene &) = delete;
//仮想関数化したデストラクタ
virtual ~Scene() = default;
protected:
//共有データ
CommonData *const commonData;
public:
virtual Scene *update() = 0;
virtual void draw() const = 0;
protected:
//実体生成用関数
template<typename T>
Scene *makeScene();
};
#pragma once
class CommonData {
public:
CommonData() {}
//...
};
#pragma once
#include"Secene.hpp"
class SceneManager : public Scene {
public:
SceneManager();
~SceneManager();
private:
Scene *mScene;
public:
void update();
void draw() const;
};
#include"SceneManager.hpp"
#include"CommonData.hpp"
class Title;
SceneManager::SceneManager() :
Scene(new CommonData()),
mScene(makeScene<Title>())
{}
SceneManager::~SceneManager() {
if(mScene != nullptr) delete mScene;
delete commonData;
}
void SceneManager::update() {
Scene *p = mScene->update();
if(p != mScene) {
delete mScene;
mScene = p;
}
}
void SceneManager::draw() const {
mScene->draw();
}
#pragma once
#include"Scene.hpp"
class Title : public Scene {
public:
Title(CommonData *const commonData) :
Scene(commonData)
{
//...
}
~Title() {
//...
}
virtual Scene *update() override {
//...
return this;
}
virtual void draw() const override {
//...
}
};
template<>
Scene *Scene::makeScene<Title>() {
return new Title();
}
階層化
規模が大きくなってくると、CommonDataが少し変更されるだけで再コンパイルされるファイルも増えてきてしまう。
実際、今のままではCommonDataはグローバル変数と大して変わりがない。
そこで、シーン管理の構造を階層化する。
Titleの次のGameシーンを、PlayシーンとPauseシーンに分けてみよう。
Playシーンはゲーム本体の処理が書かれるシーン、Pauseシーンはアイテム整理などが行われるシーンとでも思っておくと理解がしやすいだろう。
実装の手順は以下のようにする。
・階層ごとにSceneを継承した新たな基底クラスを作る。
・各シーンは新たな基底クラスを継承して作る。
・その階層を管理するクラスをSceneを継承して作成する。
・SceneManagerを参考にdynamic_castで階層間遷移を行うか確認しながら更新。
・共有データ群GameDataクラスのポインタを基底クラスに持たせる。
コードにしていこう。
まず、階層の基底クラスを作成する。
#pragma once
#include"Scene.hpp"
class GameData;
class Game : public Scene {
protected:
Game(Scene *scene, GameData *const gameData) :
Scene(*scene),
gameData(gameData)
{}
Game(const Game &) = default;
public:
virtual ~Game() = default;
protected:
GameData *const gameData;
public:
virtual Scene *update() override = 0;
virtual void draw() const override = 0;
//実体生成用関数
template<typename T>
Scene *makeScene();
};
実体生成用関数は、コンストラクタの引数が異なるので新たに宣言する。
コンストラクタの引数を階層が深くなるにつれて増えないように、一つ浅い階層のポインタをもらってきてコピーコンストラクタを実行させる。
これに伴ってSceneのコピーコンストラクタをデフォルト宣言に変更する。
こうすることで
Scene(const Scene &) = default;
Gameクラスのコピーコンストラクタもこの方式に従った。
仮想関数を持つクラスのインスタンスは、自分がどのクラスであるかを判断するために仮想関数テーブルというものを持つ。
コピーコンストラクタでは、仮想関数テーブルの周りで何かが起きてしまうかもしれないが、とりあえず正常に動いているのでこのまま進める。(つよい人教えてください)
対策としては、コンストラクタの引数で基底クラスのポインタを持ってくることでコピーコンストラクタではなく、通常のコンストラクタとして扱われるため、そこでポインタのコピーを記述する方法がある。
大して複雑な変更ではないのでやってみてもいいかもしれない。
次にGameクラスを継承したGameManagerクラスを作成する。
#pragma once
#include"Game.hpp"
class GameManager : public Game {
public:
GameManager(Scene *scene);
~GameManager();
private:
Scene *mScene;
public:
virtual Scene *update() override;
virtual void draw() const override;
};
#include"GameManager.hpp"
#include"GameData.hpp"
class Play;
GameManager::GameManager(Scene *scene) :
Game(scene, new GameData()),
mScene(makeScene<Play>())
{
}
GameManager::~GameManager() {
if( mScene != nullptr ) delete mScene;
delete gameData;
}
Scene *GameManager::update() {
auto p = mScene->update();
if( p != mScene ) {
delete mScene;
auto p2 = dynamic_cast<Game*>(p);
if( p2 != nullptr ) {
mScene = p2;
return this;
}
else {
mScene = nullptr;
return p;
}
}
return this;
}
void GameManager::draw() const {
std::cout << "GameManager::draw \n";
mScene->draw();
}
template<>
Scene *Scene::makeScene<GameManager>() {
return new GameManager(this);
}
あとはGameを継承して各シーンクラスを作成する。
#include"Game.hpp"
class Play : public Game {
public:
Play(Game *game) :
Game(*game)
{
}
virtual ~Play() = default;
Scene *update() override {
return this;
}
void draw() const {
}
};
template<>
Scene *Game::makeScene<Play>() {
return new Play(this);
}
その階層の共有データにはそのままアクセスでき、前の階層の共有データにもアクセスできる。
中身を書き換えたいときはcppファイル内でヘッダーをインクルードする必要がある。
VSを使う場合
VSの初期設定ではdynamic_castを利用できないので、プロジェクトのプロパティ->C/C++->原語->ランタイム型情報を有効にする の項目を はい に変更する必要がある。
名前空間の導入
ここまで適当に命名してきたが、ほとんど同じ機能のクラスが多数存在する。
そのうちテンプレート化したいが、今回はコピペと名前空間の別名で代用する。
ついでに名前空間とフォルダを対応させて可視化する。
命名規則として今回は
・階層ごとに名前空間を作成する。
・各階層の基底クラス(前節までのGameクラス)は、BaseクラスとしてBase.hppに作成
・各階層の管理クラス(GameManager)は、ManagerクラスとしてManager.cppに作成
・共有データ群(GameData)は、CommonDataクラスとしてCommonData.hpp及びCommonData.cppに作成
コードを変更していこう。
基本的に以下のものをコピペして、各自変更というコメントを付けたところを状況に合わせて変更していけばできるはずだ。
#pragma once
namespace Scene {
class CommonData;
class Base {
protected:
//メインループ前にのみ使用するコンストラクタ
Base(CommonData *const commonData) :
commonData(commonData)
{}
//派生クラスのコンストラクタで使用するコンストラクタ
Base(const Base &) = default;
public:
//コピー禁止
Base &operator=(const Base &) = delete;
//仮想関数化したデストラクタ
virtual ~Base() = default;
protected:
//共有データ
CommonData *const commonData;
public:
virtual Base *update() = 0;
virtual void draw() const = 0;
//実体生成用関数
template<typename T>
Base *makeScene();
};
}
#include"Base.hpp"
#include"CommonData.hpp"
namespace Scene {
class CommonData;
class Manager : public Base {
public:
Manager();
Manager(const Manager &) = delete;
Manager &operator=(const Manager &) = delete;
~Manager();
private:
Base *mScene;
public:
Base *update() override;
void draw() const;
};
}
namespace Scene {
class Title; //各自変更
Manager::Manager() :
Base(new CommonData()),
mScene(Base::makeScene<Title>()) //各自変更
{
}
Manager::~Manager() {
if( mScene != nullptr ) delete mScene;
delete commonData;
}
Base *Manager::update() {
Base *p = mScene->update();
if( p != mScene ) {
delete mScene;
mScene = p;
}
return mScene;
}
void Manager::draw() const {
mScene->draw();
}
}
#pragma once
namespace Scene {
class CommonData {
public:
CommonData() {}
//...
};
}
#include"Base.hpp"
namespace Scene {
//遷移先クラス名
namespace Game { class Manager; } //各自変更
class Title : public Base { //各自変更(クラス名)
public:
Title(Base *scene) :
Base(*scene)
{
//...
}
~Title() {
//...
}
virtual Base *update() override {
//...
return makeScene<Game::Manager>();
}
virtual void draw() const override {
}
};
template<> //各自変更(クラス名)
Base *Base::makeScene<Title>() {
return new Title(this);
}
}
#pragma once
#include"../Base.hpp"
namespace Scene { namespace Game { //各自変更(名前空間名)
//一つ前の階層の名前空間
namespace nsPrev = Scene; //各自変更
class CommonData;
class Base : public nsPrev::Base {
protected:
Base(nsPrev::Base *scene, CommonData *const commonData) :
nsPrev::Base(*scene),
commonData(commonData)
{}
Base(const Base &) = default;
public:
virtual ~Base() = default;
protected:
CommonData *const commonData;
public:
virtual Scene::Base *update() override = 0;
virtual void draw() const override = 0;
//実体生成用関数
template<typename T>
Scene::Base *makeScene();
};
}}
#include"Base.hpp"
#include"./CommonData.hpp"
namespace nsThis = Scene::Game; //各自変更
namespace Scene { namespace Game { //各自変更(名前空間名)
class Manager : public Base {
public:
Manager(nsPrev::Base *scene);
~Manager();
private:
Scene::Base *mScene;
public:
virtual Scene::Base *update() override;
virtual void draw() const override;
};
//遷移先
class Play; //各自変更
Manager::Manager(nsPrev::Base *scene) :
Base(scene, new CommonData()),
mScene(makeScene<Play>()) //各自変更
{
}
Manager::~Manager() {
if( mScene != nullptr ) delete mScene;
delete commonData;
}
Scene::Base *Manager::update() {
auto p = mScene->update();
if( p != mScene ) {
delete mScene;
auto p2 = dynamic_cast<Base *>(p);
if( p2 != nullptr ) {
mScene = p2;
return this;
}
else {
mScene = nullptr;
return p;
}
}
return this;
}
void Manager::draw() const {
mScene->draw();
}
}
template<>
Scene::Base *nsThis::nsPrev::Base::makeScene<nsThis::Manager>() {
return new nsThis::Manager(this);
}
}
名前空間の別名を利用して、書き換える必要のあるところを極力減らした。
新しい階層を作りたいときはGame階層を参考にコピペして一部書き換えればできる。
VSを使う場合
名前空間とクラスの関係を可視化できるようにフォルダと対応付けをした。
VSの初期の設定では、同じ名前のcppファイルをコンパイルできないので この記事 を参考に設定を変更しておくといいだろう。
今後の課題
・テンプレート化したい。
・階層管理クラスの最初に実行するシーンを選択できるようにしたい。
・階層管理クラスを使う上で意識せずに階層間移動もmakeScene<遷移先>だけでできるようにしたい。
・遷移時のアニメーションを作りたい。(ブラックアウトなど)
追記(2020/4/20)
もうちょっとマシにしたものを上げました。
こちらもどうぞ。