テンプレートを用いたシーン遷移


はじめに

この記事は前回の記事の続きですが、読まなくても理解できる内容にしたつもりです。

やりたいこと

各シーンでクラスを作ってそのupdate関数でChangeScene関数を呼ぶだけでシーンを遷移。
メインループではSceneManagerのupdateとdrawを呼ぶ。
関数テンプレートの明示的インスタンス化とextern templateを利用して遷移先のクラスの宣言のみでシーンを遷移。
共有データクラスをテンプレート引数に指定して管理させる。

extern template周り

f.hpp
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の定義を知っている別ファイルで明示的インスタンス化を行う必要がある。

Hoge.cpp
#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() {
            //シーンを遷移する処理
        }

    };
}

共有データのポインタコピー

共有データのポインタは各シーンで頻繁にアクセスすることが予想されるためメンバにコピーして持っておきたい。
今後ほかにもコピーしたいものが出てくるかもしれないので、初期化データクラスInitDataBaseTのインナークラスとして定義してその参照を渡すことで解決する。

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以外はmCommonDatanullptrを代入しておけば解決する。
これは関数テンプレートの明示的特殊化で解決できる。

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でmScenedeleteしてしまうと、メンバ関数の実行中にインスタンスが破棄されてしまう。
(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::managermNextprivateメンバなので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>::Managerusing して別名を付けておくといいだろう。


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 関数の中身は変えずにそのまま使えるはずだ。

完成形

Scene/Template.hpp
#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;
}
Scene/CommonData.hpp
#pragma once

namespace Scene {
    class CommonData {
    public:
        CommonData();


    };
}
Scene/Base.hpp
#pragma once
#include"Template.hpp"
#include"CommonData.hpp"

namespace Scene {
    using Base = BaseT<CommonData>;
}
Title.cpp
#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宣言でごまかした部分の見直し
・テンプレートを使った静的な多態を勉強してみたい