Video Coming Soon...
63: Entity Component System
Show how to use templates to make an ECS, and also how ECS does components better than most other ways of doing a component system. This exercise is really about composition and inheritance, but I also need to write about how I was thinking about composition wrong for many years.
The Code
First is a stripped down DinkyECS from my games:
View Source file dinkyecs.hpp Only
#pragma once
#include <any>
#include <functional>
#include <queue>
#include <tuple>
#include <typeindex>
#include <typeinfo>
#include <unordered_map>
#include <optional>
#include <memory>
namespace DinkyECS
{
using Entity = unsigned long;
const Entity NONE = 0;
template <typename T>
struct ComponentStorage {
std::vector<T> data;
};
using EntityMap = std::unordered_map<Entity, size_t>;
using TypeMap = std::unordered_map<std::type_index, std::any>;
struct World {
unsigned long entity_count = NONE+1;
std::unordered_map<std::type_index, EntityMap> $components;
std::unordered_map<std::type_index, std::any> $component_storages;
std::unordered_map<std::type_index, std::queue<size_t>> $free_indices;
Entity entity() { return ++entity_count; }
void destroy(Entity entity) {
for(auto& [tid, map] : $components) {
if(map.contains(entity)) {
size_t index = map.at(entity);
auto& free_queue = $free_indices.at(tid);
free_queue.push(index);
map.erase(entity);
}
}
}
template <typename Comp>
size_t make_component() {
auto &storage = component_storage_for<Comp>();
auto &free_queue = $free_indices.at(std::type_index(typeid(Comp)));
size_t index;
if(!free_queue.empty()) {
index = free_queue.front();
free_queue.pop();
} else {
storage.data.emplace_back();
index = storage.data.size() - 1;
}
return index;
}
template <typename Comp>
ComponentStorage<Comp> &component_storage_for() {
auto type_index = std::type_index(typeid(Comp));
$component_storages.try_emplace(type_index, ComponentStorage<Comp>{});
$free_indices.try_emplace(type_index, std::queue<size_t>{});
return std::any_cast<ComponentStorage<Comp> &>(
$component_storages.at(type_index));
}
template <typename Comp>
EntityMap &entity_map_for() {
return $components[std::type_index(typeid(Comp))];
}
template <typename Comp>
void set(Entity ent, Comp val) {
EntityMap &map = entity_map_for<Comp>();
if(has<Comp>(ent)) {
get<Comp>(ent) = val;
return;
}
map.insert_or_assign(ent, make_component<Comp>());
get<Comp>(ent) = val;
}
template <typename Comp>
void remove(Entity ent) {
EntityMap &map = entity_map_for<Comp>();
if(map.contains(ent)) {
size_t index = map.at(ent);
auto& free_queue = $free_indices.at(std::type_index(typeid(Comp)));
free_queue.push(index);
map.erase(ent);
}
}
template <typename Comp>
Comp &get(Entity ent) {
EntityMap &map = entity_map_for<Comp>();
auto &storage = component_storage_for<Comp>();
auto index = map.at(ent);
return storage.data[index];
}
template <typename Comp>
bool has(Entity ent) {
EntityMap &map = entity_map_for<Comp>();
return map.contains(ent);
}
template <typename Comp>
Comp* get_if(Entity entity) {
EntityMap &map = entity_map_for<Comp>();
auto &storage = component_storage_for<Comp>();
if(map.contains(entity)) {
auto index = map.at(entity);
return &storage.data[index];
} else {
return nullptr;
}
}
template <typename Comp>
void query(std::function<void(Entity, Comp &)> cb) {
EntityMap &map = entity_map_for<Comp>();
for(auto &[entity, index] : map) {
cb(entity, get<Comp>(entity));
}
}
template <typename CompA, typename CompB>
void query(std::function<void(Entity, CompA &, CompB &)> cb) {
EntityMap &map_a = entity_map_for<CompA>();
EntityMap &map_b = entity_map_for<CompB>();
for(auto &[entity, index_a] : map_a) {
if(map_b.contains(entity)) {
cb(entity, get<CompA>(entity), get<CompB>(entity));
}
}
}
};
}
Next is a fuc2 test that demonstrates how it works:
View Source file ex63.cpp Only
#include <fmt/core.h>
#include <chrono>
#include <thread>
#include <string>
#include "dinkyecs.hpp"
#include <fuc2/testing.hpp>
#include <fuc2/run.hpp>
using namespace fuc2;
using namespace DinkyECS;
struct Player {
std::string name;
};
struct Combat {
int damage = 0;
int hp = 0;
};
struct Enemy {
std::string name;
};
void spawn_enemies(World& world, Entity player_id, Entity rat_id) {
// create the player
world.set<Player>(player_id, {"Zed the Destroyer"});
world.set<Combat>(player_id, {10, 100});
// add a rat enemy
world.set<Enemy>(rat_id, {"Rat the Disgusting"});
world.set<Combat>(rat_id, {3, 30});
}
void test_crud_ops() {
World world;
auto player_id = world.entity();
auto rat_id = world.entity();
spawn_enemies(world, player_id, rat_id);
// confirm the player exists
CHECK(world.has<Player>(player_id), "should have player");
CHECK(world.has<Combat>(player_id), "no player combat?");
// check for the rat
CHECK(world.has<Enemy>(rat_id), "missing rat Enemy");
// confirm get_if works
auto player_combat = world.get_if<Combat>(player_id);
CHECK(player_combat != nullptr, "get_if should find player");
CHECK(world.get_if<Enemy>(player_id) == nullptr, "there should _not_ be an enemy at player_id");
// normal get operation
auto& rat_combat = world.get<Combat>(rat_id);
CHECK(rat_combat.hp == 30, "rat is corrupted?");
// confirm remove works
world.remove<Combat>(rat_id);
CHECK(!world.has<Combat>(rat_id));
world.remove<Enemy>(rat_id);
CHECK(!world.has<Enemy>(rat_id));
// make sure player still exists
CHECK(world.has<Combat>(player_id));
CHECK(world.has<Player>(player_id));
}
void test_queries() {
World world;
auto player_id = world.entity();
auto rat_id = world.entity();
spawn_enemies(world, player_id, rat_id);
bool found_player = false;
bool found_rat = false;
world.query<Combat>([&](auto id, auto& combat) {
if(id == player_id) {
found_player = true;
} else if(id == rat_id) {
found_rat = true;
}
CHECK(id == player_id || id == rat_id, "invalid id, not player or rat");
});
world.query<Combat, Player>([&](auto id, auto& combat, auto& player) {
CHECK(id == player_id);
CHECK(player.name == "Zed the Destroyer");
CHECK(combat.hp == 100);
});
}
int main(int argc, char* argv[]) {
return run({
.name="DinkyECS",
.tests={
TEST(test_crud_ops),
TEST(test_queries),
}
}, {}, false);
}
The Breakdown
line of code- Description.
The Discussion
Blah blah.
Further Study
- Do this next.
Register for Learn C++ the Hard Way
Register to gain access to additional videos which demonstrate each exercise. Videos are priced to cover the cost of hosting.