From e0e0df8128502c6775f84275f640cd0cca26111f Mon Sep 17 00:00:00 2001 From: lisyarus Date: Tue, 22 Aug 2023 20:30:04 +0300 Subject: [PATCH] ECS library wip & tests --- libs/ecs/CMakeLists.txt | 2 +- .../psemek/ecs/detail/apply_helper.hpp | 3 +- .../psemek/ecs/detail/component_index.hpp | 20 +++ libs/ecs/include/psemek/ecs/detail/table.hpp | 26 +--- .../include/psemek/ecs/entity_accessor.hpp | 22 ++- .../include/psemek/ecs/entity_container.hpp | 50 ++---- libs/ecs/source/detail/entity_list.cpp | 4 +- libs/ecs/tests/apply.cpp | 113 ++++++++++++++ libs/ecs/tests/component.cpp | 120 ++++++++++++++ libs/ecs/tests/entity.cpp | 147 ++++++++++++++++++ 10 files changed, 441 insertions(+), 66 deletions(-) create mode 100644 libs/ecs/tests/apply.cpp create mode 100644 libs/ecs/tests/component.cpp create mode 100644 libs/ecs/tests/entity.cpp diff --git a/libs/ecs/CMakeLists.txt b/libs/ecs/CMakeLists.txt index 2feb2512..44883154 100644 --- a/libs/ecs/CMakeLists.txt +++ b/libs/ecs/CMakeLists.txt @@ -5,5 +5,5 @@ psemek_add_library(psemek-ecs ${PSEMEK_ECS_HEADERS} ${PSEMEK_ECS_SOURCES}) target_include_directories(psemek-ecs PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") target_link_libraries(psemek-ecs PUBLIC psemek-util) -#psemek_glob_tests(psemek-ecs tests) +psemek_glob_tests(psemek-ecs tests) diff --git a/libs/ecs/include/psemek/ecs/detail/apply_helper.hpp b/libs/ecs/include/psemek/ecs/detail/apply_helper.hpp index edd7fd8a..09f2627d 100644 --- a/libs/ecs/include/psemek/ecs/detail/apply_helper.hpp +++ b/libs/ecs/include/psemek/ecs/detail/apply_helper.hpp @@ -12,7 +12,8 @@ namespace psemek::ecs::detail std::size_t row_count; entity_id const * entity_id_pointer; entity_data const * entity_data_pointer; - table::component_pointer pointers[sizeof...(Components)]; + // (+1) to prevent zero-sized array + table::component_pointer pointers[sizeof...(Components) + 1]; static_apply_helper(util::span entity_ids, util::span entities) : row_count(entity_ids.size()) diff --git a/libs/ecs/include/psemek/ecs/detail/component_index.hpp b/libs/ecs/include/psemek/ecs/detail/component_index.hpp index 7a8fe204..8219cfad 100644 --- a/libs/ecs/include/psemek/ecs/detail/component_index.hpp +++ b/libs/ecs/include/psemek/ecs/detail/component_index.hpp @@ -8,6 +8,26 @@ namespace psemek::ecs::detail { + template + struct component_uuid_holder + { + util::uuid uuids[sizeof...(Components)] { Components::uuid() ... }; + + auto get() const + { + return util::span(uuids); + } + }; + + template <> + struct component_uuid_holder<> + { + auto get() const + { + return util::span(); + } + }; + struct component_index { template diff --git a/libs/ecs/include/psemek/ecs/detail/table.hpp b/libs/ecs/include/psemek/ecs/detail/table.hpp index cea85fbb..120e54ce 100644 --- a/libs/ecs/include/psemek/ecs/detail/table.hpp +++ b/libs/ecs/include/psemek/ecs/detail/table.hpp @@ -67,7 +67,6 @@ namespace psemek::ecs::detail table_impl(util::span component_uuids); std::size_t push_row(entity_id id) override; - std::size_t push_row_with_components(entity_id id, Components && ... components); void swap_rows(std::size_t row1, std::size_t row2) override; void pop_row() override; @@ -108,29 +107,6 @@ namespace psemek::ecs::detail return row_count_ - 1; } - template - std::size_t table_impl::push_row_with_components(entity_id id, Components && ... components) - { - if (row_count_ == capacity_) - reallocate(); - - [[maybe_unused]] auto push_row_impl = [&](std::uint8_t * data, Component && value) - { - if constexpr (!detail::is_empty_v) - { - new (reinterpret_cast(data) + row_count_) Component{std::move(value)}; - } - }; - - std::size_t i = 0; - (push_row_impl.template operator()(component_pointers_[i++].data, std::move(components)), ...); - ++row_count_; - - entity_ids_.push_back(id); - - return row_count_ - 1; - } - template void table_impl::swap_rows(std::size_t row1, std::size_t row2) { @@ -198,6 +174,8 @@ namespace psemek::ecs::detail std::size_t i = 0; (reallocate_impl.template operator()(component_pointers_[i++].data), ...); + + capacity_ = new_capacity; } } diff --git a/libs/ecs/include/psemek/ecs/entity_accessor.hpp b/libs/ecs/include/psemek/ecs/entity_accessor.hpp index d736ab54..10adf552 100644 --- a/libs/ecs/include/psemek/ecs/entity_accessor.hpp +++ b/libs/ecs/include/psemek/ecs/entity_accessor.hpp @@ -1,16 +1,36 @@ #pragma once #include +#include namespace psemek::ecs { struct entity_accessor { + entity_accessor(detail::table * table, std::uint32_t row) + : table_(table) + , row_(row) + {} + template + Component & get() + { + util::uuid const uuid = Component::uuid(); + + auto const component_uuids = table_->get_component_uuids(); + + for (std::size_t i = 0; i < component_uuids.size(); ++i) + if (uuid == component_uuids[i]) + return *reinterpret_cast(table_->get_component_pointers()[i].data + detail::stride() * row_); + + assert(false); + __builtin_unreachable(); + } private: - + detail::table * table_; + std::uint32_t row_; }; } diff --git a/libs/ecs/include/psemek/ecs/entity_container.hpp b/libs/ecs/include/psemek/ecs/entity_container.hpp index 8dd12165..85d9f16d 100644 --- a/libs/ecs/include/psemek/ecs/entity_container.hpp +++ b/libs/ecs/include/psemek/ecs/entity_container.hpp @@ -12,33 +12,22 @@ namespace psemek::ecs struct entity_container { - entity_handle create() - { - // Specialization for an empty entity to - // prevent creation of an empty uuid array - - detail::component_mask mask; - - auto & table = table_container_.insert<>(mask, {}); - - auto id = entity_list_.create(&table, table.row_count()); - table.push_row(id); - - return {id, 0}; - } - template entity_handle create(Components && ... components) { - util::uuid component_uuids[] { components.uuid()... }; - detail::component_mask mask = component_index_.make_component_mask(util::span(component_uuids)); + detail::component_uuid_holder uuids; + detail::component_mask mask = component_index_.make_component_mask(uuids.get()); - auto & table = table_container_.insert(mask, component_uuids); + auto & table = table_container_.insert(mask, uuids.get()); auto id = entity_list_.create(&table, table.row_count()); - table.push_row_with_components(id, std::move(components)...); + entity_handle handle{id, entity_list_.get_entities()[id].epoch}; + [[maybe_unused]] entity_accessor accessor = get(handle); - return {id, entity_list_.get_entities()[id].epoch}; + table.push_row(id); + ((accessor.get() = std::move(components)), ...); + + return handle; } bool alive(entity_handle const & entity) const @@ -63,16 +52,16 @@ namespace psemek::ecs template void apply(Function && function) { - util::uuid const component_uuids[] { Components::uuid() ... }; + detail::component_uuid_holder uuids; - detail::component_mask mask = component_index_.make_component_mask(util::span(component_uuids)); + detail::component_mask mask = component_index_.make_component_mask(uuids.get()); table_container_.apply([&](detail::table & table){ detail::static_apply_helper apply_helper(table.get_entity_ids(), entity_list_.get_entities()); for (std::size_t i = 0; i < sizeof...(Components); ++i) for (std::size_t j = 0; j < table.component_count(); ++j) - if (component_uuids[i] == table.get_component_uuids()[j]) + if (uuids.get()[i] == table.get_component_uuids()[j]) apply_helper.pointers[i++] = table.get_component_pointers()[j]; for (std::size_t i = 0; i < apply_helper.size(); ++i) @@ -84,21 +73,10 @@ namespace psemek::ecs }, mask); } - template - Component & get(entity_handle const & entity) + entity_accessor get(entity_handle const & entity) { - util::uuid const uuid = Component::uuid(); - auto const & data = entity_list_.get_entities()[entity.id]; - - auto const component_uuids = data.table->get_component_uuids(); - - for (std::size_t i = 0; i < component_uuids.size(); ++i) - if (uuid == component_uuids[i]) - return *reinterpret_cast(data.table->get_component_pointers()[i].data + detail::stride() * data.row); - - assert(false); - __builtin_unreachable(); + return {data.table, data.row}; } private: diff --git a/libs/ecs/source/detail/entity_list.cpp b/libs/ecs/source/detail/entity_list.cpp index 25b4bf2f..803200bf 100644 --- a/libs/ecs/source/detail/entity_list.cpp +++ b/libs/ecs/source/detail/entity_list.cpp @@ -25,10 +25,8 @@ namespace psemek::ecs::detail void entity_list::allocate_ids() { - static constexpr std::size_t batch_size = 1024; - auto old_size = entities_.size(); - entities_.resize(entities_.size() + batch_size); + entities_.resize(std::max(1024, entities_.size() * 2)); for (std::size_t id = entities_.size(); id --> old_size;) free_ids_.push_back(id); } diff --git a/libs/ecs/tests/apply.cpp b/libs/ecs/tests/apply.cpp new file mode 100644 index 00000000..1a5ba187 --- /dev/null +++ b/libs/ecs/tests/apply.cpp @@ -0,0 +1,113 @@ +#include + +#include +#include +#include + +using namespace psemek; +using namespace psemek::ecs; + +namespace +{ + + struct component_1 + { + int value; + + static constexpr util::uuid uuid() + { + return {1, 0}; + } + }; + + struct component_2 + { + int value; + + static constexpr util::uuid uuid() + { + return {2, 0}; + } + }; + +} + +test_case(ecs_apply_empty) +{ + entity_container container; + + int const count = 2048; + for (int i = 0; i < count; ++i) + container.create(); + + int call_count = 0; + container.apply<>([&](ecs::entity_handle const &){ ++call_count; }); + expect_equal(count, call_count); +} + +test_case(ecs_apply_components_1) +{ + entity_container container; + random::generator rng; + + int const expected_count = 1024 * 1024; + int expected_sum = 0; + + for (int i = 0; i < expected_count; ++i) + { + int value = random::uniform(rng, -1024, 1024); + expected_sum += value; + container.create(component_1{value}); + } + + int count = 0; + int sum = 0; + container.apply([&](ecs::entity_handle const &, component_1 const & component){ + ++count; + sum += component.value; + }); + + expect_equal(count, expected_count); + expect_equal(sum, expected_sum); +} + +test_case(ecs_apply_components_2) +{ + entity_container container; + random::generator rng; + + int const expected_count = 1024*1024; + int expected_sum = 0; + + for (int i = 0; i < expected_count; ++i) + { + int value = random::uniform(rng, -1024, 1024); + int type = random::uniform(rng, 0, 2); + if (type == 0) + container.create(component_1{value}); + else if (type == 1) + container.create(component_2{value}); + else if (type == 2) + container.create(component_1{value}, component_2{value}); + + expected_sum += value; + } + + int count = 0; + int sum = 0; + container.apply([&](ecs::entity_handle const &, component_1 const & component){ + ++count; + sum += component.value; + }); + container.apply([&](ecs::entity_handle const &, component_2 const & component){ + ++count; + sum += component.value; + }); + container.apply([&](ecs::entity_handle const &, component_1 const & component1, component_2 const &){ + --count; + sum -= component1.value; + }); + + expect_equal(count, expected_count); + expect_equal(sum, expected_sum); +} diff --git a/libs/ecs/tests/component.cpp b/libs/ecs/tests/component.cpp new file mode 100644 index 00000000..65dd7a39 --- /dev/null +++ b/libs/ecs/tests/component.cpp @@ -0,0 +1,120 @@ +#include + +#include +#include +#include + +using namespace psemek; +using namespace psemek::ecs; + +namespace +{ + + struct component_small + { + int value; + + static constexpr util::uuid uuid() + { + return {1, 0}; + } + }; + + struct component_big + { + int values[16]; + + static constexpr util::uuid uuid() + { + return {2, 0}; + } + }; + + struct component_noncopyable + { + std::unique_ptr value; + + static constexpr util::uuid uuid() + { + return {2, 0}; + } + }; + + struct component_counter + { + std::shared_ptr value; + + static constexpr util::uuid uuid() + { + return {2, 0}; + } + }; + +} + +test_case(ecs_component_order) +{ + entity_container container; + + auto h0 = container.create(component_small{10}, component_big{}); + expect(container.alive(h0)); + expect_equal(container.get(h0).get().value, 10); + + auto h1 = container.create(component_big{}, component_small{20}); + expect(container.alive(h0)); + expect(container.alive(h1)); + expect_equal(container.get(h0).get().value, 10); + expect_equal(container.get(h1).get().value, 20); + + auto h2 = container.create(component_small{30}); + expect(container.alive(h0)); + expect(container.alive(h1)); + expect(container.alive(h2)); + expect_equal(container.get(h0).get().value, 10); + expect_equal(container.get(h1).get().value, 20); + expect_equal(container.get(h2).get().value, 30); +} + +test_case(ecs_component_noncopyable) +{ + entity_container container; + + auto h0 = container.create(component_noncopyable{std::make_unique(10)}); + expect(container.alive(h0)); + expect_equal(*container.get(h0).get().value, 10); + + auto h1 = container.create(component_noncopyable{std::make_unique(20)}); + expect(container.alive(h0)); + expect(container.alive(h1)); + expect_equal(*container.get(h0).get().value, 10); + expect_equal(*container.get(h1).get().value, 20); + + container.destroy(h0); + expect(!container.alive(h0)); + expect(container.alive(h1)); + expect_equal(*container.get(h1).get().value, 20); +} + +test_case(ecs_component_lifetime) +{ + random::generator rng; + entity_container container; + + std::vector entities; + + for (int i = 0; i < 1024 * 1024; ++i) + entities.push_back(container.create(component_counter{std::make_shared(42)})); + + container.apply([&](entity_handle const &, component_counter const & component){ + expect_equal(component.value.use_count(), 1); + }); + + std::shuffle(entities.begin(), entities.end(), rng); + + for (int i = 0; i < entities.size() / 2; ++i) + container.destroy(entities[i]); + + container.apply([&](entity_handle const &, component_counter const & component){ + expect_equal(component.value.use_count(), 1); + }); +} diff --git a/libs/ecs/tests/entity.cpp b/libs/ecs/tests/entity.cpp new file mode 100644 index 00000000..9fd3e52c --- /dev/null +++ b/libs/ecs/tests/entity.cpp @@ -0,0 +1,147 @@ +#include + +#include +#include +#include + +using namespace psemek; +using namespace psemek::ecs; + +namespace +{ + + struct component_1 + { + int value; + + static constexpr util::uuid uuid() + { + return {1, 0}; + } + }; + + struct component_2 + { + int value; + + static constexpr util::uuid uuid() + { + return {2, 0}; + } + }; + +} + +test_case(ecs_entity_empty_single) +{ + entity_container container; + + auto h0 = container.create(); + expect(container.alive(h0)); + + container.destroy(h0); + expect(!container.alive(h0)); +} + +test_case(ecs_entity_empty_multiple) +{ + entity_container container; + + auto h0 = container.create(); + expect(container.alive(h0)); + + auto h1 = container.create(); + expect(container.alive(h1)); + + auto h2 = container.create(); + expect(container.alive(h2)); + + container.destroy(h1); + expect(!container.alive(h1)); + + container.destroy(h0); + expect(!container.alive(h0)); + + container.destroy(h2); + expect(!container.alive(h2)); +} + +test_case(ecs_entity_components_single) +{ + entity_container container; + + auto h0 = container.create(component_1{10}, component_2{20}); + expect(container.alive(h0)); + expect_equal(container.get(h0).get().value, 10); + expect_equal(container.get(h0).get().value, 20); + + container.destroy(h0); + expect(!container.alive(h0)); + + h0 = container.create(component_2{200}, component_1{100}); + expect(container.alive(h0)); + expect_equal(container.get(h0).get().value, 100); + expect_equal(container.get(h0).get().value, 200); +} + +test_case(ecs_entity_components_multiple) +{ + entity_container container; + + auto h0 = container.create(component_1{10}, component_2{20}); + expect(container.alive(h0)); + expect_equal(container.get(h0).get().value, 10); + expect_equal(container.get(h0).get().value, 20); + + auto h1 = container.create(component_1{30}, component_2{40}); + expect(container.alive(h1)); + expect_equal(container.get(h1).get().value, 30); + expect_equal(container.get(h1).get().value, 40); + + auto h2 = container.create(component_1{50}, component_2{60}); + expect(container.alive(h2)); + expect_equal(container.get(h2).get().value, 50); + expect_equal(container.get(h2).get().value, 60); + + container.destroy(h0); + expect(!container.alive(h0)); + + container.destroy(h2); + expect(!container.alive(h2)); + + container.destroy(h1); + expect(!container.alive(h1)); +} + +test_case(ecs_entity_random) +{ + random::generator rng; + entity_container container; + + std::vector> entities; + + for (int i = 0; i < 1024 * 1024; ++i) + { + int value = random::uniform(rng, -1024, 1024); + entity_handle handle; + if (random::uniform(rng)) + handle = container.create(component_1{value}); + else + handle = container.create(component_1{value}, component_2{-value}); + entities.push_back({handle, value}); + } + + std::shuffle(entities.begin(), entities.end(), rng); + + while (!entities.empty()) + { + auto entity = entities.back(); + entities.pop_back(); + + expect(container.alive(entity.first)); + expect_equal(container.get(entity.first).get().value, entity.second); + + container.destroy(entity.first); + expect(!container.alive(entity.first)); + } +}