An Entity Component System is an architectural design pattern aimed at performing operations within a system in a cache-friendly manner. There is too much to get into with this design pattern, so check out more information on its usage and concept here.
Entity-Component-Systems are designed with performance in mind, especially for real-time applications where speed and efficiency are critical. Incorporating run-time type information (RTTI) introduces unnecessary overhead, increases binary size, and can hinder performance in time-sensitive scenarios. I eliminated RTTI from the framework to maintain a lean, fast, and predictable system. This decision ensures that the ECS remains lightweight and optimized, avoiding runtime costs and enabling better control over memory and execution flow.
A configuration policy is just a configuration for your world. The policy is composed of 5 different template parameters.
- entity type : an integral type (u32, u64) for your entity
- component list : a type list of all the components needed for your world
- tag type : an enumerable list of constants for different entity tags
- world allocator : how the internal storage of this world should be allocated. (defaulted to std::allocator<entity_type>)
- policy tag type : a tag that distinguishes this policy from others (defaulted to snek::snek_main_policy_tag)
//assuming we have this set of components (a, b, c, d)
using component_types = snek::component_list<component_a, component_b, component_c, component_d>;
enum class TagTypes
{
PLAYER,
ENEMY
};
using configuration_policy = snek::world_policy<std::uint64_t, component_types, TagTypes, std::allocator<std::uint64_t>, snek::snek_main_policy_tag>;Since our components are explicitly injected via the policy, we don't have to worry about any runtime overhead with dynamic component types.
The motive of the policy-based design allows for static unified systems based on policy constraints. Snek utilizes multi-world applications, alluding to the separation of concerns of each world. For instance, say we have two unique states of our application, Game state and Menu state. Each state has its world of entities that don't necessarily relate to the other's entities but may implement shared components differently.
snek::world<menu_configuration_policy> menu_world;and
snek::world<game_configuration_policy> game_world;Let's say both states incorporate a particle system, but the game state may incorporate particles relative to rigid bodies.
using game_component_types = snek::component_list<particle, rigidbody>;
using game_configuration_policy = snek::world_policy<std::uint64_t, game_component_types, GameEntityTags>;
using menu_component_types = snek::component_list<particle>;
using menu_configuration_policy = snek::world_policy<std::uint64_t, menu_component_types, MenuEntityTags>;
We can create a system that handles both of these cases through static policy constraints :
template<typename Policy>
class ParticleSystem {
snek::world<Policy>& particle_world;
void update_particle_with_rigidbody() {
std::cout << "updating particles relative to rigidbody" << std::endl;
};
void update_menu_particles() {
std::cout << "updating particles in menu" << std::endl;
};
public:
ParticleSystem(snek::world<Policy>& w) : particle_world(w) {};
void update(float dt) {
//game policy constraint
if constexpr(Policy::is_valid_component_set<particle, rigidbody>()) {
update_particle_with_rigidbody();
}
//constraint menu policy constraint
if constexpr(Policy::is_valid_component<particle>()) {
update_menu_particles();
}
}
~ParicleSystem() {};
}However, there is one problem: what if both worlds share the same components...?
The world policy takes a policy tag as its final template argument (defaulted to snek::snek_main_policy_tag). This argument must derive from snek::policy_tag
struct game_policy_tag : public snek::policy_tag {};
struct menu_policy_tag : public snek::policy_tag {};and may be used to distinguish different policies in our unified system.
if constexpr(Policy::is_valid_component_set<particle, rigidbody>() && Policy::is_tagged<menu_policy_tag>()) {
//handle menu particle system
}
if constexpr(Policy::is_valid_component_set<particle, rigidbody>() && Policy::is_tagged<game_policy_tag>()) {
//handle game particle system
} #include "snakeecs.hpp"
struct A
{
int x;
A(int x) : x(x) {};
~A() {};
};
struct B
{
char x;
B(char x) : x(x) {};
~B() {};
};
enum class TagTypes
{
PLAYER,
ENEMY
};
using component_types = snek::component_list<A, B>;
using configuration_policy = snek::world_policy<std::uint64_t, component_types, TagTypes>;
// systematic view
void update(snek::world<configuration_policy>& w) {
auto view = w.view<A, B>();
view.for_each([](const A& a, const B& b) {
int ax = a.x;
char bx = b.x;
std::cout << ax << " : " << bx << std::endl;
});
}
int main(int argc, char** argv) {
/*
Let's begin by instantiating a world for our ECS.
Remember the configuration policy we created (See above)?
Let's inject it into our world.
*/
snek::world<configuration_policy> w;
for(int i = 0; i < 10; i++) {
auto e = w.spawn();
//bind one component
w.bind<A>(e, 5);
}
//or initialize
for(int i = 0; i < 10; i++) {
auto e = w.spawn();
//initialize multiple components
w.initialize<A, B>(e, 5, 'B');
}
return 0;
}In SnakeECS, entities are represented via either a 32-bit or 64-bit number. These numbers represent two common ideas.
- Indexing
- Versioning
The lower 8 bits of each entity ID are reserved strictly for versioning. This means each entity has a max version of 255, this version does not wrap to 0. 255 will be the last version of the entity, no matter what. The remaining higher bits (depending on whether 32-bit or 64-bit) are reserved strictly for indexing the entity. For example, when you call,
auto entity = world.spawn();
//if entity = 1 its representation (assuming 32-bit) -> 0000 0000 0000 0000 0000 0001 0000 0000
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ : ^^^^^^^^^
// index representation version representationAn index is returned to the caller. SnakeECS bases all API calls on indexing rather than arbitrary IDs. The library handles IDs internally. To retrieve the version of an entity, you may call :
auto version = world.to_version(entity);versions are incremented on each "removal".
world.kill(entity); // increments versionThis project uses CMake (Version 3.28) with the ISO C++20 Standard. To run existing tests, from the root folder, input these commands if not done already.
mkdir build
cd build
cmake -G "<Preferred-Generator>" ..
cmake --build .
./SnakeECSI'd like to thank the following developers for their inspiration for different subsets of this project...