A Three Parts Tutorial

This tutorial consists of three parts.
You are currently looking at part 3.
If you haven’t done so already, I can recommend you to check out part 2 first.
Don’t worry, part 3 won’t go anywhere.

If you’ve completed part 2 already, be prepared to finish the ECS implementation.

Finalizing Systems

I think it’s time for a little break of some sort.
We’ve come a long way already, but we still need to write most of the level code.
But the systems don’t need a lot of work, so let’s just finish those first.

Back at the beginning of this tutorial we started writing our movement system.
All other systems will actually look a lot like that one.
We just have a function with two parameters which represent a system.
However, instead of taking a player component as parameter, we will take a level as parameter.

We are going to store the systems in a level.
This can be done through function pointers.
That’s why all systems need to have the same parameter.
A function pointer, just like you would expect, points to a function and can be used to call a function (just like we did with destroy_function in the Component class).

Enough talking. Let’s create a type definition which represents a system.
Just below the includes in level.h we can write the following.

typedef struct level _level;
typedef void(*system_t)(_level* level, float delta_time);

You may immediately wonder what _level is for.
It’s a forward declaration for our level struct.
Otherwise we will get errors when trying to store the systems in our level.

zel_system_t is in this case our newly defined type, which we will use for our systems.
If you are familiar with function pointers you will see that this it points to a void function.
That void function only takes a pointer to a level and a float which is the delta time.

With this code in place we can store our systems in the level struct.
We use an unordered map for this, so don’t forget to include it.

#include <unordered_map>

struct level
{
	//systems
	std::unordered_map<std::string, system_t> systems;

	//entities
	std::vector<uint8_t> entities = { 0 };
	std::queue<uint32_t> empty_entities_spots;
};

We use a string to be able to easily register and unregister a system during runtime.
This makes it possible to easily turn off input during runtime for example.
If the player input needs to be ignored for a couple seconds, we just unregister the input system.
Then when those couple seconds have passed, we enable it again by registering it.

For that feature, we need two functions.
Let’s define these at the bottom of the level header file.

void level_register_system(level* level, system_t system_update, const char* system_name);
void level_unregister_system(level* level, const char* system_name);

The code is very simple for registering and unregistering systems.

void level_register_system(level* level, system_t system_update, const char* system_name)
{
	level->systems.insert({ system_name, system_update });
}

void level_unregister_system(level* level, const char* system_name)
{
	level->systems.erase(system_name);
}

That’s all we need for the systems.

With this code in place, we can take a quick look at our movement system.
We will change it a bit so it fits the system_t function pointer.
As well as register and run it in our main code.

#pragma once
#include "player.h"
#include "level.h"

void movement_update(level* level, float delta_time)
{
	printf("moving...\n");

	//player->velocity.x = player->velocity.x + (player->acceleration.x * delta_time);
	//player->velocity.y = player->velocity.y + (player->acceleration.y * delta_time);
	//player->position.x += player->velocity.x * delta_time;
	//player->position.y += player->velocity.y * delta_time;
}

The player code is still commented out, we will fix that later.
But the movement system now takes a level pointer as parameter.
Through the level we will be able to access components and entities later on.

Next thing to change is the main code.

void main()
{
	level* test_level = level_create();
	entity_id player_entity = create_entity(test_level);

	level_register_system(test_level, movement_update, movement_name);

	//We also hard code the delta time for now
	//We aim for 60FPS, so one frame should take
	//1/60th of a second
	float delta_time = 1.0f / 60.0f;

	while (1)
	{
		for (std::pair<std::string, system_t> system : test_level->systems)
		{
			system.second(test_level, delta_time);
		}

		printf("Player's entity ID: %d\n", player_entity);
	}

	destroy_entity(test_level, player_entity);
	level_destroy(test_level);
}

We register the movement system in our test level.
Then we use a for-loop to execute all the systems in our level.

If you run this now, you will see two messages appear in the command prompt every frame.
Indicating that our systems are working!

That’s the end of the “break”.
Back to focusing on the components again.

Managing Components

We are done with the systems, but we still need to do some stuff for the components and entities.
Just as with the systems, the components will be stored inside a level.

However, as mentioned before, we will use the ComponentBase for this instead of the Component class.
We will do it the same way as with the systems.
So we use an unordered_map to store a string and a pointer to the ComponentBase.

Through the level we can then create components.
These will get stored inside a Component class instance specifically for that component type.
But first let’s look at how to store those instances.

struct level
{
	//components
	std::unordered_map<std::string, ComponentBase*> components;

	//systems
	std::unordered_map<std::string, system_t> systems;

	//entities
	std::vector<uint8_t> entities = { 0 };
	std::queue<uint32_t> empty_entities_spots;
};

That’s how our level struct should look now.

When a level gets created there are no ComponentBase instances loaded into the unordered_map.
Therefore we need some functions to do this for us.

There is a catch though.
The Component class is a templated class.
We need to specify for which type we want to create an instance.
That means our function will also be templated and needs to be written inside a header file, level.h.

template <typename T>
void level_register_component(level* level)
{
	std::string type_name = typeid(T).name();

	if (level->components.find(type_name) != level->components.end())
	{
		//The component type is already registered
		return;
	}

	Component<T>* new_component_type = new Component<T>();
	ComponentBase* base_component_type = new_component_type;
	level->components.insert({ type_name, base_component_type });
}

So before using any components inside our level, we need to register a component type through this function.
Just as with the Component class T here means a type of a component.
This could be transform_t or rigidbody_t in our case.

First we will get the name of that type to store it inside the components unordered_map.
Then we do a little check to prevent duplicate instances.

The final step is to create that new instance for the component type we put in.
We can only store the base class though, so we grab a pointer to that base class.
Then we can simply insert that pointer in the unordered_map together with the component type name.

When calling this function the code should look like below.

level_register_component<transform_t>(level);

There also may be instances where some memory need to be freed.
For example when a material uses several textures.
Those textures need to be unloaded from memory.
We can simply use the destroy_function for that scenario.

template <typename T>
void level_register_component_with_destroy(level* level, void(*destroy_function)(T*))
{
	std::string type_name = typeid(T).name();

	if (level->components.find(type_name) != level->components.end())
	{
		//The component type is already registered
		return;
	}

	Component<T>* new_component_type = new Component<T>();
	new_component_type->destroy_function = destroy_function;
	ComponentBase* base_component_type = new_component_type;
	level->components.insert({ type_name, base_component_type });
}

The only difference here is line 13.
However this function clearly communicates that the destroy_function is used and that this component type allocates memory in some way.

Don’t forget to include the header files of both classes at the top of level.h.
As well as typeinfo for the typeid(T) on line 4.

#include <typeinfo>
#include "component_base.h"
#include "component_class.h"

Let’s use our new function right away.
In the main.cpp add the following code just below the create_entity line of the player.
This enables us to use the transform component in our level.

level_register_component<transform_t>(test_level);

When trying to run the code now, we actually find a problem.
It’s a circular dependency.

The level.h header file includes component_base.h.
Then component_base.h needs the header file entities.h.
The problem arises in that entities.h needs the level header file again, level.h.

We will solve it the easy way.
Since everything is done through levels, we will copy the two functions to the level files.

entity_id create_entity(level* level);
void destroy_entity(level* level, entity_id entity);

We cut and past these two lines from entities.h into level.h.
Below level_unregister_system should be fine.

entity_id create_entity(level* level)
{
	if (!level->empty_entities_spots.empty())
	{
		uint32_t empty_spot = level->empty_entities_spots.front();
		level->empty_entities_spots.pop();
		level->entities[empty_spot] += 1;
		entity_id new_entity = CREATE_ID(level->entities[empty_spot], empty_spot);
		return new_entity;
	}

	level->entities.push_back(1);
	return CREATE_ID(1, level->entities.size() - 1);
}

void destroy_entity(level* level, entity_id entity)
{
	uint32_t entity_index = GET_INDEX(entity);
	level->entities[entity_index] += 1;
	level->empty_entities_spots.push(entity_index);
}

Then the implementation can be cut from entities.cpp and be pasted at the end of level.cpp.
That will solve our build error.

Try to run the code and see if you get any errors.
If you do, please look closely again what the error says.
You can always go back and see if you followed along correctly.

Memory Leak

Running our code now actually creates a memory leak.
As we create instances of Component on the heap we need to also free those when destroying a level.
That means we need to properly delete those instances in our level_destroy.

void level_destroy(level* level)
{
	std::unordered_map<std::string, ComponentBase*>::iterator level_components_iterator = level->components.begin();
	while (level_components_iterator != level->components.end())
	{
		level_components_iterator->second->free();
		delete level_components_iterator->second;

		++level_components_iterator;
	}

	delete level;
}

We iterate over every component type registered inside our level.
Then we can properly call the free function which we already added to the class.
That should take care of properly disposing all data inside it.

Adding Components

Cool! We can register components now and therefore we are able to use them.
But how do we actually use them?

In an ECS entities have components attached to them in some way or form.
We already discussed how this is managed.
However now it’s time to actually add a component to an entity.

The following code will all be templated again.
We don’t know which components we may need in the future.
Therefore the code to add a component to an entity also works with templates.

Our next function can be created at the bottom of level.h.

template <typename T>
void level_add_component(level* level, entity_id entity, T component)
{
	std::string type_name = std::string(typeid(T).name());
	Component<T>* component_type = (Component<T>*)(level->components[type_name]);

	component_type->create(entity, component);
}

As you can see this function requires three types of data.
The level, the entity in that level to add the component to and the created component itself.

What happens is that we grab the name of the component type.
With that string we can look in the components unordered_map inside the level.
That gives us the pointer to ComponentBase of that component type.

However we actually need the Component class instance.
Since that is the one storing all components of a particular type.
Luckily we can cast the ComponentBase pointer, since we know for sure it holds the correct instance to the Component class for the type of component we want to add.

So after grabbing the Component instance through the unordered_map, we can create the component.
We already have a function for this.
Just calling create on the Component instance should do the job.

Getting Components

Only adding components isn’t so interesting.
We also want to get them back, so we can use the data.

Our next function should take care of this.
Let’s create it below the previous one.

template <typename T>
T* level_get_component(level* level, entity_id entity)
{
	std::string type_name = std::string(typeid(T).name());
	Component<T>* component_type = (Component<T>*)(level->components[type_name]);

	return component_type->get_component(entity);
}

It may look very familiar.
The code is almost the same as for adding component.

This time we call get_component on the Component instance.
It will return a pointer to the component.
So we can simply do the same.

Checking for Components

There are moments where you only need to know if an entity has a component.
So you don’t actually need the data, you just want to know if it is attached to it.
Therefore we add just one more function to our level.h.

template <typename... T>
bool level_has_components(level* level, entity_id entity)
{
	std::string component_names[] = { "", std::string(typeid(T).name())... };

	uint32_t types_size = sizeof...(T);
	std::vector<ComponentBase*> type_bases;
	for (size_t i = 1; i < types_size + 1; i++)
	{
		type_bases.push_back(level->components[component_names[i]]);
	}

	for (size_t j = 0; j < types_size; j++)
	{
		if (!type_bases[j]->has_component(entity))
			return false;
	}

	return true;
}

This function uses a new concept we haven’t seen yet.
It uses a type template parameter pack.
Which basically means we can input multiple types.
The function will then be able to check if the entity has all those component types attached to it.

So what happens here is we first collect all the names of the component types.
The names are used to grab the pointers to all the ComponentBase instances.
Finally we can call has_component on every ComponentBase instance.

Once we find one component type that isn’t attached to the entity we can return false.
Otherwise we keep going and return true if all component types are attached to the entity.

Destroying Entities

Again we may create memory leaks when not properly destroying components.
This can happen when destroying entities.

We already have a destroy_entity function.
Let’s add some code to it that handles the destruction of components.

void destroy_entity(level* level, entity_id entity)
{
	uint32_t entity_index = GET_INDEX(entity);
	level->entities[entity_index] += 1;
	level->empty_entities_spots.push(entity_index);

	std::unordered_map<std::string, ComponentBase*>::iterator level_components_iterator = level->components.begin();
	while (level_components_iterator != level->components.end())
	{
		level_components_iterator->second->destroy(entity);

		++level_components_iterator;
	}
}

We can iterate over the components and call destroy for the entity.
If the entity doesn’t have a certain component type attached, the destroy function does nothing.

This finally concludes the components side of this implementation.
Before we run this code, we can try to use this code in our movement system.
However running it right now shouldn’t give you any errors.
So feel free to try it.

Finalizing Our Code

At the beginning of this tutorial we created movement code that would move our player.
We are now at a point where we can create entities, add components to it, get components from entities and run our systems.
That may sound as if that’s everything we need to recreate our movement code as a system.
However there is one thing missing.
Actually accessing entities in our systems.

Every system has access to a level which contains entities.
But you probably only want entities with a certain combination of components.
In the case of our movement system we only want the transform components.

The Zel Game Engine uses its own special iterator.
It only gives back the entities that have the components you want.
I won’t explain its iterator here, since I think it is a bit too difficult for now.
We have a simpler option we will use this time.
Yet if you really want to learn more about it you can look at the source code here.

Iterating Over Entities

Our simple solution is to just iterate over the entities.
Then check if they have the required components.
If they do, we can run our system code.

void movement_update(level* level, float delta_time)
{
	std::vector<uint8_t>::iterator entities_iterator = level->entities.begin();
	std::vector<uint8_t>::iterator entities_begin = level->entities.begin();
	while (entities_iterator != level->entities.end())
	{
		uint32_t entity_index = entities_iterator - entities_begin;
		entity_id entity = CREATE_ID((*entities_iterator), entity_index);

		if (entity != 0 && level_has_components<transform_t>(level, entity))
		{
			//player->velocity.x = player->velocity.x + (player->acceleration.x * delta_time);
			//player->velocity.y = player->velocity.y + (player->acceleration.y * delta_time);
			//player->position.x += player->velocity.x * delta_time;
			//player->position.y += player->velocity.y * delta_time;
		}
		entities_iterator++;
	}
}

This is how our movement system will now look.
At this moment nothing will happen and we still have the old code commented out here.

What this code does is it first creates an iterator and store the beginning of the entities vector.
Then it starts iterating over all the entities.
However to be able to get components from entities, we need the entity ID.
But we can create that ID from the index and the stored generation from the entities vector.
Finally we check if the entity is valid and has the components we want, in this case the transform component.

This is now a kind of template for every other system you may create.
The only things you need to change is the component types level_has_components needs to check and the code inside the if-statement.

Time to finally rewrite our movement system.
We need to add the rigidbody component type to the components we want to check for.
That way we can grab all the data we need.

void movement_update(level* level, float delta_time)
{
	std::vector<uint8_t>::iterator entities_iterator = level->entities.begin();
	std::vector<uint8_t>::iterator entities_begin = level->entities.begin();
	while (entities_iterator != level->entities.end())
	{
		uint32_t entity_index = entities_iterator - entities_begin;
		entity_id entity = CREATE_ID((*entities_iterator), entity_index);

		if (entity != 0 && level_has_components<transform_t, rigidbody_t>(level, entity))
		{
			transform_t* entity_transform = level_get_component<transform_t>(level, entity);
			rigidbody_t* entity_rigidbody = level_get_component<rigidbody_t>(level, entity);

			//player->velocity.x = player->velocity.x + (player->acceleration.x * delta_time);
			//player->velocity.y = player->velocity.y + (player->acceleration.y * delta_time);
			//player->position.x += player->velocity.x * delta_time;
			//player->position.y += player->velocity.y * delta_time;
		}
		entities_iterator++;
	}
}

Now we have all the data we need.
It’s only a matter of replacing some words and the code should work again.
So instead of accessing everything through the player, we can access the data with the component pointers.

void movement_update(level* level, float delta_time)
{
	std::vector<uint8_t>::iterator entities_iterator = level->entities.begin();
	std::vector<uint8_t>::iterator entities_begin = level->entities.begin();
	while (entities_iterator != level->entities.end())
	{
		uint32_t entity_index = entities_iterator - entities_begin;
		entity_id entity = CREATE_ID((*entities_iterator), entity_index);

		if (entity != 0 && level_has_components<transform_t, rigidbody_t>(level, entity))
		{
			transform_t* entity_transform = level_get_component<transform_t>(level, entity);
			rigidbody_t* entity_rigidbody = level_get_component<rigidbody_t>(level, entity);

			entity_rigidbody->velocity.x = entity_rigidbody->velocity.x + (entity_rigidbody->acceleration.x * delta_time);
			entity_rigidbody->velocity.y = entity_rigidbody->velocity.y + (entity_rigidbody->acceleration.y * delta_time);
			entity_transform->position.x += entity_rigidbody->velocity.x * delta_time;
			entity_transform->position.y += entity_rigidbody->velocity.y * delta_time;
		}
		entities_iterator++;
	}
}

This is our final movement system code!

Unfortunately we still can’t really use it.
Let’s make our final changes to main.cpp to test our ECS implementation.

	while (1)
	{
		for (std::pair<std::string, system_t> system : test_level->systems)
		{
			system.second(test_level, delta_time);
		}

		transform_t* player_transform = level_get_component<transform_t>(test_level, player_entity);
		printf("Player's position [%d]: %0.2f %0.2f\n", 
			player_entity, 
			player_transform->position.x, 
			player_transform->position.y
		);
	}

To know if the player actually moves we can use a printf as a way of rendering the player.
We just print its entity ID and its position.

Running this code gives us an error.
That’s because the player doesn’t have a transform component attached to it.
It’s also the reason why the movement system isn’t doing anything yet.
So the final step is to create and attach the right component to the player entity.

void main()
{
	level* test_level = level_create();
	entity_id player_entity = create_entity(test_level);

	level_register_component<transform_t>(test_level);
	level_register_component<rigidbody_t>(test_level);

	transform_t transform;
	transform.position.x = 0;
	transform.position.y = 0;

	rigidbody_t rigidbody;
	rigidbody.acceleration.x = 1.0f;
	rigidbody.acceleration.y = 0;
	rigidbody.velocity.x = 0;
	rigidbody.velocity.y = 0;

	level_add_component<transform_t>(test_level, player_entity, transform);
	level_add_component<rigidbody_t>(test_level, player_entity, rigidbody);

	level_register_system(test_level, movement_update, movement_name);

	//We also hard code the delta time for now
	//We aim for 60FPS, so one frame should take
	//1/60th of a second
	float delta_time = 1.0f / 60.0f;

	while (1)
	{
		for (std::pair<std::string, system_t> system : test_level->systems)
		{
			system.second(test_level, delta_time);
		}

		transform_t* player_transform = level_get_component<transform_t>(test_level, player_entity);
		printf("Player's position [%d]: %0.2f %0.2f\n", 
			player_entity, 
			player_transform->position.x, 
			player_transform->position.y
		);
	}

	destroy_entity(test_level, player_entity);
	level_destroy(test_level);
}

This is how our final main code should look.
Notice that we registered the rigidbody component type and the 1.0f for acceleration on the X-axis.

Now it’s time to test out your ECS implementation.
Try to run the code and see how the player’s X position will keep increasing.

You can now use this ECS in your own game or game engine.
I’m very curious to see what you’ll will make with it.

Reflection

It was a long journey, but now you have your very own ECS implementation.
A Sparse ECS that can be used in games or game engines.

Of course there are always things that can be improved.
You can try to implement the entities_list from the Zel Game Engine to improve readability and ease-of-use in your systems.
I encourage you to go through the code and really try to understand what happens.
Looking through other people’s code may also help.
There are multiple ECS types, so enough things to learn and discover.

For myself, I really started to understand the difference between other ECS implementations when I profiled my own ECS.
The Intel VTune Profiler may help with this.
To find out which types of ECS exist or just to learn more about ECS in general, Sander Mertens has created a very good FAQ for you to read.

That’s all I can give you for now.
Good luck utilizing this ECS implementation in your own projects.
Feel free to send me a message when you’ve used it anywhere.
Or if you have any questions you can also contact me or comment below.

All code in this tutorial can be downloaded from the ZelECSTutorial repository.

Sources

These are the sources I used while developing my own ECS implementation and this tutorial. They helped me greatly with understanding how this concept works and how to possibly implement one. Hopefully it helps you as much as it did me.
 
Last modified: June 22, 2021

Comments

Write a Reply or Comment

Your email address will not be published.