Component design question

Started by
16 comments, last by Sova 10 years, 3 months ago

Currently I store a string name along with each component inside a map in my GameObject. This is how I identify each component when I call GetComponent<ComponentType>(componentName).

I think I'm going to go the route of only allowing 1 component type per GameObject (make it so you can't add 2 of the same components). To me that sort of makes having to pass a name in the GetComponent function because I'm already defining the type via the template param. So I'm wondering how can I structure my C++ code to make my get call more like:

GetComponent<ComponentType>()

If this is how I want t get my components, how would the storage of these components look in GameObject? The type itself is basically identifying it. Would I still need to ID this type another way? Possibly have each component have a static function that returns a unique ID for that component and inside GetComponent I use Type::GetID() and I still store them in a map where the key is the ID? Or is there another way? I can't think of another way, but curious if I'm missing some ideas around this.

Thanks

Advertisement

There's no reason to use naming to get components. You have all you need just by the template parameter.

Rather than looping through the components looking for a name match, you can do a dynamic_cast instead, or some other sort of manual rtti, like a ComponentType::GetComponentType() which returns a unique identifier per component type to compare against and you could then static cast.

So this is what I currently have. The thing I like about this is that GetComponent() doesn't need anything but the type (since it can call the static method based on that). The thing I don't like is that in AddComponent() I have to specify the type because that Type() function is static. I'd prefer AddComponent to not need a type.

I guess I could make some kind of virtual ToString() on Component, and in each of my components return that components static Type() inside the virtual ToString(). Seems a little hooky and requires extra steps to remember when making a component but it should work.


#include <map>
#include <string>
 
using namespace std;
 
class Component
{
};
 
class HUD : public Component
{
private:
   int health; // just for testing
public:
   HUD()
   {
      health = 100;
   }
   int GetHealth() { return health; }
   static string Type() { return "HUD"; }
};
 
class Object
{
private:
   map<string, Component*> components;
public:
   template<class T>
   void AddComponent(Component* comp)
   {
      if(components.find(T::Type()) == components.end())
         components[T::Type()] = comp;
   }
 
   template<class T>
   T* GetComponent()
   {
      if(components.find(T::Type()) == components.end())
         return NULL;
 
      return (T*)components[T::Type()];
   }
};
 
 
 
int main()
{
   Object o;
 
   o.AddComponent<HUD>(new HUD());
 
   HUD* h = o.GetComponent<HUD>();
 
   int health = h->GetHealth();
 
   return 0;
}

You could also use type_info as the map key and void your own key management.

That doesn't seem to work given I don't want to pass the type in the AddComponent(). So if I do typeid() around the component passed into AddComponent() it returns "class Component*", but since I do need the type in GetComponent() to both ID and cast doing typeid() on the type gives "HUD". So given the syntax I'm looking at this doesn't look like it would work. The benefit of making AddComponent() require the type template would be that the creators or the components don't have to worry about ID's. The downside is just visually looking at AddComponent() where I'm already passing in an instance of a component it would seem redundant to have to pass the type also. I guess that's the lesser of 2 evils though as the less rules component creators have to remember the better.

I guess I could make a version of AddComponent() that doesn't take any parameters and inside it'll just make a new object of 'T'. This would work for any components that don't need arguments in the ctor or I could fill the arguments out after the fact.

I agree with not liking the idea of passing the type when you have already passed an instance of the component into the object.

The way I have my AddComponent() method implemented is to use a template for the type, as you have, however having the method do the allocation of the component and adding it to its internal list for management and then returning a pointer to the object.


template<typename T>
T* AddComponent()
{
    T* newT = new T();
    // add to lists
    // do whatever else
    return newT;
}

This way the component is managed from the start and the caller is just a consumer.

My issue with that method is that I don't know if I like not being able to use the ctor with parameters though. Seems there won't be a "perfect" way to do it though and I'll just have to pick the one that I dislike the least.

I also think I'll look at returning references from GetComponent() instead of pointers too (because the Object class owns the component memory management), but I'm not sure how I would do that if I need to check if the component exists in the container and if not what to return.

These problems you have are the reason why I think its the wrong approach to put the components into the entity.

I would create some template storage class, where each instantiation holds all components of one type, and then retrieve it by giving the entity id to the appropriate one. Its automatically determined then which type it is and you avoid all RTTI/emulated RTTI. Meanwhile the storage could be a nice cache-friendly array internally, where it allows updating all components of a type with one iteration through it by the corresponding system, without repeatedly asking for each single component.

Do you mean something like what I have below? This does take away the problems I was having and should be faster because of less cache misses right? This is for sure a different way to look at it. I don't like explicitly making containers for each one, but there must be a way to mask that in another templated class or something. A way I can sort/group like component types so they get looped over together all in 1 container. I know I have "property" code that allows storage of any data type in one container so I'll see if I can't merge the ideas.


#include <map>
 
using namespace std;
 
class Component
{
private:
    int entityID;
public:
    Component() {}
    Component(int id)
    {
        entityID = id;
    }
 
    virtual void Update()
    {
    }
};
 
//--------------------------------
 
class Object
{
private:
    static int ID;
    int id;
public:
    Object()
    {
        id = ID;
        ID++;
    }
 
    int GetID() { return id; }
};
 
int Object::ID = 1;
 
//--------------------------------
 
class AI : public Component
{
public:
    AI() {}
    AI(int id) : Component(id) {}
    virtual void Update()
    {
    }
};
 
//--------------------------------
 
template<class T>
class Storage
{
private:
    map<int, T> data;
public:
    void Add(Object& obj)
    {
        int id = obj.GetID();
 
        // make sure this object can only have 1 of this type
        if(data.find(id) == data.end())
        { 
            data[id] = T(obj.GetID());
        }
    }
 
    T& Get(Object& obj)
    {
        return data[obj.GetID()];   // this is dangerous because it could cause an exception, but I don't want to return a pointer because the caller shouldn't be able to mistakenly delete it. a reference explicitly tells them they don't have to worry about the memory
    }
 
    void Update()
    {
        map<int, T>::iterator iter;
 
        for(iter = data.begin(); iter != data.end(); ++iter)
            (*iter).second.Update();
    }
};
 
//--------------------------------
 
int main()
{
    Storage<AI> ai;
    //etc. for all component types
 
    Object o1;
    Object o2;
 
    // make a new AI component and assign it to o1 object
    ai.Add(o1);
 
    // make another AI component and assign it to o2 object
    ai.Add(o2);
 
    AI a1 = ai.Get(o1);
    AI a2 = ai.Get(o2);
 
    while(true)
    {
        // update each component type together
        ai.Update();
 
        // if we had other component type containers we'd update all of them as well
    }
 
    return 0;
}

Yes, though you have it a bit different than I have.

It is kind of flexible so you can adapt it as you like. Some examples of what could be done different:

- I did not care for handling different component types the same, so I dont have any virtual method there. I just add on whats needed and let the systems handle that.

- I have the storage class (I called it ComponentTable in my code) be only responsible for storing the components so it can be a single templated class mostly independent of the component types. I let it keep a vector sorted by entity id currently, but I imagine many different data structures have the potential to be useful for that (can be easily changed and profiled later on).

- I'm making system classes that get a reference to the corresponding storage (to get iterators) and provide the update method, but can internally do what they like with all components of the corresponding type (it could sometimes simplify the code if putting most logic into a system that knows the exact component type). Its good to try for a 1:1 relationship at first, but there may be some system reading one component type and writing a second, which some other system could read later or it may be needed even more sometimes, which can be done if the system and storage are not combined.

- I dont even have an Entity/Object class, only an EntityTable to avoid the static variable and not disallow the future possibility of having more than one ECS running at once (although thats not very likely). I also put a sorted vector with all entity ids of currently alive entities there, but I may remove that eventually (but then there would be only the way of asking all component storages if any got a component with that entity id left).

- It may be useful to collect one instance of all storage and system classes into one class that handles completely deleting an entity with all components or creating or calling all system updates in the right sequence, but it would need to be modified depending on which systems/component types the game needs.

This topic is closed to new replies.

Advertisement