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)?