5
\$\begingroup\$

This question and its answers move against utilizing an entity component system where the component itself is just a generic class with an dynamic container for the attributes/properties of the component.

Some definitions for this thread:

  • Entity = ID + collection of components
  • Component = Container (std::map<attribute_name, attribute_value>) of attributes/properties, such as health, age, name, gender, etc.

I understand and am aware of the drawbacks (no compile-time type safety, added overhead of looking up values from the container). Nonetheless, I currently am working on/with such a system, with the following argument, which I would now like to put to the test: data-driven design.

If all the attributes of the component are in a dynamic container, it is very easy (in fact possible at all) to add new ones later on, outside of the source code. So you can easily script new attributes later on. And dumping/saving the state of the component is a piece of cake.

Compared to the proposal in the above mentioned thread, I also have an own class for each component, to implement some basic behavior and helper methods directly in them (but those also need to look up their values from the container).

Example:

My component base class has a map for all the attributes/property:

typedef std::map<std::string, PropertyBase*> PropertyMap;

So an actual component looks something like the following. In the constructor I populate the attributes I want the component to have, by adding them to the map. Later I can read and update them if needed, but always with a performance penalty, since I first have to find them in the std::map.

class Person : public ClonableComponent<Person>
{
public:
    Person(ComponentFactory* const pFactory, ComponentType type, ComponentID id, GameEntity* const pEntity) 
    : ClonableComponent(pFactory, type, id, pEntity)
{
    // General
    addProperty(new Property<int>("age", PROP_INT, 0));
    addProperty(new Property<String>("gender", PROP_STRING, "unknown"));        
    addProperty(new Property<String>("currentLocation", PROP_STRING, "unknown"));

    // Feelings
    addProperty(new Property<bool>("isTired", PROP_BOOL, false));
    addProperty(new Property<bool>("isHungry", PROP_BOOL, false));
    addProperty(new Property<bool>("isThursty", PROP_BOOL, false));
}

void enterLocation(GameEntity* pLocGE)
{
    if(pLocGE->hasComponent(COMP_TYPE_LOCATION))
    {
        updateProperty("currentLocation", pLocGE->getName());
        updateProperty("casinoEnteredDateTime", (int)std::time(0));
    }
}

void leaveLocation()
{
    updateProperty("currentLocation", std::string("unknown"));
}
};

However, that thread made me think, and I am now doubtful that my approach will work well in the long run (maintenance, scaling and performance wise) and I am therefore considering moving to an directly typed approach with no look-up overhead. At the same time I don't want to loose that data-driven extendability.

I thought of a hybrid approach: Keep all known variables as actual language variables but also add a container (std::map) for scripted/data-driven variables. Therefore I'd know that my internal access is very fast and only the later added scripted extension would have the slow look-up and the other mentioned penalties.

Question: What do you use / would propose as an approach for a data-driven ECS in terms of attribute storage/handling? How do you keep it extendable? Would you recommend such a hybrid approach (real variables internally, container for new, external definitions)?

\$\endgroup\$
1
  • 2
    \$\begingroup\$ make something -> make it work -> make it work better. You pick up neat tricks throughout the journey, but the important thing is having games that people play \$\endgroup\$
    – wes
    Mar 7, 2014 at 19:32

2 Answers 2

3
\$\begingroup\$

If all the attributes are in a dynamic container, it is very easy (more correctly: possible at all) to add new ones later on, outside of the source code. So you can easily script new attributes later on.

Yes, but you can't do anything with new attributes added via scripts except via scripts without losing this "advantage." Once you need to refer to these values in source code, you should have just added a concrete field to a concrete type rather than something in a Dictionary or whatever.

If you want the ability for scripts to dynamically add new bits of data without recompilation (which is a very useful feature) than provide this functionality just to the ScriptComponent (or what have you) in your system. This sounds relatively identical to your "hybrid" approach anyway: I'd just caution that you shouldn't need the functionality anywhere but a component that is providing/running a script.

And dumping/saving the state of the component is a piece of cake.

This should not be the concern of the entity/component system. This should be the concern of the reflection/content representation system you use. It's trivial to implement without dynamic "property bag" dictionaries in languages like C# with good reflection support, and it's only slightly more work to do it in something like C++ with really poor reflection support. Consequently you shouldn't use this as a strong rationale for storing component attributes in a runtime associative array.

Would you recommend such a hybrid approach (real variables internally, container for new, external definitions)?

Basically, what I said above: don't use a runtime associative array for component attributes by default. If your scripting system needs that functionality, provide it via the components bound to the scripting system.

\$\endgroup\$
4
  • \$\begingroup\$ I currently have this extendability everywhere since the entities are defined in XML files, where (for each component) values can be altered and completely new ones added. The latter ones are then of course only usable in scripts. But I am not really sure why I should limit that feature to only a specific component. Additionally, I will have to look into C++ reflection then, if I go with that hybrid approach then. \$\endgroup\$ Mar 7, 2014 at 20:00
  • \$\begingroup\$ Allowing the attachment of arbitrary data to arbitrary types tends to result in data bloat (via potential accidental duplication), promiscuity (relying on pathological coupling to reach across to attached data on other components), and sloppiness: it doesn't make sense. What do you need to attach to a position component that isn't a position? Once you attach that data, it's not a position any more. You can achieve the same results with cleaner semantics by providing only a single defined attachment point for that data owned by the thing doing the attaching. \$\endgroup\$
    – user1430
    Mar 7, 2014 at 20:05
  • \$\begingroup\$ @JoshPetrie But don't you have to use associative runtime containers in order to have functionality that works on arbitrary entity types? E.g. a movement system that supports all entities with a position component. Trying to go around this problem with inheritance would have you end up at exactly the position that component-based systems try to get away from, doesn't it? \$\endgroup\$
    – TravisG
    Mar 7, 2014 at 22:03
  • \$\begingroup\$ No; the question (and the linked related question) are referring to using such a container as a replacement for fields in a type. In other words, instead of "struct position { float x; float y; float z; }" you have "struct position { map<string, object> attributes; }." \$\endgroup\$
    – user1430
    Mar 7, 2014 at 22:13
0
\$\begingroup\$

ECSs are somewhat complicated.

At first I used the following approach: Entities only contain a position vector and a set of components. A component contains a specific behavior and necessary data e.g. RenderComponent, PhysicsComponent etc. This is commonly referred to as ECS.

But this has some disadvantages: The data of an entity is closely entangled with the component functionality. Sooner or later a component needs to access and modify data stored in a different component type e.g. the MovementComponents needs to apply a force to the PhysicsComponents etc. So either directly access the other component via the entity, send an event causing the change in the other component or pull the needed value up into the entity. All bad solutions in my opinion. Also maintaining the lifecycle of each component type was quite a hassle.

Currently I'm using the following approach: An Entity consist of an unique id and a bag of keys and values (hash the key strings for ultimate performance ;-) ). Subsystems (e.g. physicsEngine, renderer) look for entities with property keys they can use and create components internally with the required behaviors. These components store the unique id of their entity and use it to read and modify the entities property values. You could even get rid of the entity class entirely, since it's only an id anyways and store the properties under the entity id in a global property store making the whole design more like a database. Jason Gregory calls this Property Centric Archictecture in his book Game Engine Architecture. Thief and Deus Ex used this approach.

It's trivial to inspect, store and load the game state. Easy to extend and I can create new behavior entirely in script (I'm using python for scripting - great language btw). Components are only known in their respective subsystem. I sacrificed compile time safety and OOP style data hiding sure and I might need some kind of templating mechanism to define and check properties. But for now this approach works suprisingly well.

\$\endgroup\$
4
  • \$\begingroup\$ I fail to see the link to my question. You are just talking about ECS and their entity<->component<->system relationship design. I already got that covered. I am however asking about the attributes/properties/values within one component and their extendability. \$\endgroup\$ Mar 8, 2014 at 7:40
  • \$\begingroup\$ You asked: What do you use / would propose as an approach for a data-driven ECS in terms of attribute storage/handling? This is what I'm using. Sorry if my answer is long winded. I'm still figuring this out. Short answer: I would move the properties from your component class to the entity class. Lookup of properties is not that expensive. I would not keep copies of variables in code. Fetch them from the store, modify them and write them back. You need to avoid allocation and copying of keys and values. Hash key strings into constants. Hope this helps. \$\endgroup\$
    – Stephen
    Mar 8, 2014 at 12:33
  • \$\begingroup\$ So basically, your components just contain the behavior/logic, but no data? Isn't one of the key concepts in a "classical" ECS that each component is self-sufficient and has its own data directly accessible? \$\endgroup\$ Mar 8, 2014 at 17:07
  • \$\begingroup\$ Also it seems to me as if Jason Gregory is n fact advocating distributing the data: "The properties of each game object are distributed across many data tables, one per property type [...] (rather than being centralized in a single class instance [...])." Or am I misinterpreting that part? \$\endgroup\$ Mar 8, 2014 at 17:17

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .