In the past months, I’ve rewritten the entity-component system of my engine pet project about three times. Finally, something that ticks all the boxes has emerged. Today, I’d like to present this architecture. So far it has worked wonders for me, though I wouldn’t guarantee this to scale up to AAA sized projects. I still have much testing to do.
The goal of this entity-component system is focused on gameplay programmers and their mental well-being. I want a system that is extremely fast to code with, extremely fast to prototype with and that lets you create small games for events like the One Hour Game Jam. Yes that’s a thing. At minimum, the system should be as easy to use as Unreal’s or Unity’s entity-component system. I also want it to be data-oriented and cache-friendly. That is a problem.
Note, The system requires some C++17 features. It works on Visual Studio 2017 and Apple Clang.
User Requirements
In this blog post, the user is a gameplay programmer.
- The user writes a component as if it was 1 object (like other popular engines).
- You can store a reference/pointer to a single component in your class. It persists engine events.
- No more work has to be done than in Unreal or Unity to create a new component.
Entity-Component Requirements
- Contiguous component data (ie. the challenge).
- Provide get_component, add_component, kill_component methods on individual components.
Well that’s not such a big list. It turns out it’s quite simple to achieve with a nifty little trick.
The Basic Idea
The challenging part of such an architecture is storing a component itself, and not a pointer to it. The engine needs to call predefined potentially implemented member functions. I refer to these as events, because of how the ended up being implemented. Usually you’d have some simple virtual methods, the user would implement as required, and everyone would be happy. This is traditional polymorphism, which will trash your cache and as such, we will shun and ignore such heresies for our performant code path ;)
One of my failed experiments involved a main ComponentManager tuple and **a ton **of SFINAE and macros. The result actually worked but was quite unreadable and hard to debug. I didn’t find this to be such a great solution, but feel free to investigate such an architecture. It works.
What changed everything is the curiously recurring template pattern (CRTP) idiom. At that moment, something in my mind unlocked (and I evolved to super-sayan mode etc). The problem fixed itself. If you know what CRTP is, then you’ve probably figured out where I’m going with this.
CRTP To The Rescue
So, I want to store an actual object (not a pointer), but I also want the user to create it as easily as he would’ve with a traditional polymorphic solution. Here is the magic.
struct MyAwesomeComponent : public Component<MyAwesomeComponent> {
};
We are inheriting a Component base class, but we are also providing our new class as a template parameter to it. That way, we can interact with the “true” T. As a bonus, we can transparently use SFINAE to check whether to call an engine event or not on the user class. Another plus is we can provide some helpful methods to the user component, since it inherits the base Component. It does get a little tricky to remember what is what when writing the base class though.
Ultimately, this solves our problem (and many others) as we can store a static vector<T>
in our base class. This guarantees the data is contiguous.
Potential Issues
- If you do not like CRTP, well. What can I say? ¯_(ツ)_/¯
- If you are working in dynamic libraries or other systems were you cannot simply use static data members. There may be a way to hack this system to make it work, though you will loose some precious simplicity.
- Template “explosion” is a real issue. For small to medium games it should be reasonable, but your compiling may slow down to a halt on big teams. I’d love to hear ideas on how to improve upon this.
The Entity
Before we dig into the Component class, let’s write a simple Entity class. In this post, we will make a tiny example system with an init
and an update
event. We’ll implement a Transform component and a MegaSonicAirplane component. This code is for demonstration purposes only, and is most definitely not production ready.
template <class T>
struct Component;
struct Entity {
Entity()
: id(_id_count++) // For demo only. Id == position in component
// buffer.
{}
template <class T>
Component<T> add_component() {
static_assert(std::is_base_of<Component<T>, T>::value,
"Your component needs to inherit Component<>.");
/* Don't allow duplicate components. */
if (auto ret = get_component<T>())
return ret;
return Component<T>::add_component(*this);
}
template <class T>
Component<T> get_component() {
static_assert(std::is_base_of<Component<T>, T>::value,
"Components must inherit Component<>.");
return Component<T>{ *this };
}
uint32_t id;
static const Entity dummy;
private:
Entity(uint32_t id_)
: id(id_) {
}
static uint32_t _id_count;
};
const Entity Entity::dummy{ std::numeric_limits<uint32_t>::max() };
uint32_t Entity::_id_count = 0;
Our demo Entity is a simple 32 bit unsigned int. For simplicities sake, we increment it every time we create a new entity. The entity class provides an invalid dummy entity. This is required so our user can use Component “smart references” without having to initialize them.
The Entity and Component classes are tightly coupled. The Entity forwards Component messages to the appropriate Component Managers. It is the “glue” which makes the whole system work. Getting a new Component is really simple, as validation is done at a later time. We simply construct a new Component “smart ref” and return that.
SFINAE Ground Work
A little SFINAE has never hurt anyone… Or has it? I promise this is cleaner than my last post on the subject!
/* Beautiful SFINAE detector, <3 Walter Brown */
namespace detail {
template <template <typename> typename Op, typename T, typename = void>
struct is_detected : std::false_type {};
template <template <typename> typename Op, typename T>
struct is_detected<Op, T, std::void_t<Op<T>>> : std::true_type {};
} // namespace detail
template <template <typename> typename Op, typename T>
static constexpr bool is_detected_v = detail::is_detected<Op, T>::value;
/* Engine provided member function "look ups". */
namespace detail {
template <class U>
using has_init = decltype(std::declval<U>().init());
template <class U>
using has_update = decltype(std::declval<U>().update(std::declval<float>()));
} // namespace detail
First, I use a simplified version of an upcoming proposal, is_detected
. This is the most elegant way to use SFINAE and doesn’t require macros! For more information, see the cppreference entry or Marshall Clow’s talk on the subject.
Next, we define template aliases to look for our desired engine “events”. Namely, the init()
function and the update(float)
function. This system is extremely flexible and future proof, it makes adding new “events” quite simple.
The Component
At this point, we are ready for the Component
class. The inner-workings are explained below.
template <class T>
struct Component {
Component(Entity e = Entity::dummy)
: entity(e) {
}
static void* operator new(size_t) = delete;
static void* operator new[](size_t) = delete;
operator bool() const {
return entity.id < _components.size();
}
T* operator->() const {
assert(*this == true && "Component doesn't exist.");
return &_components[entity.id];
}
template <class U>
Component<U> add_component() {
static_assert(std::is_base_of<Component<U>, U>::value,
"Components must inherit Component<>.");
return entity.add_component<U>();
}
template <class U>
Component<U> get_component() {
static_assert(std::is_base_of<Component<U>, U>::value,
"Components must inherit Component<>.");
return Component<U>{ entity };
}
static Component<T> add_component(Entity e) {
// printf("Constructing %s Component. Entity : %u",
// typeid(T).name(), e.id);
T t;
t.entity = e;
_components.emplace_back(std::move(t));
if constexpr (is_detected_v<detail::has_init, T>) {
_components.back().init();
}
return Component<T>{ e };
}
static void update_components(float dt) {
if constexpr (is_detected_v<detail::has_update, T>) {
for (size_t i = 0; i < _components.size(); ++i) {
_components[i].update(dt);
}
}
}
protected:
Entity entity;
private:
static std::vector<T> _components;
};
template <class T>
std::vector<T> Component<T>::_components = {};
The Component class acts as both a “smart reference” for the component itself and as a Component Manager.
The user interfaces the smart ref: when using operator->()
, the object will search in our contiguous data vector and return a pointer to the appropriate data. A bool()
operator is provided to streamline the gameplay programmers code. Currently, it is up to the user to check whether the component reference is still valid, though I’m undecided if I like this or not.
Get_component
and add_component
member functions have a few benefits. You can easily get a Component attached on the same Entity as yourself. Or just as easily get a Component attached to another Component (aka what Unity does).
The Component constructor requires an Entity, we provide a default dummy value. This was added so a user can easily add Components to his class definition. The new
operators are deleted for good measure. Init
will be called if provided by the user on component creation.
The engine will interface with the Component Manager, it is the static portion of the Component. The Entity uses the static add_component
for example. Here the events will be called manually (update_components
). SFINAE is used to choose whether or not to execute the event if a user Component provides it. No cache misses. No overhead.
Finally, we have the static vector
, which is the “core” of our system. There isn’t much to it. In a real world use case, you’d want a lookup table of sorts to index into the vector. Every frame, Components should be sorted in 2 groups; enabled and disabled.
Example
Whew! That was a mouth-full. Seeing the system in action will probably help understand what is going on. Here is the simplest Transform ever written, and a damn fast plane Component.
struct Transform : public Component<Transform> {
struct vec3 {
float x = 0.f;
float y = 0.f;
float z = 0.f;
};
vec3 pos;
};
struct MegaSonicAirplane : public Component<MegaSonicAirplane> {
void init() {
_transform = add_component<Transform>();
}
void update(float dt) {
_transform->pos.y += speed * dt;
/* Another option for the user : */
// auto t = get_component<Transform>();
// t->pos.y += speed * dt;
}
void mega_render() {
printf("MegaSonicAirplane %u : { %f, %f, %f }\n", entity.id,
_transform->pos.x, _transform->pos.y, _transform->pos.z);
}
Component<Transform> _transform;
const float speed = 1000.f;
};
This all seems quite sane and readable to me. All the previous requirements have been respected. Lets launch a few airplanes to celebrate! They’re really just going upwards anyway, like fireworks. 5'000'000 should do it…
const bool twin_peaks_is_perfection = true;
int main(int, char**) {
std::vector<Entity> es;
es.reserve(5'000'000);
for (int i = 0; i < 5'000'000; ++i) {
es.push_back(Entity());
es.back().add_component<MegaSonicAirplane>();
}
while (twin_peaks_is_perfection) {
Component<MegaSonicAirplane>::update_components(dt);
es[0].get_component<MegaSonicAirplane>()->mega_render();
}
return 0;
}
And that’s it for the core system. We have arrived (somewhat) safely to destination. The weather is a cool breeze and sunny day. Thank you for travelling on contiguous data airlines…
Where To Go From Here
Personally, I am working on multi-threading the whole system. Even though it doesn’t make much sense for small games, I think it’ll be an interesting experiment. There is also more work required for the scene graph, which has a tendency to break data-contiguous systems by its nature. Finally, a proxy data structure used to store the components as Structure of Arrays is definitely on the horizon.
I want to extend a huge thanks to Alex, Francis and Houssem for the constant brainstorming and discussions about game engine architectures.
Full Code
Yes, with includes.
|
|
Enjoy o/