feat(ecs): new (simple) implementation without entt

This commit is contained in:
light7734 2025-09-20 15:39:18 +03:30
parent 0c35c13ac1
commit 21e7291189
Signed by: light7734
GPG key ID: 8C30176798F1A6BA
18 changed files with 871 additions and 418 deletions

View file

@ -1,10 +0,0 @@
#include <ecs/entity.hpp>
#include <ecs/scene.hpp>
namespace lt {
Entity::Entity(entt::entity handle, Scene *scene): m_handle(handle), m_scene(scene)
{
}
} // namespace lt

View file

@ -0,0 +1,349 @@
#include <ecs/registry.hpp>
#include <ranges>
#include <test/expects.hpp>
#include <test/test.hpp>
using lt::test::Case;
using lt::test::expect_unreachable;
using lt::test::Suite;
using lt::test::expect_eq;
using lt::test::expect_ne;
using lt::test::expect_throw;
using lt::test::expect_false;
using lt::test::expect_true;
using lt::ecs::Entity;
using lt::ecs::Registry;
struct Component
{
int m_int;
std::string m_string;
[[nodiscard]] friend auto operator==(const Component &lhs, const Component &rhs) -> bool
{
return lhs.m_int == rhs.m_int && lhs.m_string == rhs.m_string;
}
};
template<>
struct std::formatter<Component>
{
constexpr auto parse(std::format_parse_context &context)
{
return context.begin();
}
auto format(const Component &val, std::format_context &context) const
{
return std::format_to(context.out(), "{}, {}", val.m_int, val.m_string);
}
};
struct Component_B
{
float m_float;
[[nodiscard]] friend auto operator==(const Component_B lhs, const Component_B &rhs) -> bool
{
return lhs.m_float == rhs.m_float;
}
};
template<>
struct std::formatter<Component_B>
{
constexpr auto parse(std::format_parse_context &context)
{
return context.begin();
}
auto format(const Component_B &val, std::format_context &context) const
{
return std::format_to(context.out(), "{}", val.m_float);
}
};
Suite raii = [] {
Case { "happy path won't throw" } = [] {
std::ignore = Registry {};
};
Case { "many won't throw" } = [] {
for (auto idx : std::views::iota(0, 100'000))
{
std::ignore = Registry {};
}
};
Case { "unhappy path throws" } = [] {
};
Case { "post construct has correct state" } = [] {
auto registry = Registry {};
expect_eq(registry.get_entity_count(), 0);
};
};
Suite entity_raii = [] {
Case { "create_entity returns unique values" } = [] {
auto registry = Registry {};
auto set = std::unordered_set<Entity> {};
for (auto idx : std::views::iota(0, 10'000))
{
auto entity = registry.create_entity();
expect_false(set.contains(entity));
set.insert(entity);
expect_eq(set.size(), idx + 1);
}
};
Case { "post create/destroy_entity has correct state" } = [] {
auto registry = Registry {};
auto entities = std::vector<Entity> {};
for (auto idx : std::views::iota(0, 10'000))
{
entities.emplace_back(registry.create_entity());
expect_eq(registry.get_entity_count(), idx + 1);
}
for (auto idx : std::views::iota(0, 10'000))
{
auto entity = entities.back();
registry.destroy_entity(entity);
entities.pop_back();
expect_eq(registry.get_entity_count(), 10'000 - (idx + 1));
}
};
};
Suite component_raii = [] {
Case { "add has correct state" } = [] {
auto registry = Registry {};
for (auto idx : std::views::iota(0, 100'000))
{
auto entity = registry.create_entity();
auto &component = registry.add<Component>(
entity,
{ .m_int = idx, .m_string = std::to_string(idx) }
);
expect_eq(component.m_int, idx);
expect_eq(component.m_string, std::to_string(idx));
}
};
Case { "remove has correct state" } = [] {
auto registry = Registry {};
for (auto idx : std::views::iota(0, 100'000))
{
auto entity = registry.create_entity();
auto &component = registry.add<Component>(
entity,
{ .m_int = idx, .m_string = std::to_string(idx) }
);
expect_eq(component.m_int, idx);
expect_eq(component.m_string, std::to_string(idx));
}
};
};
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) {});
};
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(); });
for (auto idx : std::views::iota(0, 100'000))
{
registry.add<Component_B>(registry.create_entity(), {});
}
};
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> {};
registry.connect_on_construct<Component>([&](Registry &, Entity entity) {
on_construct_called.emplace_back(entity);
});
registry.connect_on_destruct<Component>([&](Registry &, Entity entity) {
on_destruct_called.emplace_back(entity);
});
expect_true(on_construct_called.empty());
expect_true(on_destruct_called.empty());
for (auto idx : std::views::iota(0, 100'000))
{
auto entity = all_entities.emplace_back(registry.create_entity());
registry.add<Component>(entity, {});
}
expect_eq(on_construct_called, all_entities);
expect_true(on_destruct_called.empty());
for (auto &entity : all_entities)
{
registry.remove<Component>(entity);
}
expect_eq(on_construct_called, all_entities);
expect_eq(on_destruct_called, all_entities);
};
};
Suite each = [] {
auto registry = Registry {};
auto shared_entity_counter = 0u;
auto component_map_a = std::unordered_map<Entity, Component> {};
auto entities_a = std::vector<Entity> {};
for (auto idx : std::views::iota(0, 10'000))
{
auto entity = entities_a.emplace_back(registry.create_entity());
auto &component = registry.add<Component>(
entity,
{ .m_int = idx, .m_string = std::to_string(idx) }
);
component_map_a[entity] = component;
}
auto component_map_b = std::unordered_map<lt::ecs::Entity, Component_B> {};
for (auto idx : std::views::iota(0, 10'000))
{
auto entity = Entity {};
if (idx % 3 == 0)
{
entity = entities_a[idx];
++shared_entity_counter;
}
else
{
entity = registry.create_entity();
}
auto &component = registry.add<Component_B>(
entity,
{ .m_float = static_cast<float>(idx) / 2.0f }
);
component_map_b[entity] = component;
}
Case { "each one element" } = [&] {
auto counter = 0u;
registry.each<Component>([&](Entity entity, Component &component) {
++counter;
expect_eq(component_map_a[entity], component);
});
expect_eq(component_map_a.size(), counter);
counter = 0u;
registry.each<Component_B>([&](Entity entity, Component_B &component) {
++counter;
expect_eq(component_map_b[entity], component);
});
expect_eq(component_map_b.size(), counter);
};
Case { "each two element" } = [&] {
auto counter = 0u;
registry.each<Component, Component_B>(
[&](Entity 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;
}
);
expect_eq(counter, shared_entity_counter);
};
};
Suite views = [] {
auto registry = Registry {};
auto shared_entity_counter = 0u;
auto component_map_a = std::unordered_map<Entity, Component> {};
auto entities_a = std::vector<Entity> {};
for (auto idx : std::views::iota(0, 10'000))
{
auto entity = entities_a.emplace_back(registry.create_entity());
auto &component = registry.add<Component>(
entity,
{ .m_int = idx, .m_string = std::to_string(idx) }
);
component_map_a[entity] = component;
}
auto component_map_b = std::unordered_map<Entity, Component_B> {};
for (auto idx : std::views::iota(0, 10'000))
{
auto entity = Entity {};
if (idx % 3 == 0)
{
entity = entities_a[idx];
++shared_entity_counter;
}
else
{
entity = registry.create_entity();
}
auto &component = registry.add<Component_B>(
entity,
{ .m_float = static_cast<float>(idx) / 2.0f }
);
component_map_b[entity] = component;
}
Case { "view one component" } = [&] {
for (const auto &[entity, component] : registry.view<Component>())
{
expect_eq(component_map_a[entity], component);
}
for (const auto &[entity, component] : registry.view<Component_B>())
{
expect_eq(component_map_b[entity], component);
}
};
Case { "view two component" } = [&] {
auto counter = 0u;
for (const auto &[entity, component, component_b] : registry.view<Component, Component_B>())
{
expect_eq(component_map_a[entity], component);
expect_eq(component_map_b[entity], component_b);
++counter;
}
expect_eq(counter, shared_entity_counter);
counter = 0u;
for (const auto &[entity, component_b, component] : registry.view<Component_B, Component>())
{
expect_eq(component_map_b[entity], component_b);
expect_eq(component_map_a[entity], component);
++counter;
}
expect_eq(counter, shared_entity_counter);
};
};

View file

@ -1,31 +0,0 @@
#include <ecs/components.hpp>
#include <ecs/entity.hpp>
#include <ecs/scene.hpp>
namespace lt {
auto Scene::create_entity(const std::string &name, const TransformComponent &transform) -> Entity
{
return create_entity_with_uuid(name, UUID(), transform);
}
auto Scene::get_entity_by_tag(const std::string &tag) -> Entity
{
return {};
}
auto Scene::create_entity_with_uuid(
const std::string &name,
UUID uuid,
const TransformComponent &transform
) -> Entity
{
auto entity = Entity { m_registry.create(), this };
entity.add_component<TagComponent>(name);
entity.add_component<TransformComponent>(transform);
entity.add_component<UUIDComponent>(uuid);
return entity;
}
} // namespace lt

View file

View file

@ -0,0 +1,138 @@
#include <ecs/sparse_set.hpp>
#include <ranges>
#include <test/expects.hpp>
#include <test/test.hpp>
using lt::test::Case;
using lt::test::Suite;
using lt::test::expect_eq;
using lt::test::expect_false;
using lt::test::expect_ne;
using lt::test::expect_throw;
using lt::test::expect_true;
using Set = lt::ecs::SparseSet<int>;
constexpr auto capacity = 100;
Suite raii = [] {
Case { "happy path won't throw" } = [] {
std::ignore = Set {};
std::ignore = Set { Set::max_capacity };
};
Case { "unhappy path throws" } = [] {
expect_throw([] { std::ignore = Set { Set::max_capacity + 1 }; });
};
Case { "post construct has correct state" } = [&] {
auto set = Set { capacity };
expect_eq(set.get_size(), 0);
expect_eq(set.get_capacity(), capacity);
};
};
Suite element_raii = [] {
Case { "many inserts/removes won't throw" } = [] {
auto set = Set {};
for (auto idx : std::views::iota(0, 10'000))
{
set.insert(idx, {});
}
for (auto idx : std::views::iota(0, 10'000))
{
set.remove(idx);
}
};
Case { "insert returns reference to inserted value" } = [] {
auto set = Set {};
for (auto idx : std::views::iota(0, 10'000))
{
const auto val = Set::Dense_T { idx, {} };
expect_eq(set.insert(val.first, val.second), val);
}
};
Case { "post insert/remove has correct state" } = [] {
auto set = Set {};
for (auto idx : std::views::iota(0, 10'000))
{
set.insert(idx, idx * 2);
expect_eq(set.get_size(), idx + 1);
expect_eq(set.at(idx), Set::Dense_T { idx, idx * 2 });
expect_true(set.contains(idx));
}
for (auto idx : std::views::iota(0, 10'000))
{
expect_eq(set.at(idx), Set::Dense_T { idx, idx * 2 });
expect_true(set.contains(idx));
}
for (auto idx : std::views::iota(0, 10'000))
{
set.remove(idx);
expect_eq(set.get_size(), 10'000 - (idx + 1));
expect_throw([&] { std::ignore = set.at(idx); });
expect_false(set.contains(idx));
}
};
};
Suite getters = [] {
Case { "get_size returns correct values" } = [] {
auto set = Set {};
for (auto idx : std::views::iota(0, 10'000))
{
expect_eq(set.get_size(), idx);
set.insert(idx, {});
}
expect_eq(set.get_size(), 10'000);
};
Case { "get_capacity returns correct values" } = [] {
auto set = Set { 10'000 };
for (auto idx : std::views::iota(0, 10'000))
{
expect_eq(set.get_capacity(), 10'000); // are we testing std::vector's implementation?
set.insert(idx, {});
}
expect_eq(set.get_capacity(), 10'000);
set.insert(set.get_size(), {});
expect_ne(set.get_capacity(), 10'000);
};
Case { "at throws with out of bound access" } = [] {
auto set = Set {};
for (auto idx : std::views::iota(0, 50))
{
expect_throw([&] {
set.insert(idx, {});
std::ignore = set.at(50);
});
}
set.insert(50, {});
std::ignore = set.at(50); // should not throw
};
};
Suite clear = [] {
Case { "post clear has correct state" } = [] {
auto set = Set { 0 };
for (auto idx : std::views::iota(0, 10'000))
{
set.insert(idx, {});
}
set.clear();
expect_eq(set.get_size(), 0);
};
};

View file

@ -1,14 +0,0 @@
#include <ecs/uuid.hpp>
namespace lt {
std::mt19937_64 UUID::s_engine = std::mt19937_64(std::random_device()());
std::uniform_int_distribution<uint64_t>
UUID::s_distribution = std::uniform_int_distribution<uint64_t> {};
UUID::UUID(uint64_t uuid /* = -1 */): m_uuid(uuid == -1 ? s_distribution(s_engine) : uuid)
{
}
} // namespace lt

View file

@ -1,6 +0,0 @@
#pragma once
#include <ecs/components/native_script.hpp>
#include <ecs/components/sprite_renderer.hpp>
#include <ecs/components/tag.hpp>
#include <ecs/components/transform.hpp>

View file

@ -1,28 +0,0 @@
#pragma once
#include <ecs/components/scriptable_entity.hpp>
namespace lt {
struct NativeScriptComponent
{
NativeScript *(*CreateInstance)();
void (*DestroyInstance)(NativeScriptComponent *);
template<typename t>
void bind()
{
CreateInstance = []() {
return static_cast<NativeScript *>(new t());
};
DestroyInstance = [](NativeScriptComponent *nsc) {
delete (t *)(nsc->instance);
nsc->instance = nullptr;
};
}
NativeScript *instance;
};
} // namespace lt

View file

@ -1,46 +0,0 @@
#pragma once
#include <ecs/entity.hpp>
namespace lt {
class NativeScript
{
public:
friend class Scene;
NativeScript() = default;
virtual ~NativeScript() = default;
[[nodiscard]] auto get_uid() const -> unsigned int
{
return m_unique_identifier;
}
template<typename t>
auto GetComponent() -> t &
{
return m_entity.get_component<t>();
}
protected:
virtual void on_create()
{
}
virtual void on_destroy()
{
}
virtual void on_update(float ts)
{
}
private:
Entity m_entity;
unsigned int m_unique_identifier = 0; // :#todo
};
} // namespace lt

View file

@ -1,35 +0,0 @@
#pragma once
#include <math/vec4.hpp>
#include <utility>
namespace lt {
class Texture;
struct SpriteRendererComponent
{
SpriteRendererComponent() = default;
SpriteRendererComponent(const SpriteRendererComponent &) = default;
SpriteRendererComponent(
Ref<Texture> _texture,
const math::vec4 &_tint = math::vec4 { 1.0f, 1.0f, 1.0f, 1.0f }
)
: texture(std::move(std::move(_texture)))
, tint(_tint)
{
}
operator Ref<Texture>() const
{
return texture;
}
Ref<Texture> texture;
math::vec4 tint {};
};
} // namespace lt

View file

@ -1,30 +0,0 @@
#pragma once
#include <utility>
namespace lt {
struct TagComponent
{
TagComponent() = default;
TagComponent(const TagComponent &) = default;
TagComponent(std::string _tag): tag(std::move(_tag))
{
}
operator std::string() const
{
return tag;
}
operator const std::string &() const
{
return tag;
}
std::string tag = "Unnamed";
};
} // namespace lt

View file

@ -1,43 +0,0 @@
#pragma once
#include <math/mat4.hpp>
#include <math/vec3.hpp>
namespace lt {
struct TransformComponent
{
TransformComponent(const TransformComponent &) = default;
TransformComponent(
const math::vec3 &_translation = math::vec3(0.0f, 0.0f, 0.0f),
const math::vec3 &_scale = math::vec3(1.0f, 1.0f, 1.0f),
const math::vec3 &_rotation = math::vec3(0.0f, 0.0f, 0.0f)
)
: translation(_translation)
, scale(_scale)
, rotation(_rotation)
{
}
[[nodiscard]] auto get_transform() const -> math::mat4
{
return math::translate(translation)
* math::rotate(rotation.z, math::vec3 { 0.0f, 0.0f, 1.0f }) //
* math::scale(scale);
}
operator const math::mat4() const
{
return get_transform();
}
math::vec3 translation;
math::vec3 scale;
math::vec3 rotation;
};
} // namespace lt

View file

@ -1,18 +0,0 @@
#pragma once
#include <ecs/uuid.hpp>
namespace lt {
struct UUIDComponent
{
UUIDComponent(UUID _uuid): uuid(_uuid)
{
}
UUIDComponent(const UUIDComponent &) = default;
UUID uuid;
};
} // namespace lt

View file

@ -1,59 +1,5 @@
#pragma once
#include <ecs/components/uuid.hpp>
#include <ecs/scene.hpp>
#include <entt/entt.hpp>
namespace lt {
class Entity
{
public:
Entity(entt::entity handle = entt::null, Scene *scene = nullptr);
template<typename t, typename... Args>
auto add_component(Args &&...args) -> t &
{
return m_scene->m_registry.emplace<t>(m_handle, std::forward<Args>(args)...);
}
template<typename t>
auto get_component() -> t &
{
return m_scene->m_registry.get<t>(m_handle);
}
template<typename t>
auto has_component() -> bool
{
return m_scene->m_registry.any_of<t>(m_handle);
}
template<typename t>
void remove_component()
{
m_scene->m_registry.remove<t>(m_handle);
}
auto get_uuid() -> uint64_t
{
return get_component<UUIDComponent>().uuid;
}
[[nodiscard]] auto is_valid() const -> bool
{
return m_handle != entt::null && m_scene != nullptr;
}
operator uint32_t()
{
return (uint32_t)m_handle;
}
private:
entt::entity m_handle;
Scene *m_scene;
};
} // namespace lt

View file

@ -0,0 +1,243 @@
#pragma once
#include <ecs/sparse_set.hpp>
namespace lt::ecs {
using Entity = uint32_t;
/** A registry of components, the heart of an ECS architecture.
*
* @todo(Light): optimize multi-component views
* @todo(Light): support more than 2-component views
* @todo(Light): handle edge cases or specify the undefined behaviors
*/
class Registry
{
public:
using UnderlyingSparseSet_T = TypeErasedSparseSet<Entity>;
using Callback_T = std::function<void(Registry &, Entity)>;
template<typename Component_T>
void connect_on_construct(Callback_T callback)
{
m_on_construct_hooks[get_type_id<Component_T>()] = callback;
}
template<typename Component_T>
void connect_on_destruct(Callback_T callback)
{
m_on_destruct_hooks[get_type_id<Component_T>()] = callback;
}
template<typename Component_T>
void disconnect_on_construct()
{
m_on_construct_hooks.erase(get_type_id<Component_T>());
}
template<typename Component_T>
void disconnect_on_destruct()
{
m_on_destruct_hooks.erase(get_type_id<Component_T>());
}
auto create_entity() -> Entity
{
++m_entity_count;
return m_current++;
}
void destroy_entity(Entity entity)
{
for (const auto &[key, set] : m_sparsed_sets)
{
set->remove(entity);
}
--m_entity_count;
}
template<typename Component_T>
auto get(Entity entity) -> 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 &derived_set = get_derived_set<Component_T>();
auto &added_component = derived_set.insert(entity, std::move(component)).second;
if (m_on_construct_hooks.contains(get_type_id<Component_T>()))
{
m_on_construct_hooks[get_type_id<Component_T>()](*this, entity);
}
return added_component;
};
template<typename Component_T>
void remove(Entity entity)
{
if (m_on_destruct_hooks.contains(get_type_id<Component_T>()))
{
m_on_destruct_hooks[get_type_id<Component_T>()](*this, entity);
}
auto &derived_set = get_derived_set<Component_T>();
derived_set.remove(entity);
}
template<typename Component_T>
auto view() -> SparseSet<Component_T, Entity>&
{
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 &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 &>> {};
/* iterate over the "smaller" component-set, and check if its entities have the other
* component */
if (set_a.get_size() > set_b.get_size())
{
for (auto &[entity, component_b] : set_b)
{
if (set_a.contains(entity))
{
view.emplace_back(std::tie(entity, set_a.at(entity).second, component_b));
}
}
}
else
{
for (auto &[entity, component_a] : set_a)
{
if (set_b.contains(entity))
{
view.emplace_back(std::tie(entity, component_a, set_b.at(entity).second));
}
}
}
return view;
};
template<typename Component_T>
void each(std::function<void(Entity, Component_T &)> functor)
{
for (auto &[entity, component] : get_derived_set<Component_T>())
{
functor(entity, component);
}
};
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)
{
auto &set_a = get_derived_set<ComponentA_T>();
auto &set_b = get_derived_set<ComponentB_T>();
/* iterate over the "smaller" component-set, and check if its entities have the other
* component */
if (set_a.get_size() > set_b.get_size())
{
for (auto &[entity, component_b] : set_b)
{
if (set_a.contains(entity))
{
functor(entity, set_a.at(entity).second, component_b);
}
}
return;
}
for (auto &[entity, component_a] : set_a)
{
if (set_b.contains(entity))
{
functor(entity, component_a, set_b.at(entity).second);
}
}
};
[[nodiscard]] auto get_entity_count() const -> size_t
{
return static_cast<size_t>(m_entity_count);
}
private:
using TypeId = size_t;
static consteval auto hash_cstr(const char *str) -> TypeId
{
constexpr auto fnv_offset_basis = size_t { 14695981039346656037ull };
constexpr auto fnv_prime = size_t { 1099511628211ull };
auto hash = fnv_offset_basis;
for (const auto &ch : std::string_view { str })
{
hash *= fnv_prime;
hash ^= static_cast<uint8_t>(ch);
}
return hash;
}
template<typename T>
static consteval auto get_type_id() -> TypeId
{
#if defined _MSC_VER
#define GENERATOR_PRETTY_FUNCTION __FUNCSIG__
#elif defined __clang__ || (defined __GNUC__)
#define GENERATOR_PRETTY_FUNCTION __PRETTY_FUNCTION__
#else
#error "Compiler not supported"
#endif
constexpr auto value = hash_cstr(GENERATOR_PRETTY_FUNCTION);
#undef GENERATOR_PRETTY_FUNCTION
return value;
}
template<typename T>
auto get_derived_set() -> SparseSet<T, Entity> &
{
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>>();
}
auto *base_set = m_sparsed_sets[type_id].get();
auto *derived_set = dynamic_cast<SparseSet<T, Entity> *>(base_set);
ensure(derived_set, "Failed to downcast to derived set");
return *derived_set;
}
Entity m_current;
TypeId m_entity_count;
std::flat_map<TypeId, Scope<UnderlyingSparseSet_T>> m_sparsed_sets;
std::flat_map<TypeId, Callback_T> m_on_construct_hooks;
std::flat_map<TypeId, Callback_T> m_on_destruct_hooks;
};
} // namespace lt::ecs

View file

@ -1,65 +0,0 @@
#pragma once
#include <ecs/components/transform.hpp>
#include <ecs/uuid.hpp>
#include <entt/entt.hpp>
#include <functional>
namespace lt {
class Entity;
class Framebuffer;
class Scene
{
public:
template<typename... T>
auto group()
{
return m_registry.group(entt::get<T...>);
}
template<typename T>
auto view()
{
return m_registry.view<T>();
}
auto create_entity(
const std::string &name,
const TransformComponent &transform = TransformComponent()
) -> Entity;
auto get_entity_by_tag(const std::string &tag) -> Entity;
auto get_entt_registry() -> entt::registry &
{
return m_registry;
}
private:
friend class Entity;
friend class SceneSerializer;
friend class SceneHierarchyPanel;
entt::registry m_registry;
auto create_entity_with_uuid(
const std::string &name,
UUID uuid,
const TransformComponent &transform = TransformComponent()
) -> Entity;
};
namespace ecs {
using Registry = Scene;
using Entity = ::lt::Entity;
} // namespace ecs
} // namespace lt

View file

@ -0,0 +1,141 @@
#pragma once
namespace lt::ecs {
/**
*
* @ref https://programmingpraxis.com/2012/03/09/sparse-sets/
*/
template<typename Identifier_T = uint32_t>
class TypeErasedSparseSet
{
public:
TypeErasedSparseSet() = default;
TypeErasedSparseSet(TypeErasedSparseSet &&) = default;
TypeErasedSparseSet(const TypeErasedSparseSet &) = default;
auto operator=(TypeErasedSparseSet &&) -> TypeErasedSparseSet & = default;
auto operator=(const TypeErasedSparseSet &) -> TypeErasedSparseSet & = default;
virtual ~TypeErasedSparseSet() = default;
virtual void remove(Identifier_T identifier) = 0;
};
/**
*
* @todo(Light): implement identifier recycling.
*/
template<typename Value_T, typename Identifier_T = uint32_t>
class SparseSet: public TypeErasedSparseSet<Identifier_T>
{
public:
using Dense_T = std::pair<Identifier_T, Value_T>;
static constexpr auto max_capacity = size_t { 1'000'000 };
static constexpr auto null_identifier = std::numeric_limits<Identifier_T>().max();
explicit SparseSet(size_t initial_capacity = 1)
{
ensure(
initial_capacity <= max_capacity,
"Failed to create SparseSet: capacity too large ({} > {})",
initial_capacity,
max_capacity
);
m_dense.reserve(initial_capacity);
m_sparse.resize(initial_capacity, null_identifier);
}
auto insert(Identifier_T identifier, Value_T value) -> Dense_T &
{
if (m_sparse.size() < identifier + 1)
{
auto new_capacity = std::max(static_cast<size_t>(identifier + 1), m_sparse.size() * 2);
new_capacity = std::min(new_capacity, max_capacity);
// log_dbg("Increasing sparse vector size:", m_dead_count);
// log_dbg("\tdead_count: {}", m_dead_count);
// log_dbg("\talive_count: {}", m_alive_count);
// log_dbg("\tsparse.size: {} -> {}", m_sparse.size(), new_capacity);
m_sparse.resize(new_capacity, null_identifier);
}
++m_alive_count;
m_sparse[identifier] = m_dense.size();
return m_dense.emplace_back(identifier, std::move(value));
}
void remove(Identifier_T identifier) override
{
auto &idx = m_sparse[identifier];
auto &[entity, component] = m_dense[idx];
idx = null_identifier;
++m_dead_count;
--m_alive_count;
}
void clear()
{
m_dense.clear();
m_sparse.clear();
m_alive_count = 0;
}
[[nodiscard]] auto contains(Identifier_T identifier) const -> bool
{
return m_sparse.size() > identifier //
&& m_sparse[identifier] != null_identifier //
&& m_dense[m_sparse[identifier]].first == identifier;
}
auto begin() -> std::vector<Dense_T>::iterator
{
return m_dense.begin();
}
auto end() -> std::vector<Dense_T>::iterator
{
return m_dense.end();
}
[[nodiscard]] auto at(Identifier_T identifier) -> Dense_T &
{
return m_dense.at(m_sparse.at(identifier));
}
/** @warn unsafe, for bound-checked access: use `.at` */
[[nodiscard]] auto &&operator[](this auto &&self, Identifier_T identifier)
{
using Self_T = decltype(self);
return std::forward<Self_T>(self).m_dense[std::forward<Self_T>(self).m_sparse[identifier]];
}
[[nodiscard]] auto get_size() const noexcept -> size_t
{
return m_alive_count;
}
[[nodiscard]] auto get_capacity() const noexcept -> size_t
{
return m_sparse.capacity();
}
private:
std::vector<Dense_T> m_dense;
std::vector<Identifier_T> m_sparse;
size_t m_alive_count {};
size_t m_dead_count {};
};
} // namespace lt::ecs

View file

@ -1,38 +0,0 @@
#pragma once
#include <random>
namespace lt {
class UUID
{
public:
UUID(uint64_t uuid = -1);
operator uint64_t() const
{
return m_uuid;
}
private:
static std::mt19937_64 s_engine;
static std::uniform_int_distribution<uint64_t> s_distribution;
uint64_t m_uuid;
};
} // namespace lt
namespace std {
template<>
struct hash<lt::UUID>
{
std::size_t operator()(const lt::UUID &uuid) const
{
return hash<uint64_t>()(static_cast<uint64_t>(uuid));
}
};
} // namespace std