diff --git a/modules/ecs/private/entity.cpp b/modules/ecs/private/entity.cpp deleted file mode 100644 index 08e72b0..0000000 --- a/modules/ecs/private/entity.cpp +++ /dev/null @@ -1,10 +0,0 @@ -#include -#include - -namespace lt { - -Entity::Entity(entt::entity handle, Scene *scene): m_handle(handle), m_scene(scene) -{ -} - -} // namespace lt diff --git a/modules/ecs/private/registry.test.cpp b/modules/ecs/private/registry.test.cpp new file mode 100644 index 0000000..096ec68 --- /dev/null +++ b/modules/ecs/private/registry.test.cpp @@ -0,0 +1,349 @@ +#include +#include +#include +#include + +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 +{ + 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 +{ + 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 {}; + + 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 {}; + 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( + 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( + 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([&](Registry &, Entity) {}); + registry.connect_on_destruct([&](Registry &, Entity) {}); + }; + + Case { "on_construct/on_destruct won't get called on unrelated component" } = [] { + auto registry = Registry {}; + registry.connect_on_construct([&](Registry &, Entity) { expect_unreachable(); }); + registry.connect_on_destruct([&](Registry &, Entity) { expect_unreachable(); }); + + for (auto idx : std::views::iota(0, 100'000)) + { + registry.add(registry.create_entity(), {}); + } + }; + + Case { "on_construct/on_destruct gets called" } = [] { + auto registry = Registry {}; + auto all_entities = std::vector {}; + auto on_construct_called = std::vector {}; + auto on_destruct_called = std::vector {}; + + registry.connect_on_construct([&](Registry &, Entity entity) { + on_construct_called.emplace_back(entity); + }); + registry.connect_on_destruct([&](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(entity, {}); + } + expect_eq(on_construct_called, all_entities); + expect_true(on_destruct_called.empty()); + + for (auto &entity : all_entities) + { + registry.remove(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 {}; + auto entities_a = std::vector {}; + + for (auto idx : std::views::iota(0, 10'000)) + { + auto entity = entities_a.emplace_back(registry.create_entity()); + auto &component = registry.add( + entity, + { .m_int = idx, .m_string = std::to_string(idx) } + ); + + component_map_a[entity] = component; + } + + auto component_map_b = std::unordered_map {}; + 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( + entity, + { .m_float = static_cast(idx) / 2.0f } + ); + + component_map_b[entity] = component; + } + + Case { "each one element" } = [&] { + auto counter = 0u; + registry.each([&](Entity entity, Component &component) { + ++counter; + expect_eq(component_map_a[entity], component); + }); + + expect_eq(component_map_a.size(), counter); + + counter = 0u; + registry.each([&](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( + [&](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 {}; + auto entities_a = std::vector {}; + + for (auto idx : std::views::iota(0, 10'000)) + { + auto entity = entities_a.emplace_back(registry.create_entity()); + auto &component = registry.add( + entity, + { .m_int = idx, .m_string = std::to_string(idx) } + ); + + component_map_a[entity] = component; + } + + auto component_map_b = std::unordered_map {}; + 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( + entity, + { .m_float = static_cast(idx) / 2.0f } + ); + + component_map_b[entity] = component; + } + + + Case { "view one component" } = [&] { + for (const auto &[entity, component] : registry.view()) + { + expect_eq(component_map_a[entity], component); + } + + for (const auto &[entity, component] : registry.view()) + { + expect_eq(component_map_b[entity], component); + } + }; + + Case { "view two component" } = [&] { + auto counter = 0u; + for (const auto &[entity, component, component_b] : registry.view()) + { + 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()) + { + expect_eq(component_map_b[entity], component_b); + expect_eq(component_map_a[entity], component); + ++counter; + } + expect_eq(counter, shared_entity_counter); + }; +}; diff --git a/modules/ecs/private/scene.cpp b/modules/ecs/private/scene.cpp deleted file mode 100644 index 49168d5..0000000 --- a/modules/ecs/private/scene.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include -#include -#include - -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(name); - entity.add_component(transform); - entity.add_component(uuid); - - return entity; -} - -} // namespace lt diff --git a/modules/ecs/private/sparse_set.cpp b/modules/ecs/private/sparse_set.cpp new file mode 100644 index 0000000..e69de29 diff --git a/modules/ecs/private/sparse_set.test.cpp b/modules/ecs/private/sparse_set.test.cpp new file mode 100644 index 0000000..1465eec --- /dev/null +++ b/modules/ecs/private/sparse_set.test.cpp @@ -0,0 +1,138 @@ +#include +#include +#include +#include + +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; +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); + }; +}; diff --git a/modules/ecs/private/uuid.cpp b/modules/ecs/private/uuid.cpp deleted file mode 100644 index 09a55cc..0000000 --- a/modules/ecs/private/uuid.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include - -namespace lt { - -std::mt19937_64 UUID::s_engine = std::mt19937_64(std::random_device()()); - -std::uniform_int_distribution - UUID::s_distribution = std::uniform_int_distribution {}; - -UUID::UUID(uint64_t uuid /* = -1 */): m_uuid(uuid == -1 ? s_distribution(s_engine) : uuid) -{ -} - -} // namespace lt diff --git a/modules/ecs/public/components.hpp b/modules/ecs/public/components.hpp deleted file mode 100644 index bd457f5..0000000 --- a/modules/ecs/public/components.hpp +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -#include -#include -#include -#include diff --git a/modules/ecs/public/components/native_script.hpp b/modules/ecs/public/components/native_script.hpp deleted file mode 100644 index a0d8f5a..0000000 --- a/modules/ecs/public/components/native_script.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include - -namespace lt { - -struct NativeScriptComponent -{ - NativeScript *(*CreateInstance)(); - - void (*DestroyInstance)(NativeScriptComponent *); - - template - void bind() - { - CreateInstance = []() { - return static_cast(new t()); - }; - DestroyInstance = [](NativeScriptComponent *nsc) { - delete (t *)(nsc->instance); - nsc->instance = nullptr; - }; - } - - NativeScript *instance; -}; - -} // namespace lt diff --git a/modules/ecs/public/components/scriptable_entity.hpp b/modules/ecs/public/components/scriptable_entity.hpp deleted file mode 100644 index 9131add..0000000 --- a/modules/ecs/public/components/scriptable_entity.hpp +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once - -#include - -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 - auto GetComponent() -> t & - { - return m_entity.get_component(); - } - -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 diff --git a/modules/ecs/public/components/sprite_renderer.hpp b/modules/ecs/public/components/sprite_renderer.hpp deleted file mode 100644 index 2b11c3d..0000000 --- a/modules/ecs/public/components/sprite_renderer.hpp +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include -#include - -namespace lt { - -class Texture; - -struct SpriteRendererComponent -{ - SpriteRendererComponent() = default; - - SpriteRendererComponent(const SpriteRendererComponent &) = default; - - SpriteRendererComponent( - Ref _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() const - { - return texture; - } - - Ref texture; - - math::vec4 tint {}; -}; - -} // namespace lt diff --git a/modules/ecs/public/components/tag.hpp b/modules/ecs/public/components/tag.hpp deleted file mode 100644 index 4e19b74..0000000 --- a/modules/ecs/public/components/tag.hpp +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include - -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 diff --git a/modules/ecs/public/components/transform.hpp b/modules/ecs/public/components/transform.hpp deleted file mode 100644 index cd9fe4e..0000000 --- a/modules/ecs/public/components/transform.hpp +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -#include -#include - -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 diff --git a/modules/ecs/public/components/uuid.hpp b/modules/ecs/public/components/uuid.hpp deleted file mode 100644 index a3f5a79..0000000 --- a/modules/ecs/public/components/uuid.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include - -namespace lt { - -struct UUIDComponent -{ - UUIDComponent(UUID _uuid): uuid(_uuid) - { - } - - UUIDComponent(const UUIDComponent &) = default; - - UUID uuid; -}; - -} // namespace lt diff --git a/modules/ecs/public/entity.hpp b/modules/ecs/public/entity.hpp index bbcd7bf..6ab0f0b 100644 --- a/modules/ecs/public/entity.hpp +++ b/modules/ecs/public/entity.hpp @@ -1,59 +1,5 @@ #pragma once -#include -#include -#include - namespace lt { -class Entity -{ -public: - Entity(entt::entity handle = entt::null, Scene *scene = nullptr); - - template - auto add_component(Args &&...args) -> t & - { - return m_scene->m_registry.emplace(m_handle, std::forward(args)...); - } - - template - auto get_component() -> t & - { - return m_scene->m_registry.get(m_handle); - } - - template - auto has_component() -> bool - { - return m_scene->m_registry.any_of(m_handle); - } - - template - void remove_component() - { - m_scene->m_registry.remove(m_handle); - } - - auto get_uuid() -> uint64_t - { - return get_component().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 diff --git a/modules/ecs/public/registry.hpp b/modules/ecs/public/registry.hpp new file mode 100644 index 0000000..1377cfd --- /dev/null +++ b/modules/ecs/public/registry.hpp @@ -0,0 +1,243 @@ +#pragma once + +#include + +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; + + using Callback_T = std::function; + + template + void connect_on_construct(Callback_T callback) + { + m_on_construct_hooks[get_type_id()] = callback; + } + + template + void connect_on_destruct(Callback_T callback) + { + m_on_destruct_hooks[get_type_id()] = callback; + } + + template + void disconnect_on_construct() + { + m_on_construct_hooks.erase(get_type_id()); + } + + template + void disconnect_on_destruct() + { + m_on_destruct_hooks.erase(get_type_id()); + } + + 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 + auto get(Entity entity) -> Component_T & + { + auto &derived_set = get_derived_set(); + return derived_set.at(entity).second; + } + + template + auto add(Entity entity, Component_T component) -> Component_T & + { + auto &derived_set = get_derived_set(); + auto &added_component = derived_set.insert(entity, std::move(component)).second; + + if (m_on_construct_hooks.contains(get_type_id())) + { + m_on_construct_hooks[get_type_id()](*this, entity); + } + + return added_component; + }; + + template + void remove(Entity entity) + { + if (m_on_destruct_hooks.contains(get_type_id())) + { + m_on_destruct_hooks[get_type_id()](*this, entity); + } + + auto &derived_set = get_derived_set(); + derived_set.remove(entity); + } + + template + auto view() -> SparseSet& + { + return get_derived_set(); + }; + + template + requires(!std::is_same_v) + auto view() -> std::vector> + { + auto &set_a = get_derived_set(); + auto &set_b = get_derived_set(); + auto view = std::vector> {}; + + /* 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 + void each(std::function functor) + { + for (auto &[entity, component] : get_derived_set()) + { + functor(entity, component); + } + }; + + template + requires(!std::is_same_v) + void each(std::function functor) + { + auto &set_a = get_derived_set(); + auto &set_b = get_derived_set(); + + /* 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(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(ch); + } + + return hash; + } + + template + 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 + auto get_derived_set() -> SparseSet & + { + constexpr auto type_id = get_type_id(); + if (!m_sparsed_sets.contains(type_id)) + { + m_sparsed_sets[type_id] = create_scope>(); + } + + auto *base_set = m_sparsed_sets[type_id].get(); + auto *derived_set = dynamic_cast *>(base_set); + ensure(derived_set, "Failed to downcast to derived set"); + + return *derived_set; + } + + Entity m_current; + + TypeId m_entity_count; + + std::flat_map> m_sparsed_sets; + + std::flat_map m_on_construct_hooks; + + std::flat_map m_on_destruct_hooks; +}; + +} // namespace lt::ecs diff --git a/modules/ecs/public/scene.hpp b/modules/ecs/public/scene.hpp deleted file mode 100644 index 2d8bf7c..0000000 --- a/modules/ecs/public/scene.hpp +++ /dev/null @@ -1,65 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -namespace lt { - -class Entity; -class Framebuffer; - -class Scene -{ -public: - template - auto group() - { - return m_registry.group(entt::get); - } - - template - auto view() - { - return m_registry.view(); - } - - 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 diff --git a/modules/ecs/public/sparse_set.hpp b/modules/ecs/public/sparse_set.hpp new file mode 100644 index 0000000..abaf441 --- /dev/null +++ b/modules/ecs/public/sparse_set.hpp @@ -0,0 +1,141 @@ +#pragma once + +namespace lt::ecs { + +/** + * + * @ref https://programmingpraxis.com/2012/03/09/sparse-sets/ + */ +template +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 +class SparseSet: public TypeErasedSparseSet +{ +public: + using Dense_T = std::pair; + + static constexpr auto max_capacity = size_t { 1'000'000 }; + + static constexpr auto null_identifier = std::numeric_limits().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(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::iterator + { + return m_dense.begin(); + } + + auto end() -> std::vector::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).m_dense[std::forward(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 m_dense; + + std::vector m_sparse; + + size_t m_alive_count {}; + + size_t m_dead_count {}; +}; + +} // namespace lt::ecs diff --git a/modules/ecs/public/uuid.hpp b/modules/ecs/public/uuid.hpp deleted file mode 100644 index 19aa552..0000000 --- a/modules/ecs/public/uuid.hpp +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include - -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 s_distribution; - - uint64_t m_uuid; -}; - -} // namespace lt - -namespace std { - -template<> -struct hash -{ - std::size_t operator()(const lt::UUID &uuid) const - { - return hash()(static_cast(uuid)); - } -}; - -} // namespace std