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 }

" , 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.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 #include <chrono> #include <limits> #include <type_traits> #include <unordered_map> #include <vector> #include <cassert> #include <cstdint> #include <cstdio> const bool twin_peaks_is_perfection = true; /* Detector for beautiful SFINAE */ 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 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 ; 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 = {}; 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 }

" , entity.id, _transform -> pos.x, _transform -> pos.y, _transform -> pos.z); } Component < Transform > _transform; const float speed = 1000.f ; }; std :: chrono :: high_resolution_clock :: time_point new_frame_time = std :: chrono :: high_resolution_clock :: now(); inline float get_dt () { auto last_frame_t = new_frame_time; new_frame_time = std :: chrono :: high_resolution_clock :: now(); std :: chrono :: duration < float > dt_duration = new_frame_time - last_frame_t; return dt_duration.count(); } 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) { float dt = get_dt(); Component < MegaSonicAirplane >:: update_components(dt); es[ 0 ].get_component < MegaSonicAirplane > () -> mega_render(); printf( "dt : %f

" , dt); } return 0 ; }

Enjoy o/