SimpleECS (rewriting in progress due to a refactor)

TestTestTest

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.

Entity1 has Transform, Mesh, Animated and AnimationController component
 
You can read more about the editor here
Here's an example of how ECS is initialized and can be used.
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?

 

EntityPool
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.

 

ComponentPool
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.

 

ECS folder
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!