Hi Folks.
This is actually my first post on gamasutra :) I am here pretty much every day and checkout cool posts, today is gonna be the day I add one by myself :) You will find the origial post here.
In this article I want to talk about the Entity-Component-System (ECS). You can find a lot of information about the matter in the internet so I am not going to deep into explanation here, but talking more about my own implementation.
First things first. You will find the full source code of my ECS in my github repository.
An Entity-Component-System – mostly encountered in video games – is a design pattern which allows you great flexibility in designing your overall software architecture[1]. Big companies like Unity, Epic or Crytek in-cooperate this pattern into their frameworks to provide a very rich tool for developers to build their software with. You can checkout these posts to follow a broad discussion about the matter[2,3,4,5].
If you have read the articles I mentioned above you will notice they all share the same goal: distributing different concerns and tasks between Entities, Components and Systems. These are the three big players in this pattern and are fairly loose coupled. Entities are mainly used to provide a unique identifier, make the environment aware of the existence of a single individual and function as a sort of root object that bundles a set of components. Components are nothing more than container objects that do not possess any complex logic. Ideally they are simple plain old data objects (POD’s). Each type of a component can be attached to an entity to provide some sort of a property. Let's say for example a “Health-Component” can be attached to an entity to make it mortal by giving it health, which is not more than an integer or floating point value in memory.
Up to this point most of the articles I came across agree about the purpose and use of entity and component objects, but for systems opinions differ. Some people suggest that systems are only aware of components. Furthermore some say for each type of component there should be a system, e.g. for “Collision-Components” there is a “Collision-System”, for “Health-Components” there is a “Health-System” etc. This approach is kind of rigid and does not consider the interplay of different components. A less restrictive approach is to let different systems deal with all components they should be concerned with. For instance a “Physics-Systems” should be aware of “Collision-Components” and “Rigidbody-Components”, as both probably contain necessary information regarding physics simulation. In my humble opinion systems are "closed environments". That is, they do not take ownership of entities nor components. They do access them through independent manager objects, which in turn will take care of the entities and components life-cycle.
This raises an interesting question: how do entities, components and systems communicate with each other, if they are more or less independent of each other? Depending on the implementation the answer differs. As for the implementation I am going to show you, the answer is event sourcing[6]. Events are distributed through an “Event-Manager” and everyone who is interested in events can listen to what the manager has to say. If an entity or system or even a component has an important state change to communicate, e.g. "position changed" or "player died", it can tell the “Event-Manager”. He will broadcast the event and all subscriber for this event will get notified. This way everything can be interconnected.
Well I guess the introduction above got longer than I was actually planning to, but here we are :) Before we are going to dive deeper into the code, which is C++11 by the way, I will outline the main features of my architecture:
memory efficiency - to allow a quick creation and removal of entity, component and system objects as well as events I could not rely on standard new/delete managed heap-memory. The solution for this was of course a custom memory allocator.
logging - to see what is going on I used log4cplus[7] for logging.
scalable - it is easy to implement new types of entities, components, systems and events without any preset upper limit except your system's memory
flexible - no dependencies exist between entities, components and systems (entities and components sure do have a sort of dependency, but do not contain any pointer logic of each other)
simple object lookup/access - easy retrieval of entity objects and there components through an EntityId or a component-iterator to iterate over all components of a certain type
flow control - systems have priorities and can depend on each other, therefore a topological order for their execution can be established
easy to use - the library can be easily in cooperate into other software; only one include.
The following figure depicts the overall architecture of my Entity-Component-System:
Figure-01: ECS Architecture Overview (ECS.dll).
As you can see there are four different colored areas in this picture. Each area defines a modular piece of the architecture. At the very bottom - actually in the picture above at the very top; it should be upside down - we got the memory management and the logging stuff (yellow area). This first-tier modules are dealing with very low-level tasks. They are used by the second-tier modules in the Entity-Component-System (blue area) and the event sourcing (red area). These guys mainly deal with object management tasks. Sitting on top is the third-tier module, the ECS_Engine (green area). This high-level global engine object orchestrates all second-tier modules and takes care of the initialization and destruction. All right, this was a short and very abstract overview now let's get more into the details.
Memory Manager
Let's start with the Memory-Manager. It's implementation is based on an article[8] I have found on gamedev.net. The idea is to keep heap-memory allocations and releases to an absolute minimum. Therefore only at application start a big chuck of system-memory is allocated with malloc. This memory now will be managed by one or more custom allocator. There are many types of allocators[9] ( linear, stack, free list...) and each one of them has it's pro's and con's (which I am not going to discuss here). But even if they internally work in a different way they all share a common public interface:
class Allocator { public: virtual void* allocate(size_t size) = 0; virtual void free(void* p) = 0; };
The code snippet above is not complete, but outlines the two major public methods each concrete allocator must provide:
allocate - which allocates a certain amount of bytes and returns the memory-address to this chunk and
free - to de-allocates a previously allocated chuck of memory given it's address.
Now with that said, we can do cool stuff like chaining-up multiple allocators like that:
Figure-02: Custom allocator managed memory.
As you can see, one allocator can get it's chunk of memory - that it is going to manage - from another (parent) allocator, which in turn could get it's memory from another allocator and so on. That way you can establish different memory management strategies. For the implementation of my ECS I provide a root stack-allocator that get's an initial allocated chuck of 1GB system-memory. Second-tier modules will allocate as much memory as they need from this root allocator and only will free it when the application get's terminated.
Figure-03: Possible distribution of global memory.
Figure-03 shows how the global memory could be distributed among the second-tier modules: "Global-Memory-User A" could be the Entity-Manager, "Global-Memory-User B" the Component-Manager and "Global-Memory-User C" the System-Manager.
Logging
I am not going to talk too much about logging as I simply used log4cplus[7] doing this job for me. All I did was defining a Logger base class hosting a log4cplus::Logger object and a few wrapper methods forwarding simple log calls like "LogInfo()", "LogWarning()", etc.
Entity-Manager, IEntity, Entity and Co.
Okay now let's talk about the real meat of my architecture; the blue area in Figure-01. You may have noticed the similar setup between all manager objects and their concerning classes. Have a look at the EntityManager, IEntity and Entity classes for example. The EntityManger class is supposed to manage all entity objects during application run-time. This includes tasks like creating, deleting and accessing existing entity objects. IEntity is an interface class and provides the very basic traits of an entity object, such as an object-identifier and (static-)type-identifier. It's static because it won't change after program initialization. This type-identifier is also consistent over multiple application runs and may only change, if source code was modified.