はじめに
この記事は前回の記事の続きですが、読まなくても理解できる内容にしたつもりです。
やりたいこと
各シーンでクラスを作ってそのupdate関数でChangeScene関数を呼ぶだけでシーンを遷移。
メインループではSceneManagerのupdateとdrawを呼ぶ。
関数テンプレートの明示的インスタンス化とextern templateを利用して遷移先のクラスの宣言のみでシーンを遷移。
共有データクラスをテンプレート引数に指定して管理させる。
extern template周り
template<typename T>
void f() {
//型Tの定義が必要な処理
}
という関数テンプレートに対し、
class Hoge;
extern template void f<Hoge>();
または
extern template void f<class Hoge>();
とすることで、class Hoge
の定義なしに関数テンプレートを呼び出すことができる。
extern templateは暗黙のインスタンス化を禁止しているだけなので、class Hoge
の定義を知っている別ファイルで明示的インスタンス化を行う必要がある。
#include"f.hpp"
class Hoge {
};
template void f<Hoge>();
この機能を用いて、シーン遷移を次のシーンのクラスの宣言のみで行う。
基底クラス
とりあえず決まっている情報で基底クラスを作成する。
今回は仮想関数を利用してポリモーフィズムを実現する。
後の都合上、基底クラスは前回の記事とは異なりScene
名前空間を導入してBaseT
という名前を使用する。
(説明の都合上今後名前空間を省いて書くことがある)
namespace Scene {
template<typename CommonData>
class BaseT {
protected:
BaseT() :
mCommonData(nullptr)//仮
{
}
public:
virtual ~BaseT() = default;
//コピー禁止
BaseT(BaseT &) = delete;
BaseT &operator(BaseT &) = delete;
virtual void update() = 0;
virtual void draw() const = 0;
private:
//共有データ
CommonData *mCommonData;
protected:
auto getData() {
return mCommonData;
}
protected:
template<typename T>
void ChangeScene() {
//シーンを遷移する処理
}
};
}
共有データのポインタコピー
共有データのポインタは各シーンで頻繁にアクセスすることが予想されるためメンバにコピーして持っておきたい。
今後ほかにもコピーしたいものが出てくるかもしれないので、初期化データクラスInitData
をBaseT
のインナークラスとして定義してその参照を渡すことで解決する。
template<typename CommonData>
class BaseT {
public:
class InitData {
public:
CommonData *data;
};
protected:
BaseT(const InitData &init) :
mCommonData(init.data)
{
}
//...
};
SceneManagerクラス
シーン全体を管理するクラスを作成する。
共有データの管理(new
, delete
)と、各シーンの管理を行う。
後の都合上BaseT
のインナークラスとして定義する。
template<typename CommonData>
class BaseT {
//...
public:
class Manager {
public:
Manager() :
mCommonData(new CommonData),
mScene(nullptr)
{
}
~Manager() {
if( mCommonData != nullptr ) mCommonData;
if( mScene != nullptr ) delete mScene;
}
void update() {
mScene->update();
//仮
}
void draw() const {
mScene->draw();
}
private:
CommonData *mCommonData;
BaseT *mScene;
};
//...
};
共有データがない場合の対処
共有データなしで作りたい場合も当然存在する。
BaseT
のテンプレート引数にvoid
を渡すことで共有データなしの状態を作りたい。
template<typename CommonData = void> //voidをデフォルトに
class BaseT {
};
new CommonData
以外はmCommonData
にnullptr
を代入しておけば解決する。
これは関数テンプレートの明示的特殊化で解決できる。
template<typename CommonData>
class BaseT {
//...
public:
class Manager {
private:
template<typename T>
CommonData *mMakeData() {
return new T;
}
template<>
CommonData *mMakeData() {
return nullptr;
}
public:
Manager() :
mCommonData(mMakeData<CommonData>()),
mScene(nullptr)
{
}
//...
};
//...
};
静的メンバの追加
ChangeScene
関数では遷移先のクラスのインスタンスを作成し、Manager
に渡す。
共有データのポインタをコピーするにはManager
のインスタンスにアクセスする必要がある。
Manager
の静的メンバに自身のポインタを保存する変数を追加して解決する。
class Manager {
//...
private:
static Manager *manager;
};
template<typename CommonData>
typename BaseT<CommonData>::Manager *BaseT<CommonData>::Manager::manager = nullptr;
これにコンストラクタでthis
ポインタを代入し、デストラクタでnullptr
に戻す。
class Manager {
Manager() :
mCommonData(mMakeData<CommonData>()),
mScene(nullptr)
{
if(manager != nullptr) throw;
manager = this;
}
~Manager() {
manager = nullptr;
}
};
Manager::update
mScene->update()
の中でChangeScene
関数が呼ばれる。
ChangeSceneでmScene
をdelete
してしまうと、メンバ関数の実行中にインスタンスが破棄されてしまう。
(C++の構造上動いてしまうが、書き方によっては内容が保証されないので避けるべき)
そのため、Manager
に何かしらの方法で遷移することを伝えなければならない。
Manager
のメンバにBaseT *mNext
を追加して次のシーンのポインタが入っていたら遷移を実行させるようにする。
class Manager {
public:
Manager() :
mCommonData(new CommonData),
mScene(nullptr),
mNext(nullptr)
{
if( manager != nullptr ) throw;
manager = this;
}
~Manager() {
if( mCommonData != nullptr ) mCommonData;
if( mScene != nullptr ) delete mScene;
if( mNext != nullptr ) delete mNext;
manager = nullptr;
}
void update() {
mScene->update();
if( mNext != nullptr ) {
delete mScene;
mScene = mNext;
mNext = nullptr;
}
}
//...
};
ChangeScene関数
ChangeScene
関数では、遷移先のシーンのインスタンスを作成し、Manager::manager::mNext
に代入したい。
Manager::manager
やmNext
はprivate
メンバなのでManager
からBaseT
をフレンド宣言して解決する。
(ここは無理やり通したので目をつぶってほしい)
class Manager {
//...
template<typename T>
friend BaseT;
};
後の都合上、Manager
のメンバにシーンをセットする関数mSetScene
と、初期化用データを作成するmGetInit
関数を作成する。
class Manager {
//...
private:
static void mSetScene(BaseT *p) {
if( mNext != nullptr ) delete mNext;
mNext = p;
}
static InitData mGetInit() {
return InitData(mCommonData);
}
//...
};
ChangeScene
関数はこれらの関数を用いてmNext
にシーンクラスのインスタンスを代入する。
template<typename T>
void ChangeScene() {
T::Manager::mSetScene(new T(T::Manager::mGetInit()));
}
最初のシーンをセット
最初のシーンをManager
にセットするメンバ関数を作成する。
各シーンクラスの定義を必要とする処理をChangeScene
関数にまとめたいので、set
関数内ではChangeScene
を呼ぶだけにする。
class Manager {
public:
template<typename T>
void set() {
ChageScene<T>();
}
};
mSetScene
関数で、mScene == nullptr
の場合、mNext
ではなくmScene
に代入させればいい。
static void mSetScene(BaseT *p) {
if( mNext != nullptr ) delete mNext;
if( mScene == nullptr ) mScene = p;
else mNext = p;
}
あとは、メインループ前でBaseT<CommonData>::Manager
のインスタンスを作成してupdate
関数とdraw
関数を呼べばひとまず完成だ。
扱いやすくするため、BaseT<CommonData>::Manager
をusing
して別名を付けておくといいだろう。
namespace Scene {
//...
template<typename CommonData = void>
using Manager = BaseT<CommonData>::Manager;
}
namespace Scene {
extern template void BaseT<>::ChangeScene<class Title>();
}
int main() {
Scene::Manager<> manager;
manager.set<Scene::Title>();
while( true ) {
//メインループ
manager.update();
manager.draw();
//...
}
}
階層化
共有データクラスは一つしか登録できないが、一部のシーンクラス内でのみ共有したいデータが出てくることがある。
具体的には、ゲーム画面で敵の挙動変化をシーンクラスの変更で行うと、ゲームの中で使用されるものの多くはタイトル画面では必要ないのがわかる。
ゲーム画面周りのクラスの共有データを登録できるように、シーンの階層化を実装する。
アイデア
階層ごとに基底クラスを作成し、それを継承してシーンを
階層を管理するクラスを、一つ前の階層の基底クラスをpublic継承して作成する。
一つ前の階層管理クラスにその階層の管理クラスを登録をセットする。
具体的な遷移の内容を書くChangeScene
関数は遷移先の型情報しかないので、遷移先の基底クラスの静的メンバ関数を用いて遷移を実行する。
各階層用の基底クラス
どの階層にいても前の階層の共有データが必要になる場面が存在しうるので前の階層の基底クラスを継承する形で実装する。
また、BaseT
のテンプレート引数を増やしてデフォルトでvoid
を指定することでもとの基底クラスをそのまま用いる。
template<typename CommonData = void, typename BellowBase = void>
class BaseT;
template<typename CommonData>
class BaseT<CommonData> {
//...
};
template<typename CommonData, typename BellowBase>
class BaseT : public BellowBase {
public:
class InitData {
public:
InitData(CommonData *data, const typename BellowBase::InitData &bellowInit) :
data(data),
bellowInit(bellowInit)
{}
CommonData *data;
typename BellowBase::InitData bellowInit;
};
protected:
BaseT(const InitData &init) :
BellowBase(init.bellowInit),
mCommonData(init.data)
{
}
public:
virtual ~BaseT() = default;
virtual void update() override = 0;
virtual void draw() const override = 0;
private:
CommonData *mCommonData;
protected:
auto getData() {
return mCommonData;
}
template<typename T, typename U>
friend class BaseT;
};
階層管理クラス
共有データのポインタ管理(new
, delete
)、階層のシーンクラスの管理を行う。
このクラスは外から用いることがないのでprivateにする。
template<typename CommonData, typename BellowBase>
class BaseT : public BellowBase {
//...
private:
class Manager : public BellowBase {
public:
Manager(const typename BellowBase::InitData &init) :
BellowBase(init),
mCommonData(new CommonData),
mScene(nullptr),
mNext(nullptr),
mBellowInit(init)
{
if( manager != nullptr ) throw;
manager = this;
}
virtual ~Manager() {
if( mCommonData != nullptr ) delete mCommonData;
if( mScene != nullptr ) delete mScene;
if( mNext != nullptr ) delete mNext;
manager = nullptr;
}
virtual void update() override {
mScene->update();
if( mNext != nullptr ) {
delete mScene;
mScene = mNext;
mNext = nullptr;
}
}
virtual void draw() const override {
mScene->draw();
}
private:
BaseT *mScene;
BaseT *mNext;
CommonData *mCommonData;
typename BellowBase::InitData mBellowInit;
static Manager *manager;
static void mSetScene(BaseT *p) {
if( manager->mNext != nullptr ) delete manager->mNext;
if( manager->mScene == nullptr ) manager->mScene = p;
else manager->mNext = p;
}
static InitData mGetInit() {
if(manager == nullptr) BellowBase::Manager::mSetScene(new Manager(BellowBase::Manager::mGetInit()));
return InitData(manager->mCommonData, manager->mBellowInit);
}
template<typename T, typename U>
friend class BaseT;
};
//...
};
ChangeScene
関数の中身は変えずにそのまま使えるはずだ。
完成形
#pragma once
namespace Scene {
template<typename CommonData = void, typename BellowBase = void>
class BaseT;
template<typename CommonData>
class BaseT<CommonData> {
public:
class InitData {
public:
InitData(CommonData *data) :
data(data)
{
}
CommonData *data;
};
protected:
BaseT(const InitData &init) :
mCommonData(init.data)
{
}
public:
virtual ~BaseT() = default;
//コピー禁止
BaseT(BaseT &) = delete;
BaseT &operator=(BaseT &) = delete;
virtual void update() = 0;
virtual void draw() const = 0;
private:
CommonData *mCommonData;
protected:
auto getData() const {
return mCommonData;
}
public:
class Manager {
private:
template<typename T>
CommonData *mMakeData() {
return new CommonData;
}
template<>
CommonData *mMakeData<void>() {
return nullptr;
}
public:
Manager() :
mScene(nullptr),
mNext(nullptr),
mCommonData(mMakeData<CommonData>())
{
if( manager != nullptr ) throw;
manager = this;
}
~Manager() {
if( mScene != nullptr ) delete mScene;
if( mNext != nullptr ) delete mNext;
if( mCommonData != nullptr ) delete mCommonData;
manager = nullptr;
}
void update() {
mScene->update();
if( mNext != nullptr ) {
delete mScene;
mScene = mNext;
mNext = nullptr;
}
}
void draw() {
mScene->draw();
}
template<typename T>
void set() {
BaseT::ChangeScene<T>();
}
private:
BaseT *mScene;
BaseT *mNext;
CommonData *mCommonData;
static void mSetScene(BaseT *p) {
if( manager->mNext != nullptr ) delete manager->mNext;
if( manager->mScene == nullptr ) manager->mScene = p;
else manager->mNext = p;
}
static InitData mGetInit() {
return InitData(manager->mCommonData);
}
static Manager *manager;
template<typename T, typename U>
friend class BaseT;
};
private:
public:
template<typename T>
static void ChangeScene() {
T::Manager::mSetScene(new T(T::Manager::mGetInit()));
}
};
template<typename CommonData>
typename BaseT<CommonData>::Manager *BaseT<CommonData>::Manager::manager = nullptr;
////////////////////////////////////////////////////////////////////////////////
template<typename CommonData, typename BellowBase>
class BaseT : public BellowBase {
public:
class InitData {
public:
InitData(CommonData *data, const typename BellowBase::InitData &bellowInit) :
data(data),
bellowInit(bellowInit)
{}
CommonData *data;
typename BellowBase::InitData bellowInit;
};
protected:
BaseT(const InitData &init) :
BellowBase(init.bellowInit),
mCommonData(init.data)
{
}
public:
virtual ~BaseT() = default;
virtual void update() override = 0;
virtual void draw() const override = 0;
private:
CommonData *mCommonData;
protected:
auto getData() {
return mCommonData;
}
private:
class Manager : public BellowBase {
public:
Manager(const typename BellowBase::InitData &init) :
BellowBase(init),
mCommonData(new CommonData),
mScene(nullptr),
mNext(nullptr),
mBellowInit(init)
{
if( manager != nullptr ) throw;
manager = this;
}
virtual ~Manager() {
if( mCommonData != nullptr ) delete mCommonData;
if( mScene != nullptr ) delete mScene;
if( mNext != nullptr ) delete mNext;
manager = nullptr;
}
virtual void update() override {
mScene->update();
if( mNext != nullptr ) {
delete mScene;
mScene = mNext;
mNext = nullptr;
}
}
virtual void draw() const override {
mScene->draw();
}
private:
BaseT *mScene;
BaseT *mNext;
CommonData *mCommonData;
typename BellowBase::InitData mBellowInit;
static Manager *manager;
static void mSetScene(BaseT *p) {
if( manager->mNext != nullptr ) delete manager->mNext;
if( manager->mScene == nullptr ) manager->mScene = p;
else manager->mNext = p;
}
static InitData mGetInit() {
if(manager == nullptr) BellowBase::Manager::mSetScene(new Manager(BellowBase::Manager::mGetInit()));
return InitData(manager->mCommonData, manager->mBellowInit);
}
template<typename T, typename U>
friend class BaseT;
};
template<typename T, typename U>
friend class BaseT;
};
template<typename CommonData, typename BellowBase>
typename BaseT<CommonData, BellowBase>::Manager *BaseT<CommonData, BellowBase>::Manager::manager;
////////////////////////////////////////////////////////////////////////////////
template<typename CommonData = void>
using Manager = typename BaseT<CommonData>::Manager;
}
#pragma once
namespace Scene {
class CommonData {
public:
CommonData();
};
}
#pragma once
#include"Template.hpp"
#include"CommonData.hpp"
namespace Scene {
using Base = BaseT<CommonData>;
}
#include"Base.hpp"
namespace Scene {
class Title : public Base {
public:
Title(const InitData &init) :
Base(init),
a(0)
{}
virtual ~Title() = default;
virtual void update() override;
virtual void draw() const override;
int a;
};
//遷移先を宣言
extern template void Base::ChangeScene<class Select>();
void Title::update() {
if( a == 120 ) ChangeScene<Select>();
}
void Title::draw() const {
}
//シーンを登録
template void Base::ChangeScene<Title>();
}
今後の課題
・遷移クラス(フェードアウトなど)の作成
・遷移クラスを使う人が作成しやすいテンプレートの作成
・遷移間のみの共有データクラスを通じない値の共有
・friend宣言でごまかした部分の見直し
・テンプレートを使った静的な多態を勉強してみたい