Video Coming Soon...

Created by Zed A. Shaw Updated 2026-06-19 17:00:40

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

Previous Lesson Next Lesson

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.