Entity Component System
During the development of our Cobra Engine, me and Jonathan Lundström wrote the implementation of our entity component system. We also wrote this entry together, so you can find a copy of it on his portfolio here (länka sen).
We used entt. Everything in our engine was an Entity, with a simplified interface for basic component handling.
Most of the functions were simple wrappers to entt.
To create an object, you first create an Entity which you then add Components to. Therefore, the second piece of our ECS was our Component class.
We were really interested in templates and wanted to experiment with it and the power of compile time polymorphism. Therefor, we designed our component system based on templates instead of inheritance hierarchies with virtual functions.
Inspired by Unity, we wanted clients to be able to easily add functionality to the components and not be concerned about were to call the functions, register them or create them. If a component were to be updated, all the client would do is to declare an Update function, and the rest would be handled internally.
With virtual functions and inheritance, this would be easy. However, this would incur a runtime penalty. We wanted to solve it with templates.
Our goal was to have a callback manager calling the corresponding functions for all components that had implemented them. If a component had implemented Update but not FixedUpdate, only a callback to Update would be called.
Our solution was using CRTP combined with compile time detection of certain member functions, like Update, LateUpdate, UpdateEditor, OnAttach etc.
The challenge with this approach was to detect which functions a class had declared and defined. If a user had declared an Update-function, we wanted it to be called each frame, without the client having to explicitly call it.
In the Component base class, the first time a Component template is instantiated, a compile time check is performed to see which callbacks should be added.
The callback is simply a wrapper around the way you call a function on each instance of component with entt, along with some extra stuff for the way we handled entities in different registries.
The more interesting part of it all is the compile time detection of member functions:
If the type has the function Update, then update_t will yield the return type of that function and is_detected will evaluate to std::true_type. If however, the type T does not have an Update function, then the std::declval will return error-type and the is_detected will return std::false_type.
This value can then be used as a condition in the if constexpr statement above.
The system was really modular and easy to use, leading us to adopting it for more than just update callbacks.
One of the strongest usages of the system was the ability to add callbacks for updating components in the editor using our Dear ImGui-based editor.
This entity had a Transform- and a RectangleLight component attached to it, both with corresponding UpdateEditor functions.
Save and Load
Another usage was the implementation of save and load. If you wanted to make a component savable, all you needed to do was implement a Save function (following certain rules for writing to a json file).