While working on game projects 4 and 5, I made a component system that we were using.
This component system was easy to work with and translated very well to how Unity works,
which was useful as we used Unity as a level editor. but had some limitations I wanted to solve
going into Project 6 when we were building our own game engine.
Some limitations I wanted to solve included:
Entity component systems seemed to be the most appropriate solution to these concerns. They work similarly to component systems in the way that behavior is defined at runtime with components. But instead of defining behavior with virtual functions, you iterate through a whole array of components in a system to do the work you want.
Entity component systems offer several benefits over other models, such as object-oriented design. One main benefit is the concept of composition over inheritance. Instead of having to keep track of a potentially deep and complex inheritance hierarchy, which could lead to expensive refactors, it is possible to separate behavior into components, which can be removed from and added to an entity at any time.
Another main benefit is performance. Imagine a scenario where you want to iterate over all objects and move them according to some velocity. If all data related to the position of an object lived in contiguous memory, you could make much better use of the CPU cache and your memory bandwidth.
Example memory layout in a data-oriented game
class Foo { Vector3 position; Vector3 rotation; Vector3 scale; };
When fetching the memory for the position in class Bar, a cache-line's worth of memory would also get loaded instead of the position of the next object we wanted to manipulate. terribly wasteful.
Example memory layout in an object-oriented game
class Bar { Vector3 position; Vector3 rotation; Vector3 scale; // unrelated data Color color; Texture texture; Model model; float health; float damage; ... more };
I was unsure how I wanted the interface to look, so I decided to look around at existing entity component systems, and after some research, I found entt. Since I really liked the interface and features entt provided and thought it would be a good challenge, I set out to replicate its features as best I could.
using Entity = uint32_t;
constexpr Entity nullentity = 0xffffffff;
Entity Create()
{
return (myFreeEntities.Size()) ? myFreeEntities.Dequeue() : myNext++;
}
void Destroy(Entity aEntity)
{
for (uint32_t i = 0; i < myContainers.Size(); ++i)
myContainers[i]->Destroy(aEntity);
myEntityQueue.Enqueue(aEntity);
}
Entities are implemented as a simple ID, destroyed entities are stored in a binary heap, making creating a new entity O(log N), where N is the number of previously destroyed entities. This is done to ensure we always use the smallest possible entity IDs we can.
Components are any type you want. They are stored internally in a templated class called Container. Container always keep components aligned in an internal array, this is done by using a sparse and dense array that maps entity IDs to the dense component array.
template <typename... Args>
T& Emplace(Entity aEntity, Args&&... args)
{
assert(!Contains(aEntity) && "Component already exists");
Grow(aEntity);
myDense[size] = aEntity;
mySparse[aEntity] = size++;
myComponents.emplace_back(std::forward<Args>(args)...);
return myComponents[size-1];
}
void Remove(Entity aEntity)
{
assert(Contains(aEntity) && "Removing nonexisting Component");
Entity denseIndex = sparse[aEntity];
--size;
std::swap(myComponents.back(), myComponents[denseIndex]);
std::swap(myDense[size], myDense[denseIndex]);
mySparse[myDense[denseIndex]] = denseIndex;
auto tmp = std::move(mirror.back());
myComponents.pop_back();
}
In entt there is a concept of a view, this was probably one of the trickier features to implement. Views are a way to specify a variadic list of component types and iterate through all entities that own that set of components. Views can also be given a variadic list of excludes that stops the view from returning any entity that has any component that is excluded.
template <typename T1, typename... Types, typename... Excludes>
TypeView<TList<T1, Types...>, TList<Excludes...>> View(ecs::Exclude<Excludes...>)
{
auto c = GetContainer<T1>();
return { std::make_tuple(c, GetContainer<Types>()...), std::make_tuple(GetContainer<Excludes>()...) };
}
The view's iterator works by finding the smallest container in the list, iterating that container's entities, and testing the other containers in the list to see if they contain the current entity too. If they do, we can return the entity as a valid entity.
Below is an example of how a view is used in our engine to couple physx colliders with our transforms. This example showcases the second iterator that uses structured bindings with an std::tuple
auto view = myRegistry.View<mys::Transform, mys::Collider>(ecs::Exclude<mys::Rigidbody>());
for (auto&& [entity, transform, collider] : view.Each()) // using structured bindings
{
if(!collider.IsSleeping())
collider.SetTransform(transform);
}
I'm overall very pleased with how the system turned out and has been used to a great extent in our engine. There are a lot of features and details omitted here but i have included the source code for those interested.
Source