top of page
Search
chilaganirajesh95

Engine System ECS

Updated: Dec 28, 2020

In this blog, I will explain the ECS system I have been working on for the past few weeks, I will explain how you can use the ECS system in your project and also how you can use the asset builder to load the component data.

I have updated the MyGame project to use the ECS system. Following is the MyGame project using the new ECS system to load meshes, effects, and other data.

ECS(Entity-Component System)

Before going through my project let's understand what an Entity-Component System is and why do we need it.

ECS is an architectural pattern used in game development to create and manage game objects in a game.

An Entity is a container into which components can be added to make it a meaningful object and program some behavior.

A Component is an object through which data or behavior is added to an entity.

So do we need ECS? ECS is not a mandatory system to make a game but it is important to set up some kind of base framework in your engine for the game developers to create objects, and modify them. This could be any framework and need not be an ECS but ECS is a popular pattern in game development so it will be easy to understand an ECS implementation rather than understanding a custom framework.

Design Approach

There are many ways an ECS can be implemented I am familiar with a couple of ECS implementations I will discuss them briefly and then explain which implementation I used and the reasons for choosing that implementation.

Design One:

In this design, you will be creating a base abstract class for your component, and this will define the interface for a component. It will contain all the necessary things a component should have in your ECS. Following is an example of the base component class with some methods but it can contain more based on your implementation.

classComponent
{public: 
virtual void init()=0;
virtual void update()=0; 
virtual ~Component(){}
private:
}; 

All other components you create should derive from this base component class and should implement all the required methods. Then you create an Entity class which has a container for all the components that this Entity can have with similar methods to what component has like init(), update(), along with methods to add, get, remove components.

Finally, you will have a Manager class which will be a container for all the entities and contains methods like init and update which will call the corresponding methods in Entity to update or initialize all entities.

Following is an illustration of how various components are stored in an Entity. Usually, you will have an array/vector of component pointers pointing to the derived component and you will use RTTI(Run-time type information) to find a specific component in the container.

DesignTwo:

In this design instead of creating a base interface for the component, we just use plain structs as components but we will create a storage pool for each component. That means all the instances of the same component are stored in the same container, unlike the previous method where different components of an Entity are stored in the same container.

Following is an illustration of the storage pool

We will also create a registry that keeps track of all the storage pools and allows to access the storage pool based on the component type. In this design Entity at its core is just an identifier that is used to map different components but it is fine to have an abstraction for the Entity which has a clean and user-friendly interface.

Each implementation has its advantages and disadvantages but I used DesignTwo for my ECS. Following are the reasons for my choice.


In the first design, the way you update things is you call update on the Manager which calls update on all of its entities and each entity will update all its components like a chain reaction. So it is not that easy to update only specific component data. For example, if you wish to update only the physics data of all entities it is not that easy.

In DesignTwo this is very simple you need to get the storage pool for physics and update the corresponding data. DesignTwo also has better performance when updating the component data. As all the component data is in a single container there will be fewer cache misses when compared to the first approach. One disadvantage I noticed in design two is that all the components data is stored inside the registry so it is not possible to view the component data of a particular entity during debugging(ex: if you have a breakpoint and want to watch all components of a particular entity), you have to look inside the registry to find the component data of a particular entity. So it is a little difficult to debug without understanding the design.


Adding the ECS system to your project

The ECS system is built as a static library. You can find the download link for the project at the bottom of this page. I have also built an Asset builder project which is used to convert human-readable component data to binary data and load it during run time. I am using Lua for the human-readable file so if you wish to use my builder you can download the builder project as well but you can create your own builder using your preferred format. You can find more details for creating human-readable files for components in other formats in my previous blogs.

Once you have downloaded the project. Add the project to your solution under the Engine folder both in your solution and in the file system. Once you have added the project you need to add ECS as a reference to the project where you intended to use the ECS. In my case, it is the MyGame project.

The references for the ECS project might depend on if you wish to use the components that I created. If you don't use my components then there are no references to be added to the ECS system but that might change based on what data your components have and how you plan on loading them. More detailed instructions can be found in the download link and Feel free to reach out if you need any help in setting up the project.

Using the ECS system

After downloading and setting up the project, to use the ECS interfaces you will only need to include ECS.h in your cpp file this will include all the necessary files to access ECS. If you wish to use my components you have to include the components.h file as well. In the ECS.h file, there are examples of components and few details that you need to be aware of while creating components like making sure you have a default constructor for all the components you create. There is also an ECSExamples function that explains in detail how to use the ECS system to create entities and how to use provided interfaces. The example talks about the Registry, Creating an Entity, and the other interfaces provided by the ECS.

The above examples should be enough to start using the ECS system but I will also provide few implementation details as well.

There are three major classes that handle the whole implementation ComponentStorage, Registry & Entity.

ComponentStorage class is the actual pool. This is where all instances of a component will be stored. You will never have to create these objects the Registry class will handle that creation of pools.

Registry class is the container for all pools. It handles the creation of pool objects and adding data to the correct pool based on the component type.

This class is what you will majorly use to add, get, remove components. This is the first thing you will need to create to able to use the ECS system.

The Entity class is just an abstraction. As mentioned above, the Entity is just a number at its core and the number is used to map a component to an Entity. Internally it uses the Registry to add & modify components. The reason I created the Entity interface is that we learned that it is always useful to create an interface that abstracts the underlying implementation details and gives a user-friendly interface to work with.

Another such useful class that I created in this system is the ComponentStorageIterator which allows you to iterate over a ComponentStorage(pool) which I think makes it very easy to iterate over a pool. You can use the GetComponentStorageIterator method in the Registry to get an iterator for a component type by providing a template parameter and use it as a standard map iterator. I have overloaded operators like (->) to access the internal data similar to a map iterator.

Using the Component Builder

If you wish to use the component builder as well the link will be available at the bottom of this page.

Once you have downloaded the project. Add the project to your solution under the Tools folder both in your solution and in the file system. It is exactly similar to other builder projects we have nothing new in the setup.

First, let's go through the human-readable file format and then later discuss the things you will need to do to able add your custom components to the human-readable file and set up the binary layout for them.

return
{
    Components = 
    {
        TagComponent = 
        {
            Name = "Plane",
        },
        RigidBodyComponent =
        {
            Position = { 0.0,0.0,0.0,},
        },
        RenderingComponent =
        {
            Mesh = "Meshes/Plane.fmesh",
            Effect = "Effects/StandardEffect.lua"
        }
    },
} 

So you have to use Lua to able to use the Component Builder. It contains a Components key table and this is where you will add a table for the information required to create your component in the game. For example, I want a TagComponent to add to my entity and I need a string to initialize my TagComponent so I created a table for TagComponent and add the string. Similarly, other components contain the information required to initialize them. Adding a key table for each component also makes it easy to understand what components are being added and what information they contain. We want these files to be readable and maintainable so that it is easy to debug if something goes wrong.

After adding your component to the human-readable file you will need to do a couple of things for the builder to build a binary file. First, you have to add your component to the ComponentType enum present in the components.h file.

This enum will be used in the layout of the binary file when building the binary file. The second step is to add a function in cComponentBuilder.cpp in the builder project which parses the data of your component from the human-readable file and creates binary data.

Once that is done you will first write the ComponentType enum of your component and then actual binary data that you parsed.

Following is an example function which writes a TagComponent

notice that the ComponentType enum is written before the actual data. You can find more examples in the cComponentBuilder.cpp file but feel free to reach out if you have any questions.

Following is the corresponding binary file generated for the above human-readable file. You can observe that before each component data there is an enum which we will use when loading the binary data.

Loading the binary data is simple all you have to is call the LoadComponents function and provide the entity to which all the components need to be added and the file path to the binary file.

One more thing you need to add is the code to extract binary data of your component. You will have to add this in the LoadComponents function in ECS.cpp

You can find examples of how other components are doing it. Basically, you will need to add a new case for your component and use the current offset to extract the information, and update the current offset properly by incrementing it with the size of your component in the binary file. That's all if you update the offset properly everything gets extracted properly and added to the entity. Make sure you add the component to your entity once it is extracted.


Challenges

One challenge that I faced while working on this system is setting up the builder project in a proper way. I know that I cannot have a generic builder project which works for every component without knowing what kind of information that the component has, So I have to come up with an easy way for users to add their components as well as the code the parses the corresponding component data and builds a binary data. Once that is done users should also able to easily extract the corresponding binary data from the file. To make it as simple as possible I created a new enum which will indicate what component data is present right next to it in the binary file. This allowed me to write a loop that will continuously check if the reading file is done or not and at the same time what component data is being extracted currently just by using the enum. As long as the offset is being updated properly after extracting each component all the components will be extracted and added to the entity properly.


I have added some new functions to the Registry & fixed a few bugs

ChangeList

- Added method to remove all components of an entity

- Added default constructor to Entity class

- Fixed a bug in loading the Rigidbody component from the human-readable file.



46 views0 comments

Recent Posts

See All

Comentários


Post: Blog2_Post
bottom of page