SimpleECS (rewriting in progress due to a refactor)
This post is being rewritten due to a major refactor of the ECS codebase, prompted by testing, discussions, and an evaluation of its limitations and practical applications. The new ECS version has now been implemented in SimpleEngine.
Anything below this is outdated (current date: 19 August 2024).
Introduction
In this post, I'll assume the reader already has some idea of what an Entity Component System (ECS) is and will dive straight into my variant of ECS. This variant intentionally diverges from some ECS principles due to specific design choices and limitations in knowledge, experience, and time.
My interest in ECS began with game project 5 at The Game Assembly (TGA), but it was actually in game project 6 where I started to delve deeper into learning about ECS. From my observation, people find it easier to understand and use architectures similar to Unity and some prefer nodes similar to Unreal. At TGA, my team was using FLECS, but despite its excellent documentation, it was challenging to learn within our limited timeframe, causing bottlenecks in gameplay and other systems.
Therefore, I would like to try implementing an ECS variant that strikes a balance between a simple user interface and performance. A design that give developers freedom, flexibility and maximize simplicity for gameplay programmers.
Goal
My aim is to develop a straightforward ECS design that mirrors Unity's functionality and is user-friendly for editors, node scripters, and gameplay programmers alike. Most importantly, I aim to enjoy and learn from the process of implementing this design.
If it looks simple, we have done a great job in hiding the complexity
Demo
The GIF below demonstrates the RenderSystem in action, interacting with an Entity that contains components such as Transform, Mesh and Animated.
Overview
Some fundamental include:
- Create/Remove Entities
- Create/Remove Component of type T
- Get all components of type T
- Get all entities
- EntityManager
- ComponentManager
- SystemManager
- MemoryPools
- Entity & IEntity & Entities
- And more
Entity
This interface is for Entity, hence it is called IEntity. Users cannot store or create IEntity directly; they must use the EntityManager, which acts as a factory. For debugging purposes during implementation, I've added std::string and padding to reach 64 bytes per IEntity class. I plan to remove these in the future and instead use name and tag components, reducing the IEntity class to 16 bytes.
Entity is defined as 'IEntity* const&'. Why? I wonder the same. It has caused me quite a headache but there's a reason for this, which I'll explain in a bit down this post.
EntityManager
All entities created via EntityManager are stored in an EntityPool, a memory pool dedicated to creating and storing IEntity objects. The EntityManager class assigns unique IDs to entities and keeps track of them.
You may have noticed the presence of "myRemovedEntityIDs". At the time, I had planned to create an object pool for entities and components, but I ultimately decided to postpone it due to the complications it introduced and if it was even worth it?
ComponentManager
The ComponentManager functions similarly to the EntityManager, but it contains an unordered map that holds ComponentPools. This design allows for quick retrieval of all components of type T. Additionally, if the ComponentID is known, a constant-time lookup can be used to retrieve a single component. Finally, the ComponentManager includes a map for calling the destructor of components.
SystemManager
The SystemManager is the most straightforward. It simply manages all systems that have been added and updates them in the order of EarlyUpdate, FixedUpdate, Update, and LateUpdate, including rendering.
Entities
This specialized class interface is designed for managing entities, ensuring that changes to original entities are reflected accurately when the EntityPool reallocates its size and relocates addresses. This approach prevents users from encountering dangling pointers due to reallocation issues. Accessing entities through this interface is as straightforward as accessing an array. Furthermore, I plan to enhance this interface to filter commonly used entities, such as those with a transform or mesh component, making them easily accessible for various systems.
The Entities class operator[] returns an Entity, which is of type 'IEntity*const&'. This aspect presents a challenge for me to decide upon. The reason is straightforward: I require a reference to a pointer to accurately reflect changes to the Entity, whether it involves removal or relocation of memory address. Simultaneously, I must make it 'const' to prevent accidental reassignment to another entity or nullptr, which could lead to significant issues withing EntityPool.
System
The base System class is relatively straightforward, whereas the derived System classes can become significantly more complex. For instance, the RenderSystem might exclusively target entities possessing Transform, Mesh, and Animated components, while the CollisionSystem might necessitate inter-system communication for data or message exchange. However, these functionalities are not yet implemented.
ECS
Once ECS.hpp is included, it's ready to use straightforwardly. The goal is to support one or more instances of ECS per level or world. However, I currently don't see the necessity for more than one ECS instance per world, and this interface is lacking several functions, such as RemoveEntity, due to time constraints.
Discussion
Entity (IEntity*const&)
Using a reference for Entity prevents straightforward storage in a standard std::vector. I've opted to store these specific entities indirectly through EntityID. However, this approach requires multiple memory access operations when accessing entities via their IDs, which is not the most efficient method. But most importantly, it was not straightforward usage such as that we need to call GetEntity using the EntityID before begin accessing. Nonetheless, after profiling and considering my limited understanding of assembly, it appears that the performance impact of these multiple indirect accesses is currently insignificant. Consequently, I've contemplated using double pointers instead, but it wasn't as user-friendly, for example calling *Entity->AddComponent<T>().
Reflection
I am currently working on a reflection system. By using a macro like REGISTER_COMPONENT on a component I want to reflect, it will be added to a list that can be displayed in the editor when clicking on 'AddComponent.' Additionally, I'm trying to implement functionality similar to Unreal Engine's 'UPROPERTY,' which exposes variables to the editor. These features would significantly enhance the ease of working with the ECS in editor and node scripting systems.
You can read more about my reflection here https://natdanaiputhom.github.io/reflection/
And more
There are many aspects I have yet to mention, including missing components and additional features I would like to incorporate. One such consideration is filtering feature that filters entities with specific type of components similarly to Archetype meanwhile maintaining the objects continuously in same memory block.
So far, this journey has been both enjoyable and rewarding, and I intend to continue exploring and experimenting with different ways to implement and enhance my ECS design. Thank you for reading this far, I'm open to discussion and suggestions, just hit me up!