feat(ecs): add convenient Entity class

This commit is contained in:
light7734 2025-09-22 18:50:59 +03:30
parent a58b0c030f
commit b6834310a7
Signed by: light7734
GPG key ID: 8C30176798F1A6BA
4 changed files with 96 additions and 43 deletions

View file

@ -12,7 +12,7 @@ using lt::test::expect_eq;
using lt::test::expect_false; using lt::test::expect_false;
using lt::test::expect_true; using lt::test::expect_true;
using lt::ecs::Entity; using lt::ecs::EntityId;
using lt::ecs::Registry; using lt::ecs::Registry;
struct Component struct Component
@ -86,7 +86,7 @@ Suite raii = [] {
Suite entity_raii = [] { Suite entity_raii = [] {
Case { "create_entity returns unique values" } = [] { Case { "create_entity returns unique values" } = [] {
auto registry = Registry {}; auto registry = Registry {};
auto set = std::unordered_set<Entity> {}; auto set = std::unordered_set<EntityId> {};
for (auto idx : std::views::iota(0, 10'000)) for (auto idx : std::views::iota(0, 10'000))
{ {
@ -101,7 +101,7 @@ Suite entity_raii = [] {
Case { "post create/destroy_entity has correct state" } = [] { Case { "post create/destroy_entity has correct state" } = [] {
auto registry = Registry {}; auto registry = Registry {};
auto entities = std::vector<Entity> {}; auto entities = std::vector<EntityId> {};
for (auto idx : std::views::iota(0, 10'000)) for (auto idx : std::views::iota(0, 10'000))
{ {
entities.emplace_back(registry.create_entity()); entities.emplace_back(registry.create_entity());
@ -154,14 +154,18 @@ Suite component_raii = [] {
Suite callbacks = [] { Suite callbacks = [] {
Case { "connecting on_construct/on_destruct won't throw" } = [] { Case { "connecting on_construct/on_destruct won't throw" } = [] {
auto registry = Registry {}; auto registry = Registry {};
registry.connect_on_construct<Component>([&](Registry &, Entity) {}); registry.connect_on_construct<Component>([&](Registry &, EntityId) {});
registry.connect_on_destruct<Component>([&](Registry &, Entity) {}); registry.connect_on_destruct<Component>([&](Registry &, EntityId) {});
}; };
Case { "on_construct/on_destruct won't get called on unrelated component" } = [] { Case { "on_construct/on_destruct won't get called on unrelated component" } = [] {
auto registry = Registry {}; auto registry = Registry {};
registry.connect_on_construct<Component>([&](Registry &, Entity) { expect_unreachable(); }); registry.connect_on_construct<Component>([&](Registry &, EntityId) {
registry.connect_on_destruct<Component>([&](Registry &, Entity) { expect_unreachable(); }); expect_unreachable();
});
registry.connect_on_destruct<Component>([&](Registry &, EntityId) {
expect_unreachable();
});
for (auto idx : std::views::iota(0, 100'000)) for (auto idx : std::views::iota(0, 100'000))
{ {
@ -171,14 +175,14 @@ Suite callbacks = [] {
Case { "on_construct/on_destruct gets called" } = [] { Case { "on_construct/on_destruct gets called" } = [] {
auto registry = Registry {}; auto registry = Registry {};
auto all_entities = std::vector<Entity> {}; auto all_entities = std::vector<EntityId> {};
auto on_construct_called = std::vector<Entity> {}; auto on_construct_called = std::vector<EntityId> {};
auto on_destruct_called = std::vector<Entity> {}; auto on_destruct_called = std::vector<EntityId> {};
registry.connect_on_construct<Component>([&](Registry &, Entity entity) { registry.connect_on_construct<Component>([&](Registry &, EntityId entity) {
on_construct_called.emplace_back(entity); on_construct_called.emplace_back(entity);
}); });
registry.connect_on_destruct<Component>([&](Registry &, Entity entity) { registry.connect_on_destruct<Component>([&](Registry &, EntityId entity) {
on_destruct_called.emplace_back(entity); on_destruct_called.emplace_back(entity);
}); });
@ -206,8 +210,8 @@ Suite each = [] {
auto shared_entity_counter = 0u; auto shared_entity_counter = 0u;
auto component_map_a = std::unordered_map<Entity, Component> {}; auto component_map_a = std::unordered_map<EntityId, Component> {};
auto entities_a = std::vector<Entity> {}; auto entities_a = std::vector<EntityId> {};
for (auto idx : std::views::iota(0, 10'000)) for (auto idx : std::views::iota(0, 10'000))
{ {
@ -220,10 +224,10 @@ Suite each = [] {
component_map_a[entity] = component; component_map_a[entity] = component;
} }
auto component_map_b = std::unordered_map<lt::ecs::Entity, Component_B> {}; auto component_map_b = std::unordered_map<lt::ecs::EntityId, Component_B> {};
for (auto idx : std::views::iota(0, 10'000)) for (auto idx : std::views::iota(0, 10'000))
{ {
auto entity = Entity {}; auto entity = EntityId {};
if (idx % 3 == 0) if (idx % 3 == 0)
{ {
entity = entities_a[idx]; entity = entities_a[idx];
@ -243,7 +247,7 @@ Suite each = [] {
Case { "each one element" } = [&] { Case { "each one element" } = [&] {
auto counter = 0u; auto counter = 0u;
registry.each<Component>([&](Entity entity, Component &component) { registry.each<Component>([&](EntityId entity, Component &component) {
++counter; ++counter;
expect_eq(component_map_a[entity], component); expect_eq(component_map_a[entity], component);
}); });
@ -251,7 +255,7 @@ Suite each = [] {
expect_eq(component_map_a.size(), counter); expect_eq(component_map_a.size(), counter);
counter = 0u; counter = 0u;
registry.each<Component_B>([&](Entity entity, Component_B &component) { registry.each<Component_B>([&](EntityId entity, Component_B &component) {
++counter; ++counter;
expect_eq(component_map_b[entity], component); expect_eq(component_map_b[entity], component);
}); });
@ -261,7 +265,7 @@ Suite each = [] {
Case { "each two element" } = [&] { Case { "each two element" } = [&] {
auto counter = 0u; auto counter = 0u;
registry.each<Component, Component_B>( registry.each<Component, Component_B>(
[&](Entity entity, Component &component_a, Component_B &component_b) { [&](EntityId entity, Component &component_a, Component_B &component_b) {
expect_eq(component_map_a[entity], component_a); expect_eq(component_map_a[entity], component_a);
expect_eq(component_map_b[entity], component_b); expect_eq(component_map_b[entity], component_b);
++counter; ++counter;
@ -277,8 +281,8 @@ Suite views = [] {
auto shared_entity_counter = 0u; auto shared_entity_counter = 0u;
auto component_map_a = std::unordered_map<Entity, Component> {}; auto component_map_a = std::unordered_map<EntityId, Component> {};
auto entities_a = std::vector<Entity> {}; auto entities_a = std::vector<EntityId> {};
for (auto idx : std::views::iota(0, 10'000)) for (auto idx : std::views::iota(0, 10'000))
{ {
@ -291,10 +295,10 @@ Suite views = [] {
component_map_a[entity] = component; component_map_a[entity] = component;
} }
auto component_map_b = std::unordered_map<Entity, Component_B> {}; auto component_map_b = std::unordered_map<EntityId, Component_B> {};
for (auto idx : std::views::iota(0, 10'000)) for (auto idx : std::views::iota(0, 10'000))
{ {
auto entity = Entity {}; auto entity = EntityId {};
if (idx % 3 == 0) if (idx % 3 == 0)
{ {
entity = entities_a[idx]; entity = entities_a[idx];

View file

@ -1,5 +1,42 @@
#pragma once #pragma once
namespace lt { #include <ecs/registry.hpp>
} // namespace lt namespace lt::ecs {
/** High-level entity convenience wrapper */
class Entity
{
public:
Entity(Ref<Registry> registry, EntityId identifier)
: m_registry(std::move(registry))
, m_identifier(identifier)
{
ensure(m_registry, "Failed to create Entity ({}): null registry", m_identifier);
}
template<typename Component_T>
auto get() -> Component_T &
{
return m_registry->get<Component_T>(m_identifier);
}
template<typename Component_T>
auto get() const -> const Component_T &
{
return m_registry->get<Component_T>(m_identifier);
}
auto get_registry() -> Ref<Registry>
{
return m_registry;
}
private:
Ref<Registry> m_registry;
EntityId m_identifier;
};
} // namespace lt::ecs

View file

@ -4,9 +4,9 @@
namespace lt::ecs { namespace lt::ecs {
using Entity = uint32_t; using EntityId = uint32_t;
constexpr auto null_entity = std::numeric_limits<Entity>::max(); constexpr auto null_entity = std::numeric_limits<EntityId>::max();
/** A registry of components, the heart of an ECS architecture. /** A registry of components, the heart of an ECS architecture.
* *
@ -24,9 +24,9 @@ constexpr auto null_entity = std::numeric_limits<Entity>::max();
class Registry class Registry
{ {
public: public:
using UnderlyingSparseSet_T = TypeErasedSparseSet<Entity>; using UnderlyingSparseSet_T = TypeErasedSparseSet<EntityId>;
using Callback_T = std::function<void(Registry &, Entity)>; using Callback_T = std::function<void(Registry &, EntityId)>;
template<typename Component_T> template<typename Component_T>
void connect_on_construct(Callback_T callback) void connect_on_construct(Callback_T callback)
@ -52,13 +52,13 @@ public:
m_on_destruct_hooks.erase(get_type_id<Component_T>()); m_on_destruct_hooks.erase(get_type_id<Component_T>());
} }
auto create_entity() -> Entity auto create_entity() -> EntityId
{ {
++m_entity_count; ++m_entity_count;
return m_current++; return m_current++;
} }
void destroy_entity(Entity entity) void destroy_entity(EntityId entity)
{ {
for (const auto &[key, set] : m_sparsed_sets) for (const auto &[key, set] : m_sparsed_sets)
{ {
@ -69,14 +69,21 @@ public:
} }
template<typename Component_T> template<typename Component_T>
auto get(Entity entity) -> Component_T & auto get(EntityId entity) const -> const Component_T &
{ {
auto &derived_set = get_derived_set<Component_T>(); auto &derived_set = get_derived_set<Component_T>();
return derived_set.at(entity).second; return derived_set.at(entity).second;
} }
template<typename Component_T> template<typename Component_T>
auto add(Entity entity, Component_T component) -> Component_T & auto get(EntityId entity) -> Component_T &
{
auto &derived_set = get_derived_set<Component_T>();
return derived_set.at(entity).second;
}
template<typename Component_T>
auto add(EntityId entity, Component_T component) -> Component_T &
{ {
auto &derived_set = get_derived_set<Component_T>(); auto &derived_set = get_derived_set<Component_T>();
auto &added_component = derived_set.insert(entity, std::move(component)).second; auto &added_component = derived_set.insert(entity, std::move(component)).second;
@ -90,7 +97,7 @@ public:
}; };
template<typename Component_T> template<typename Component_T>
void remove(Entity entity) void remove(EntityId entity)
{ {
if (m_on_destruct_hooks.contains(get_type_id<Component_T>())) if (m_on_destruct_hooks.contains(get_type_id<Component_T>()))
{ {
@ -102,18 +109,18 @@ public:
} }
template<typename Component_T> template<typename Component_T>
auto view() -> SparseSet<Component_T, Entity> & auto view() -> SparseSet<Component_T, EntityId> &
{ {
return get_derived_set<Component_T>(); return get_derived_set<Component_T>();
}; };
template<typename ComponentA_T, typename ComponentB_T> template<typename ComponentA_T, typename ComponentB_T>
requires(!std::is_same_v<ComponentA_T, ComponentB_T>) requires(!std::is_same_v<ComponentA_T, ComponentB_T>)
auto view() -> std::vector<std::tuple<Entity, ComponentA_T &, ComponentB_T &>> auto view() -> std::vector<std::tuple<EntityId, ComponentA_T &, ComponentB_T &>>
{ {
auto &set_a = get_derived_set<ComponentA_T>(); auto &set_a = get_derived_set<ComponentA_T>();
auto &set_b = get_derived_set<ComponentB_T>(); auto &set_b = get_derived_set<ComponentB_T>();
auto view = std::vector<std::tuple<Entity, ComponentA_T &, ComponentB_T &>> {}; auto view = std::vector<std::tuple<EntityId, ComponentA_T &, ComponentB_T &>> {};
/* iterate over the "smaller" component-set, and check if its entities have the other /* iterate over the "smaller" component-set, and check if its entities have the other
* component */ * component */
@ -142,7 +149,7 @@ public:
}; };
template<typename Component_T> template<typename Component_T>
void each(std::function<void(Entity, Component_T &)> functor) void each(std::function<void(EntityId, Component_T &)> functor)
{ {
for (auto &[entity, component] : get_derived_set<Component_T>()) for (auto &[entity, component] : get_derived_set<Component_T>())
{ {
@ -152,7 +159,7 @@ public:
template<typename ComponentA_T, typename ComponentB_T> template<typename ComponentA_T, typename ComponentB_T>
requires(!std::is_same_v<ComponentA_T, ComponentB_T>) requires(!std::is_same_v<ComponentA_T, ComponentB_T>)
void each(std::function<void(Entity, ComponentA_T &, ComponentB_T &)> functor) void each(std::function<void(EntityId, ComponentA_T &, ComponentB_T &)> functor)
{ {
auto &set_a = get_derived_set<ComponentA_T>(); auto &set_a = get_derived_set<ComponentA_T>();
auto &set_b = get_derived_set<ComponentB_T>(); auto &set_b = get_derived_set<ComponentB_T>();
@ -223,22 +230,22 @@ private:
} }
template<typename T> template<typename T>
auto get_derived_set() -> SparseSet<T, Entity> & auto get_derived_set() -> SparseSet<T, EntityId> &
{ {
constexpr auto type_id = get_type_id<T>(); constexpr auto type_id = get_type_id<T>();
if (!m_sparsed_sets.contains(type_id)) if (!m_sparsed_sets.contains(type_id))
{ {
m_sparsed_sets[type_id] = create_scope<SparseSet<T, Entity>>(); m_sparsed_sets[type_id] = create_scope<SparseSet<T, EntityId>>();
} }
auto *base_set = m_sparsed_sets[type_id].get(); auto *base_set = m_sparsed_sets[type_id].get();
auto *derived_set = dynamic_cast<SparseSet<T, Entity> *>(base_set); auto *derived_set = dynamic_cast<SparseSet<T, EntityId> *>(base_set);
ensure(derived_set, "Failed to downcast to derived set"); ensure(derived_set, "Failed to downcast to derived set");
return *derived_set; return *derived_set;
} }
Entity m_current; EntityId m_current;
TypeId m_entity_count; TypeId m_entity_count;

View file

@ -128,6 +128,11 @@ public:
return m_dense.end(); return m_dense.end();
} }
[[nodiscard]] auto at(Identifier_T identifier) const -> const Dense_T &
{
return m_dense.at(m_sparse.at(identifier));
}
[[nodiscard]] auto at(Identifier_T identifier) -> Dense_T & [[nodiscard]] auto at(Identifier_T identifier) -> Dense_T &
{ {
return m_dense.at(m_sparse.at(identifier)); return m_dense.at(m_sparse.at(identifier));