Okay, I think I understand your use case better now. You actively want the ability to load meaningless junk. That is, user defined attributes for which the compiled executable has no specialized code. Then, while waiting for a programmer to implement their logic, a level designer could get on with placing objects containing said attributes. If that's the case, then a fully typed entity system could still work. You'd simply define a 'EditorAttribute' which would suck up all unknown attributes. The editor system would look for object with said EditorAttribute and display it's contents as if they were first class attributes. If, on the other hand, the idea is to have the logic be implemented in script files, while the above idea would still work (e.g. through a ScriptVars arrtibute), you would probably be better off back with the old string based system (depending on the amount of data that gets stuffed into the ScriptVars attributes).
[...]
Yes, that is right. Such use cases is what I have in mind. I am not entiry sure what you mean with a 'EditorAttribute' which would suck up all unknown attributes, but I have an idea on an implementation that I kind of like. It would work like this:
The entity manager has such a function:
eManager.registerAttribute<int>("Health");
After this line, "Health" is a valid attribute and the manager remembers the type of it.
You then create entities like so:
eManager.registerAttribute<int>("Health");
eManager.registerAttribute<float>("Weight");
WeakEntityPtr entity1 = eManager.createEntity({"Health", "Weight"});
WeakEntityPtr entity2 = eManager.createEntity({"Health"});
WeakEntityPtr entity3 = eManager.createEntity({"Weight"});
The parameter is an std::vector<std::string>. Exceptions are thrown if the attributes are not valid. WeakentityPtr is a typedef for std::weak_ptr<Entity>
Entity attributes are then accessed like so:
eManager.setAttribute<int>(entityId, "Health", 45);
int health = eManager.getAttribute<int>(entityId, "Health");
And in a similar sense to what you described before, the manager remembers what type the attributes where registered with, which makes it easy to add type safety. Behind the scenes, it stores the attributes in a copy-safe type erased way such as std::shared_ptr<void> or whatever is suitable. That way it can handle any type and it also has type safety. As a shorthand you can also call .setAttribute<int>("Health", 45) directry on an entity.
However, this is quite tedious to use since you have to provide an std::vector of std::string every time you create an entity. So there will be a utility class (which was previously functionality in the EntityManager, causing messiness) called EntityFactory or something along those lines. The entity factory takes the entity manager as a reference upon creation and provides an API to register entity templates with default value setters. These templates can be loaded from JSON files. So you can have this JSON file:
{
"player":
{
"position":"900.0f,100.0f", "velocity":"", "acceleration":"", "maxvelocity":"5.5f", "maxacceleration":"1.0f", "hitbox":"24.0f,24.0f", "collisiontype":"solid", "collisiongroup":"player"
}
}
This file defines one entity template but can define more as well. Every template has a name ("player") and a bunch of associated attributes. The attributes are pairs of their name and default value when the template is instantiated.
To use the attributes, they have to be registered in the entity manager using the registerAttribute<>() function, and to put a default value, a default-setting function must be registered using entityFactory.registerDefaultSetter(). If a default setter is not needed, the default value can be left empty in the file. Example on how to register a default-setter for a glm::vec2 for position:
entityFactory.registerDefaultSetter("position",
[] (const std::string attribute&, const std::vector<std::string>& arguments, fea::WeakEntityPtr entity) {
entity.lock()->setAttribute<glm::vec2>(attribute, glm::vec2(std::stof(arguments[0]), std::stof(arguments[1])));
});
Sorry for the messy lambda code but I wanted to keep it short in the forum post.
Then to finally instantiate an entity template, you would do something like:
WeakEntityPtr entity = entityFactory.instantiate("player");
I might also look into making the system template based so that it is optional if you want strings as identifiers for the attributes or if you'd prefer a numerical type or anything else. That way you could use enums if you care about strings being low performance to compare.
That's it.
To me this API seems quite straight-forward and it seems reasonably type-safe and exception safe. But that's easy to say without another persons perspective haha. So are there in your opinion any serious drawbacks or limitations with a system like that? Thanks again for your feedback and ideas.
edit: Forgot to reply to this bit:
The code I presented for mapping a type to an integer has the additional property that for the first type queried, the returned value will be 0; for the second type queried it will be 1, and so on. This makes it suitable for use in as the index into a vector. Resizing a vector to accommodate the returned value of the code you presented could (and likely would) result in gobs of memory being wasted. This besides the fact that I don't think the std::type_index class defines a conversion to integer.
True, useful if it should be stored indexed in a vector. I guess I just personally prefer to use an unordered_map for such storage so that I don't need to worry about managing indices.