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've been getting interested in the Entity/Component style of game programming, and I've come up with a design in C++ which I'd like a critique of.

I decided to go with a fairly pure Entity system, where entities are simply an ID number. Components are stored in a series of vectors - one for each Component type.

However, I didn't want to have to add boilerplate code for every new Component type I added to the game. Nor did I want to use macros to do this, which frankly scare me. So I've come up with a system based on templates and type hinting. But there are some potential issues I'd like to check before I spend ages writing this (I'm a slow coder!)

All Components derive from a Component base class. This base class has a protected constructor, that takes a string parameter. When you write a new derived Component class, you must initialise the base with the name of your new class in a string. When you first instantiate a new DerivedComponent, it adds the string to a static hashmap inside Component mapped to a unique integer id. When you subsequently instantiate more Components of the same type, no action is taken. The result (I think) should be a static hashmap with the name of each class derived from Component that you instantiate at least once, mapped to a unique id, which can by obtained with the static method Component::getTypeId ("DerivedComponent"). Phew.

The next important part is TypedComponentList<typename PropertyType>. This is basically just a wrapper to an std::vector<typename PropertyType> with some useful methods. It also contains a hashmap of entity ID numbers to slots in the array so we can find Components by their entity owner. Crucially TypedComponentList<> is derived from the non-template class ComponentList.

This allows me to maintain a list of pointers to ComponentList in my main ComponentManager, which actually point to TypedComponentLists with different template parameters (sneaky).

The Component manager has template functions such as:

template <typename ComponentType>
void addProperty (ComponentType& component, int componentTypeId, int entityId)

and:

template <typename ComponentType>
TypedComponentList<ComponentType>* getComponentList (int componentTypeId)

which deal with casting from ComponentList to the correct TypedComponentList for you.

So to get a list of a particular type of Component you call:

TypedComponentList<MyComponent>* list =  componentManager.getComponentList<MyComponent> (Component::getTypeId("MyComponent"));

Which I'll admit looks pretty ugly.

Bad points of the design:

  • If a user of the code writes a new Component class but supplies the wrong string to the base constructor, the whole system will fail.
  • Each time a new Component is instantiated, we must check a hashed string to see if that component type has bee instantiated before.
  • Will probably generate a lot of assembly because of the extensive use of templates. I don't know how well the compiler will be able to minimise this.
  • You could consider the whole system a bit complex - perhaps premature optimisation? But I want to use this code again and again, so I want it to be performant.

Good points of the design:

  • Components are stored in typed vectors but they can also be found by using their entity owner id as a hash. This means we can iterate them fast, and minimise cache misses, but also skip straight to the component we need if necessary.
  • We can freely add Components of different types to the system without having to add and manage new Component vectors by hand.

What do you think? Do the good points outweigh the bad?

share|improve this question
    
Have a look at bitbucket.org/stalei/artemiscpp/src –  andrew Jun 30 '13 at 13:39

1 Answer 1

up vote 2 down vote accepted

I think it is a good first implementation but may I point out some things.

Component::getTypeId ("DerivedComponent")

The above piece of code worries me for a few reasons.

  1. It is prone to bugs and even if we assume you never make any obvious mistakes like naming it the same as another component type, it is hell for maintainability. What I did in my system was have a static function which returns the typdef

    typedef std::size_t ComponentType;
    
  2. Performance. It becomes a nightmare to think about how you can maximise performance, even with your string hashing scheme. Suddenly you will have to pass a string everywhere you want to get a component type, which can easily end up taking much of your frame time, even utilising const&.

Now, this may seem like a 'hack' to you but I have many dozens of components and I can attest to it working well:

static const ComponentType GetType() 
{ 
    static char uniqueVariable = 0; 
    return reinterpret_cast<ComponentType>(&uniqueVariable); 
}

It should be clear that the type will be unique (thereabouts) for each type. Strictly speaking, I have at times wondered whether some edge case of memory allocation could break the system but I haven't had any problems with it so far.

Even further, I know there is a big concerted effort against the use of macros, but any experienced programmer worth their salt will tell you that in some cases they are the lesser evil. I think that is the case here. I wrapped the above method inside of a macro so that I may simply define classes.

#define COMPONENT_TYPE_INFORMATION static const ComponentType GetType() { static char uniqueVariable = 0; return reinterpret_cast<ComponentType>(&uniqueVariable); }

If you're worried about macros then consider reading this in order to protect yourself as much as possible from bugs. The macro shown above is just for illustration purposes.

Here is an example of a component and usage:

class Component
{
public:
    virtual ComponentType GetType() = NULL;
    //etc etc 
}

class DerivedComponent : public Component
{
public:
    DerivedComponent(/*pass some arguments here perhaps*/) { }

    void MyMethod() { } //do something

    int MyAccessor() { return 1; }

    COMPONENT_TYPE_INFORMATION 
private:
};

//ComponentList stores a list of pointers to Component for the inheritance goodness

template<typename T>
bool DoesComponentMatchExist(ComponentList list) 
{ 
    for(std::size_t i = 0; i < list.size(); i++)
    {
        if(list[i]->GetType() == T::GetType())  
            return true;
    }

    return false;
}

Note that T::GetType() will conveniently return a compiler error if it does not implement GetType as specified by the pure virtual component base class.

Regarding the rest of your post, while I solved the problem with pools of memory, your approach seems fine to me, though passing around a component manager constantly may end up being a hassle. It is up to you though, there are advantages and disadvantages to doing it that way.

One thing I would mention, though, is you may want to have lists of components within entities seperate from others, rather than having a single super list. Behind the scenes, you can still store the components in one memory pool, but you should probably think about supporting it. Passing the list of the parent object into the constructor of each component is really quite powerful. You could do something like:

MyComponent::MyComponent(ComponentList parentContainer)
{
    //we check whether another component exists in the container.
    //in this way we can have dependencies on components
    //without grouping together unnecessary functionality
    //for instance, you may want a renderable to require a graphics core component
}
share|improve this answer
    
Yes, that string stuff is what worries me too. Thanks for your in-depth answer, I've never seen that reinterpret_cast trick before, that looks like a much simpler solution! –  tommaisey Jun 30 '13 at 15:37
    
In the end, I've decided to use std::type_info().hash_map() to generate (hopefully) unique ids for each Component. Thanks for your help! –  tommaisey Jun 30 '13 at 19:35

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.