All coded in C++ using SDL2 for windows and rendering. GitHub link, or see snippets below.
Intro
Whilst exploring methods for creating cache friendly code, I looked into entity component system (ECS) architecture. After having implemented a simple sparse ECS I jumped to creating an archetype (a.k.a. table-based) ECS for its benefits in iterating over entities that have the same components. For example, if an entity only has position, its data won’t be fetched when a system wishes to iterate over position and velocity components.
Features
- Templated variadic system that takes user provided functions (lambdas) to run on entities with relevant components.
- Templated component system with static ID generation.
- Custom memory management using type erasure to store components in tables.
- Table edges for quick add and remove component operations.
- Multi-threading to speed up systems.
- Runs at over 60fps with over 5 million entities on tested PCs.
Future Work
These are the considerations I have taken on the future of the project to improve it:
- Multi-threading should be changed to use thread pooling instead of spinning up threads all the time.
- More profiling and consideration into performance optimisations should be taken.
- Instead of run-time generated component IDs a method such as hashing could be used to generate compile time IDs.
- Memory pooling and other memory allocation considerations should be taken to improve the way the ECS handles memory.
- Create a more advanced project that utilises the ECS.
Interesting Problem
One of the more interesting problems I had to face when creating the ECS was the ability for the systems to get all the relevant component data from tables to be able to operate over them. In order to achieve this I utilised the IDs for each component, giving each System a type which is a set of the IDs of the components it operates on. This required using the StaticID helper class to get the created ID of each component that the variadic takes, very similar to the way the Table’s have their type generated.
Once the System had this type it could make a call to the ECS engine to get a list of pointers to all the tables that fully contained the System’s type as a subset. However, this is a potentially slow operation that could be improved by caching the relevant tables, as during run-time it would be unadvisable to be creating an entirely new Table anyway.
Then all the tables could be looped over and the desired components extracted from them, using the ID alongside the cast in a fold expression, so that we can safely reinterpret the void pointer as a pointer to the start of the component buffer. These pointers to the start of the component buffers are then all stored in a tuple so that they can be retrieved in the for loop that calls the function supplied.
This method of calling the function with the dereferenced, then incremented, pointer to the data was a critical place of note when it came to performance. However, through profiling the current implementation versus a method that makes the lambda itself iterate over the data, I found that calling the function multiple times was not slowing down the program, so for ease of implementation on the user side it was left as is.