C ++のエンティティ/コンポーネントシステム、型を検出してコンポーネントを構築するにはどうすればよいですか?


37

私はC ++でエンティティコンポーネントシステムに取り組んでいます。Artemisのスタイル(http://piemaster.net/2011/07/entity-component-artemis/)に従ってくださいロジックを含むシステム。このアプローチのデータ中心性を活用し、優れたコンテンツツールを構築したいと考えています。

ただし、データファイルから識別子文字列またはGUIDを取得し、それを使用してエンティティのコンポーネントを構築する方法があります。明らかに、1つの大きな解析関数があります。

Component* ParseComponentType(const std::string &typeName)
{
    if (typeName == "RenderComponent") {
        return new RenderComponent();
    }

    else if (typeName == "TransformComponent") {
        return new TransformComponent();
    }

    else {
        return NULL:
    }
}

しかし、それは本当にいです。コンポーネントを頻繁に追加および変更し、できればプロトタイプ作成のためにコンポーネントとシステムをLuaに実装できるように、何らかのScriptedComponentComponentを構築する予定です。あるクラスから継承するクラスを作成しBaseComponent、おそらくすべてを機能させるために2、3のマクロを投げて、実行時にインスタンスを生成できるようにしたいと思います。

C#とJavaでは、クラスとコンストラクターを検索するための素晴らしいリフレクションAPIを取得するため、これは非常に簡単です。しかし、私はC ++でこれを行っています。なぜなら、その言語の習熟度を高めたいからです。

それで、これはC ++でどのように達成されますか?RTTIの有効化について読んだことがありますが、ほとんどの人は、特にオブジェクトタイプのサブセットにのみ必要な状況では、それについて警戒しているようです。そこでカスタムRTTIシステムが必要な場合、どこでそれを書くことを学び始めることができますか?


1
まったく無関係なコメント:C ++に習熟したい場合は、文字列に関してCではなくC ++を使用してください。すみませんが、言わなければなりませんでした。
クリスは、

おもちゃの例であり、std :: string apiが記憶されていません。。。まだ!
michael.bartnett

@bearcdp私は答えにメジャーアップデートを投稿しました。実装は、より堅牢で効率的でなければなりません。
ポールマンタ

@PaulManta回答を更新していただきありがとうございます!それから学ぶことはたくさんあります。
michael.bartnett

回答:


36

コメント:
Artemisの実装は興味深いものです。コンポーネントを「Attributes」および「Behaviors」と呼ぶことを除いて、同様のソリューションを思い付きました。コンポーネントの種類を分離するこのアプローチは、私にとって非常にうまく機能しました。

解決策について:
コードは使いやすいですが、C ++の経験がない場合、実装を追跡するのは難しいかもしれません。そう...

希望のインターフェース

私がやったことは、すべてのコンポーネントの中央リポジトリを持つことです。各コンポーネントタイプは、特定の文字列(コンポーネント名を表す)にマップされます。これは、システムの使用方法です。

// Every time you write a new component class you have to register it.
// For that you use the `COMPONENT_REGISTER` macro.
class RenderingComponent : public Component
{
    // Bla, bla
};
COMPONENT_REGISTER(RenderingComponent, "RenderingComponent")

int main()
{
    // To then create an instance of a registered component all you have
    // to do is call the `create` function like so...
    Component* comp = component::create("RenderingComponent");

    // I found that if you have a special `create` function that returns a
    // pointer, it's best to have a corresponding `destroy` function
    // instead of using `delete` directly.
    component::destroy(comp);
}

実装

実装はそれほど悪くはありませんが、それでもかなり複雑です。テンプレートと関数ポインターの知識が必要です。

注: Joe Wreschnigはコメントでいくつかの良い点を指摘しました。主に、以前の実装が、コンパイラーがコードを最適化するのにどれだけ優れているかについてあまりに多くの仮定を立ててたかについてです。この問題は有害ではありませんでしたが、私にもバグがありました。また、前のCOMPONENT_REGISTERマクロはテンプレートでは機能しなかったことにも気付きました。

コードを変更しましたが、これらの問題はすべて修正する必要があります。このマクロはテンプレートで動作し、Joeが提起した問題は解決されています。コンパイラーが不要なコードを最適化するのがはるかに簡単になりました。

component / component.h

#ifndef COMPONENT_COMPONENT_H
#define COMPONENT_COMPONENT_H

// Standard libraries
#include <string>

// Custom libraries
#include "detail.h"


class Component
{
    // ...
};


namespace component
{
    Component* create(const std::string& name);
    void destroy(const Component* comp);
}

#define COMPONENT_REGISTER(TYPE, NAME)                                        \
    namespace component {                                                     \
    namespace detail {                                                        \
    namespace                                                                 \
    {                                                                         \
        template<class T>                                                     \
        class ComponentRegistration;                                          \
                                                                              \
        template<>                                                            \
        class ComponentRegistration<TYPE>                                     \
        {                                                                     \
            static const ::component::detail::RegistryEntry<TYPE>& reg;       \
        };                                                                    \
                                                                              \
        const ::component::detail::RegistryEntry<TYPE>&                       \
            ComponentRegistration<TYPE>::reg =                                \
                ::component::detail::RegistryEntry<TYPE>::Instance(NAME);     \
    }}}


#endif // COMPONENT_COMPONENT_H

component / detail.h

#ifndef COMPONENT_DETAIL_H
#define COMPONENT_DETAIL_H

// Standard libraries
#include <map>
#include <string>
#include <utility>

class Component;

namespace component
{
    namespace detail
    {
        typedef Component* (*CreateComponentFunc)();
        typedef std::map<std::string, CreateComponentFunc> ComponentRegistry;

        inline ComponentRegistry& getComponentRegistry()
        {
            static ComponentRegistry reg;
            return reg;
        }

        template<class T>
        Component* createComponent() {
            return new T;
        }

        template<class T>
        struct RegistryEntry
        {
          public:
            static RegistryEntry<T>& Instance(const std::string& name)
            {
                // Because I use a singleton here, even though `COMPONENT_REGISTER`
                // is expanded in multiple translation units, the constructor
                // will only be executed once. Only this cheap `Instance` function
                // (which most likely gets inlined) is executed multiple times.

                static RegistryEntry<T> inst(name);
                return inst;
            }

          private:
            RegistryEntry(const std::string& name)
            {
                ComponentRegistry& reg = getComponentRegistry();
                CreateComponentFunc func = createComponent<T>;

                std::pair<ComponentRegistry::iterator, bool> ret =
                    reg.insert(ComponentRegistry::value_type(name, func));

                if (ret.second == false) {
                    // This means there already is a component registered to
                    // this name. You should handle this error as you see fit.
                }
            }

            RegistryEntry(const RegistryEntry<T>&) = delete; // C++11 feature
            RegistryEntry& operator=(const RegistryEntry<T>&) = delete;
        };

    } // namespace detail

} // namespace component

#endif // COMPONENT_DETAIL_H

component / component.cpp

// Matching header
#include "component.h"

// Standard libraries
#include <string>

// Custom libraries
#include "detail.h"


Component* component::create(const std::string& name)
{
    detail::ComponentRegistry& reg = detail::getComponentRegistry();
    detail::ComponentRegistry::iterator it = reg.find(name);

    if (it == reg.end()) {
        // This happens when there is no component registered to this
        // name. Here I return a null pointer, but you can handle this
        // error differently if it suits you better.
        return nullptr;
    }

    detail::CreateComponentFunc func = it->second;
    return func();
}

void component::destroy(const Component* comp)
{
    delete comp;
}

Luaで拡張する

少しの作業(それほど難しくない)で、C ++またはLuaのいずれかで定義されたコンポーネントを、考えなくてもシームレスに使用できることに注意してください。


ありがとうございました!あなたは正しい、私はまだそれを完全に理解するためにC ++テンプレートの黒い芸術に十分に流fluentではない。しかし、1行のマクロはまさに私が探していたものであり、その上でテンプレートをより深く理解し始めるためにこれを使用します。
michael.bartnett

6
私はこれが基本的に正しいアプローチであることに同意しますが、次の2つの点に固執します。 .SO / DLLなどを作成しています)2. componentRegistryオブジェクトは、いわゆる「静的初期化順序の失敗」のために壊れる可能性があります。componentRegistryが最初に作成されるようにするには、ローカルの静的変数への参照を返す関数を作成し、componentRegistryを直接使用する代わりにそれを呼び出す必要があります。
ルーカス

@ルーカスああ、あなたはそれらについて完全に正しいです。それに応じてコードを変更しました。私はを使用したので、以前のコードにリークはなかったとは思いませんshared_ptrが、あなたのアドバイスはまだ良いです。
ポールマンタ

1
@Paul:わかりましたが、理論的ではありません。少なくともシンボルの可視性の漏れやリンカーの苦情を避けるために、静的にする必要があります。また、「このエラーを適切に処理する必要があります」というコメントは、代わりに「これはエラーではありません」と言う必要があります。

1
@PaulManta:関数は、ODRに「違反する」ことがあります(たとえば、テンプレートのように)。ただし、ここではインスタンスについて説明しており、それらは常にODRに従う必要があります。コンパイラは、これらのエラーが複数のTUで発生する場合(通常は不可能です)、これらのエラーを検出および報告する必要がないため、未定義の動作の領域に入ります。インターフェイス定義全体にうんちを絶対に塗らなければならない場合、少なくとも静的にすることでプログラムを明確に定義できますが、Coyoteには正しい考えがあります。

9

あなたが望むのは工場のようです。

http://en.wikipedia.org/wiki/Factory_method_pattern

できることは、さまざまなコンポーネントをファクトリに登録して、それらに対応する名前を付けてから、コンポーネントを生成するコンストラクタメソッドシグネチャへの文字列識別子のマップを用意することです。


1
だから、私はすべてのComponentクラスを知っているコードのいくつかのセクションが必要ComponentSubclass::RegisterWithFactory()です、呼び出しますよね?これをより動的かつ自動的に設定する方法はありますか?私が探しているワークフローは1です。クラスを作成し、対応するヘッダーとcppファイルのみを確認します。2.ゲームを再コンパイルします。3.レベルエディターを起動し、新しいコンポーネントクラスを使用できます。
michael.bartnett

2
それが本当に自動的に起こる方法はありません。ただし、スクリプトごとに1行のマクロ呼び出しに分割できます。ポールの答えは少しその中に入ります。
テトラッド

1

私はしばらくの間、選ばれた答えからポール・マンタのデザインを手がけ、最終的にこのより一般的で簡潔なファクトリー実装に行き着きました。この例では、すべてのファクトリオブジェクトはObject基本クラスから派生しています。

struct Object {
    virtual ~Object(){}
};

静的Factoryクラスは次のとおりです。

struct Factory {
    // the template used by the macro
    template<class ObjectType>
    struct RegisterObject {
        // passing a vector of strings allows many id's to map to the same sub-type
        RegisterObject(std::vector<std::string> names){
            for (auto name : names){
                objmap[name] = instantiate<ObjectType>;
            }
        }
    };

    // Factory method for creating objects
    static Object* createObject(const std::string& name){
        auto it = objmap.find(name);
        if (it == objmap.end()){
            return nullptr;
        } else {
            return it->second();
        }
    }

    private:
    // ensures the Factory cannot be instantiated
    Factory() = delete;

    // the map from string id's to instantiator functions
    static std::map<std::string, Object*(*)(void)> objmap;

    // templated sub-type instantiator function
    // requires that the sub-type has a parameter-less constructor
    template<class ObjectType>
    static Object* instantiate(){
        return new ObjectType();
    }
};
// pesky outside-class initialization of static member (grumble grumble)
std::map<std::string, Object*(*)(void)> Factory::objmap;

のサブタイプを登録するためのマクロObjectは次のとおりです。

#define RegisterObject(type, ...) \
namespace { \
    ::Factory::RegisterObject<type> register_object_##type({##__VA_ARGS__}); \
}

現在の使用法は次のとおりです。

struct SpecialObject : Object {
    void beSpecial(){}
};
RegisterObject(SpecialObject, "SpecialObject", "Special", "SpecObj");

...

int main(){
    Object* obj1 = Factory::createObject("SpecialObject");
    Object* obj2 = Factory::createObject("SpecObj");
    ...
    if (obj1){
        delete obj1;
    }
    if (obj2){
        delete obj2;
    }
    return 0;
}

サブタイプごとの多くの文字列IDの容量はアプリケーションで役立ちましたが、サブタイプごとに1つのIDに制限するのはかなり簡単です。

これが役に立つことを願っています!


1

オフ構築@TimStraubingerの答え、私が使用してファクトリクラスを建てC ++ 14の保存できる規格任意の数の引数で派生メンバーを。私の例は、Timとは異なり、機能ごとに1つの名前/キーのみを取ります。ティムさんと同じように、すべてのクラスが由来している格納されている基本クラス、鉱山が呼び出されているベース

Base.h

#ifndef BASE_H
#define BASE_H

class Base{
    public:
        virtual ~Base(){}
};

#endif

EX_Factory.h

#ifndef EX_COMPONENT_H
#define EX_COMPONENT_H

#include <string>
#include <map>
#include "Base.h"

struct EX_Factory{
    template<class U, typename... Args>
    static void registerC(const std::string &name){
        registry<Args...>[name] = &create<U>;
    }
    template<typename... Args>
    static Base * createObject(const std::string &key, Args... args){
        auto it = registry<Args...>.find(key);
        if(it == registry<Args...>.end()) return nullptr;
        return it->second(args...);
    }
    private:
        EX_Factory() = delete;
        template<typename... Args>
        static std::map<std::string, Base*(*)(Args...)> registry;

        template<class U, typename... Args>
        static Base* create(Args... args){
            return new U(args...);
        }
};

template<typename... Args>
std::map<std::string, Base*(*)(Args...)> EX_Factory::registry; // Static member declaration.


#endif

main.cpp

#include "EX_Factory.h"
#include <iostream>

using namespace std;

struct derived_1 : public Base{
    derived_1(int i, int j, float f){
        cout << "Derived 1:\t" << i * j + f << endl;
    }
};
struct derived_2 : public Base{
    derived_2(int i, int j){
        cout << "Derived 2:\t" << i + j << endl;
    }
};

int main(){
    EX_Factory::registerC<derived_1, int, int, float>("derived_1"); // Need to include arguments
                                                                    //  when registering classes.
    EX_Factory::registerC<derived_2, int, int>("derived_2");
    derived_1 * d1 = static_cast<derived_1*>(EX_Factory::createObject<int, int, float>("derived_1", 8, 8, 3.0));
    derived_2 * d2 = static_cast<derived_2*>(EX_Factory::createObject<int, int>("derived_2", 3, 3));
    delete d1;
    delete d2;
    return 0;
}

出力

Derived 1:  67
Derived 2:  6

これが、作業にIDコンストラクターを必要としないFactoryデザインを使用する必要がある人々に役立つことを願っています。設計が楽しかったので、工場の設計にもっと柔軟性が必要な人に役立つことを願っています。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.