SUPER MAOU LANDで使っているカスタムRTTI

SUPER MAOU LANDで使っているカスタムRTTIの自分用メモ。


(SUPER MAOU LAND作ってます)
SUPER MAOU LAND



もともとはUnityっぽくGetComponent();などとやりたいために作り出したもの。
そのためにカスタムRTTIが必要になったので、その実装内容をメモしておく。


カスタムRTTIとかの前にC++でどうやってGetComponent();風な実装しようか考える。
するとインスタンスに対してHoge型かどうかを尋ねる関数は必要だと思う。
なのでまずこんな感じの関数を作ってみる。

//キャスト可能ならtrue
template<class T>
bool IsA(Component *component){
    return (component->getRTTI() == T::TYPE());
}

テンプレートを使うことによって、インスタンス(component)の型情報と、任意の型(T)の型情報の比較をしてるだけ。
すると、キャストする関数はこんな感じに書ける。

//キャスト(変換できないならnullptr)
template<class T>
T* Cast(Component *component){
    if( IsA<T>(component) ){
        return static_cast<T*>(component);
    }
    return nullptr;
}

先ほど定義したIsA()を呼び出して、trueならstatic_castするだけ。
これをうまく使えばGetComponent();風な実装ができそうな気がしますね?


さて、IsA()の実装内容からして、オレオレComponentにはgetRTTI()とTYPE()の実装が必要になった。
つまり、こんな感じの実装にしとく必要がある

class ComponentBase{
public:
    using RTTI = const std::string *;       //カスタムRTTIの実体は文字列のポインタ
    virtual RTTI getRTTI() const = 0;       //実装は子供に任せます
};

//コンポーネントその1
class HogeComponent : public ComponentBase{
public:
    RTTI getRTTI() const override{
        return TYPE();                      //実装内容はTYPE()に任せます
    }
    static RTTI TYPE(){                     //s_typeが型情報になります。他の型情報とかぶらない様に注意してください
        static const std::string s_type = "HogeComponent";
        return &s_type;
    }
};

//コンポーネントその2
class FugaComponent : public ComponentBase{
public:
    RTTI getRTTI() const override{
        return TYPE();                      //実装内容はTYPE()に任せます
    }
    static RTTI TYPE(){                     //s_typeが型情報になります。他の型情報とかぶらない様に注意してください
        static const std::string s_type = "FugaComponent";
        return &s_type;
    }
};


getRTTI()はインスタンスに対して呼び出されるので、仮想関数として定義しときます。
TYPE()は、T::TYPE()の様に呼び出されるので、staticメンバ関数として定義します。


さて、ここでもう一度IsA()の実装を振り返って、実装内容を確認します。
例えば、IsA(component);の呼び出しがどうなるかというと、こんな感じになります。

bool IsA(Component *component){
    return (component->getRTTI() == HogeComponent::TYPE());
}

インスタンスが持つ型情報と、型が持つ型情報が比較されています。
componentがHogeComponentであれば、getRTTI()とTYPE()が同じものを返すので、trueが返ります。
また、componentがFugaComponentであれば、getRTTI()とTYPE()が違うものを返すので、falseが返ります。


実際には、getRTTI()とTYPE()の定義がメンドイので、マクロにして使っています。

#define IMPLEMENT_COMPONENT_RTTI(className) \
    ComponentBase::RTTI getID() const override{             \
        return TYPE();                                      \
    }                                                       \
    static ComponentBase::RTTI TYPE(){                      \
        static const std::string s_type = #className;       \
        return &s_type;                                     \
    }                                                       \

すると、実装はこんな感じになります。

class ComponentBase{
public:
    using RTTI = const std::string *;       //カスタムRTTIの実体は文字列のポインタ
    virtual RTTI getRTTI() const = 0;       //実装は子供に任せます
};

//コンポーネントその1
class HogeComponent : public ComponentBase{
    //ここに好きなように色々書く
public:
    IMPLEMENT_COMPONENT_RTTI(HogeComponent);
};

//コンポーネントその2
class FugaComponent : public ComponentBase{
    //ここに好きなように色々書く
public:
    IMPLEMENT_COMPONENT_RTTI(FugaComponent);
};


わぁ、かんたん!
この仕組みはゲーム中のイベントとかにも持たせておくと、イベント毎の処理を捌きやすくなって色々と捗る。
マクロは好きくないけど他にいいやりかたが思いつかなかったのでこれでいいや、って感じ。
かしこいカスタムRTTIの実装だと継承関係とか辿ったりとかやるんだろうけど個人製作のゲームでそこまでいらんだろーって事で、ひとまずこれで。
つーかそもそもdynamic_cast使っちゃえばこんなもん実装する必要はないのだけれど・・・。


あとはこの仕組みを使ってGetComponent();できるようにするだけなんだが、その実装メモについては後で書くことにする。
後で書く。・・・といいなぁ。