Take the 2-minute tour ×
Game Development Stack Exchange is a question and answer site for professional and independent game developers. It's 100% free, no registration required.

I'm working on an entity component system in C++ that I hope to follow the style of Artemis (http://piemaster.net/2011/07/entity-component-artemis/) in that components are mostly data bags and it's the Systems that contain the logic. I'm hoping to take advantage of the data-centric-ness of this approach and build some nice content tools.

However, one hump I'm coming across is how to take some identifier string or GUID from a data file and use that to construct component for an Entity. Obviously I could just have one big parse function:

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

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

    else {
        return NULL:
    }
}

But that's really ugly. I intend to be adding and modifying components frequently, and hopefully building some sort of ScriptedComponentComponent, such that you could implement a component and system in Lua for the purposes of prototyping. I'd like to be able to write a class inheriting from some BaseComponent class, maybe toss in a couple of macros to make everything work, and then have the class available for instantiation at runtime.

In C# and Java this would be pretty straightforward, since you get nice reflection APIs to look up classes and constructors. But, I'm doing this in C++ because I want to increase my proficiency in that language.

So How is this accomplished in C++? I've read about enabling RTTI, but it seems most people are wary about that, especially in a situation where I only need it for a subset of object types. If a custom RTTI system is what I need there, where can I go to start learning to write one?

share|improve this question
1  
Quite unrelated comment: If you want to get proficient in C++, then use C++ and not C, regarding strings. Sorry for that, but it had to be said. –  Christian Rau Sep 26 '11 at 21:33
    
I hear you, it was a toy example and I don't have the std::string api memorized . . . yet! –  michael.bartnett Sep 26 '11 at 21:44
    
@bearcdp I've posted a major update on my answer. The implementation is now must more robust and efficient. –  Paul Manta Jan 2 '12 at 17:11
    
@PaulManta Thanks so much for updating your answer! There's a lot of little things to learn from it. –  michael.bartnett Jan 3 '12 at 10:27
add comment

2 Answers

up vote 31 down vote accepted

A comment:
The Artemis implementation is interesting. I came up with a similar solution, except I called my components "Attributes" and "Behaviors". This approach of separating types of components has worked very nicely for me.

Regarding the solution:
The code is easy to use, but the implementation might be hard to follow if you're not experienced with C++. So...

The desired interface

What I did is to have a central repository of all components. Each component type is mapped to a certain string (which represents the component name). This is how you use the system:

// 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);
}

The implementation

The implementation is not that bad, but it's still pretty complex; it requires some knowledge of templates and function pointers.

Note: Joe Wreschnig has made some good points in the comments, mainly on how my previous implementation made too many assumptions about how good the compiler is at optimizing code; the issue was not detrimental, imo, but it did bug me as well. I also noticed that the former COMPONENT_REGISTER macro did not work with templates.

I've changed the code and now all of those problems should be fixed. The macro works with templates and the issues that Joe raised have been addressed: now it's much easier for compilers to optimize away unnecessary code.

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;
}

Extending with Lua

I should note that with a bit of work (it's not very hard), this can be used to seamlessly work with components defined in either C++ or Lua, without ever having to think about it.

share|improve this answer
    
Thank you! You're right, I'm not yet fluent enough in the black arts of C++ templates to totally understand that. But, the one-line macro is exactly what I was looking for, and on top of that I'll use this to begin to more deeply understand templates. –  michael.bartnett Sep 27 '11 at 4:37
5  
I agree that this is basically the right approach but two things that stick out to me: 1. Why not just use a templated function and store a map of function pointers instead of making ComponentTypeImpl instances that will leak on exit (Not really a problem unless you are making a .SO/DLL or something though) 2. The componentRegistry object could break due to the so-called "static initialization order fiasco". To ensure componentRegistry is made first you need to make a function that returns a reference to a local static variable and call that instead of using componentRegistry directly. –  Lucas Sep 27 '11 at 8:43
    
@Lucas Ah, you're totally right about those. I changed the code accordingly. I don't think there were any leaks in the previous code though, since I used shared_ptr, but your advice is still good. –  Paul Manta Sep 27 '11 at 9:29
1  
@Paul: Okay, but it's not theoretical, you should at least make it static to avoid possible symbol visibility leakage / linker complaints. Also your comment "You should handle this error as you see fit" should instead say "This is not an error". –  user744 Oct 11 '11 at 20:13
1  
@PaulManta: Functions and types are sometimes allowed to "violate" the ODR (e.g. as you say, templates). However here we're talking about instances and those always must follow the ODR. Compilers are not required to detect and report these errors if they occur in multiple TUs (it's generally impossible) and so you enter the realm of undefined behavior. If you absolutely must smear poo all over your interface definition, making it static at least keeps the program well-defined - but Coyote has the right idea. –  user744 Dec 14 '11 at 13:57
show 26 more comments

It seems like what you want is a factory.

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

What you can do is have your various components register with the factory what name they correspond to, and then you have some map of string identifier to constructor method signature to generate your components.

share|improve this answer
1  
So I'd still need to have some section of code that is aware of all of my Component classes, calling ComponentSubclass::RegisterWithFactory(), right? Is there a way to set this up do it more dynamically and automagically? The workflow I'm looking for is 1. Write a class, looking at only the corresonding header and cpp file 2. Re-compile game 3. Start level editor and new component class is available for use. –  michael.bartnett Sep 26 '11 at 21:50
2  
There's really no way for it to happen automagically. You can break it down to a 1 line macro call on a per script basis, though. Paul's answer goes into that a bit. –  Tetrad Sep 26 '11 at 23:40
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.