feat(ecs): add convenient Entity class
This commit is contained in:
parent
a58b0c030f
commit
b6834310a7
4 changed files with 96 additions and 43 deletions
|
@ -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];
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
|
|
Loading…
Add table
Reference in a new issue