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

View file

@ -1,5 +1,42 @@
#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 {
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.
*
@ -24,9 +24,9 @@ constexpr auto null_entity = std::numeric_limits<Entity>::max();
class Registry
{
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>
void connect_on_construct(Callback_T callback)
@ -52,13 +52,13 @@ public:
m_on_destruct_hooks.erase(get_type_id<Component_T>());
}
auto create_entity() -> Entity
auto create_entity() -> EntityId
{
++m_entity_count;
return m_current++;
}
void destroy_entity(Entity entity)
void destroy_entity(EntityId entity)
{
for (const auto &[key, set] : m_sparsed_sets)
{
@ -69,14 +69,21 @@ public:
}
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>();
return derived_set.at(entity).second;
}
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 &added_component = derived_set.insert(entity, std::move(component)).second;
@ -90,7 +97,7 @@ public:
};
template<typename Component_T>
void remove(Entity entity)
void remove(EntityId entity)
{
if (m_on_destruct_hooks.contains(get_type_id<Component_T>()))
{
@ -102,18 +109,18 @@ public:
}
template<typename Component_T>
auto view() -> SparseSet<Component_T, Entity> &
auto view() -> SparseSet<Component_T, EntityId> &
{
return get_derived_set<Component_T>();
};
template<typename ComponentA_T, typename 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_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
* component */
@ -142,7 +149,7 @@ public:
};
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>())
{
@ -152,7 +159,7 @@ public:
template<typename ComponentA_T, typename 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_b = get_derived_set<ComponentB_T>();
@ -223,22 +230,22 @@ private:
}
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>();
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 *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");
return *derived_set;
}
Entity m_current;
EntityId m_current;
TypeId m_entity_count;

View file

@ -128,6 +128,11 @@ public:
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 &
{
return m_dense.at(m_sparse.at(identifier));