C ++中的实体/组件系统,如何发现类型并构造组件?


37

我正在使用C ++构建实体组件系统,希望遵循Artemis的风格(http://piemaster.net/2011/07/entity-component-artemis/),因为组件主要是数据包,包含逻辑的系统。我希望利用这种方法的以数据为中心的优势,并构建一些不错的内容工具。

但是,我遇到的一个难题是如何从数据文件中获取一些标识符字符串或GUID,并使用该标识符或实体来构造实体的组件。显然,我可以拥有一个大型解析函数:

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

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

    else {
        return NULL:
    }
}

但这真的很丑。我打算经常添加和修改组件,并希望构建某种ScriptedComponentComponent,以便您可以在Lua中实现组件和系统以进行原型设计。我希望能够编写一个从某个类继承的BaseComponent类,也许将其扔进几个宏中以使一切正常工作,然后使该类可在运行时实例化。

在C#和Java中,这将非常简单,因为您获得了不错的反射API来查找类和构造函数。但是,我正在用C ++进行此操作,因为我想提高自己在该语言中的熟练程度。

那么用C ++如何做到这一点呢?我已经阅读了有关启用RTTI的信息,但似乎大多数人对此保持警惕,尤其是在我只需要将其用于对象类型的子集的情况下。如果在那里需要定制的RTTI系统,该从哪里开始学习编写一个系统?


1
完全无关的注释:如果您想精通C ++,那么就字符串而言,请使用C ++而不是C。对此感到抱歉,但这不得不说。
克里斯说,请恢复莫妮卡(Monica)

我听说您,这是一个玩具示例,并且没有记忆std :: string api。。。然而!
michael.bartnett

@bearcdp我已经对答案发布了重要更新。现在必须更加稳健和高效地实施。
保罗·曼塔

@PaulManta非常感谢您更新您的答案!从中可以学到很多小东西。
michael.bartnett 2012年

Answers:


36

评论:
Artemis实现很有趣。我提出了类似的解决方案,只是我将组件称为“属性”和“行为”。这种分离组件类型的方法对我来说非常有效。

关于解决方案:
该代码易于使用,但是如果您不熟悉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在评论中提出了一些要点,主要是关于我以前的实现如何对编译器在优化代码方面的出色表现做出了太多的假设。这个问题不是有害的,imo,但它也确实使我烦恼。我还注意到,前一个COMPONENT_REGISTER宏不适用于模板。

我已经更改了代码,现在所有这些问题都应该得到解决。该宏适用于模板,并且解决了Joe提出的问题:现在,编译器可以更轻松地优化不必要的代码。

组件/ 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

组件/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 ++模板的黑技术还不够流利,无法完全理解这一点。但是,单行宏正是我在寻找的东西,最重要的是,我将使用它来开始更深入地了解模板。
michael.bartnett

6
我同意这基本上是正确的方法,但有两点需要我注意:1.为什么不使用模板化函数并存储函数指针映射,而不是制作退出时会泄漏的ComponentTypeImpl实例(除非存在问题,否则不成问题)您正在制作.SO / DLL或其他内容。)2. componentRegistry对象可能由于所谓的“静态初始化顺序失败”而中断。为了确保首先创建componentRegistry,您需要创建一个函数,该函数返回对局部静态变量的引用并调用该函数,而不是直接使用componentRegistry。
卢卡斯

@Lucas Ah,您完全正确。我相应地更改了代码。自从我使用以来,我认为之前的代码中没有任何泄漏shared_ptr,但是您的建议仍然不错。
保罗·曼塔

1
@Paul:好的,但这不是理论上的,您至少应该将其设置为静态,以避免可能的符号可见性泄漏/链接器投诉。另外,您的注释“您应该按自己认为的方式处理此错误”应该改为“这不是错误”。

1
@PaulManta:有时允许使用函数类型来“违反” ODR(例如,如您所说的模板)。但是,这里我们谈论的是实例,并且这些实例必须始终遵循ODR。如果错误发生在多个TU中(通常是不可能的),则不需要编译器检测并报告这些错误,因此您可以进入未定义行为的领域。如果您绝对必须在整个接口定义上涂抹poo,则使其静态化至少可以使程序保持良好的定义-但是Coyote的想法正确。

9

看来您想要的是一家工厂。

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

您可以做的是让各个组件在工厂注册它们对应的名称,然后将字符串标识符映射到构造函数方法签名以生成组件。


1
因此,我仍然需要一些代码来了解我所有的Component类,调用ComponentSubclass::RegisterWithFactory(),对吧?有没有办法设置它,使其更动态,更自动地进行?我正在寻找的工作流程是1.编写一个类,仅查看相应的标头和cpp文件。2.重新编译游戏3.启动关卡编辑器并使用新的组件类。
michael.bartnett

2
真的没有办法自动发生。不过,您可以按脚本将其细分为1行宏调用。保罗的答案有点。
Tetrad

1

我从选定的答案中研究了Paul Manta的设计一段时间,最终来到了下面这个更通用,更简洁的工厂实现中,我愿意与以后遇到这个问题的任何人分享。在此示例中,每个工厂对象都派生自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的功能非常有用,但是对每个子类型一个ID的限制相当简单。

希望这对您有所帮助!


1

基于@TimStraubinger的答案,我使用C ++ 14标准构建了一个工厂类,该工厂类可以存储带有任意数量参数的派生成员。我的示例与Tim的示例不同,每个函数只使用一个名称/键。与Tim一样,每个要存储的类都派生自Base类,我的类称为Base

基数

#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

我希望这可以帮助需要使用Factory设计的人员,该设计不需要身份构造函数即可工作。设计很有趣,所以我希望它可以帮助需要更大灵活性的人们进行Factory设计。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.