From 8b1157c641c7aec89db6bb1332c93a4d1cafc3cd Mon Sep 17 00:00:00 2001 From: lisyarus Date: Sat, 26 Aug 2023 18:24:10 +0300 Subject: [PATCH] Implement ecs::entity_container::attach/detach --- libs/ecs/include/psemek/ecs/detail/column.hpp | 19 +- libs/ecs/include/psemek/ecs/detail/table.hpp | 9 +- .../psemek/ecs/detail/table_container.hpp | 19 +- .../include/psemek/ecs/entity_container.hpp | 123 +++++++-- libs/ecs/source/detail/table.cpp | 45 ++-- libs/ecs/source/entity_container.cpp | 39 ++- libs/ecs/tests/attach.cpp | 242 ++++++++++++++++++ 7 files changed, 434 insertions(+), 62 deletions(-) create mode 100644 libs/ecs/tests/attach.cpp diff --git a/libs/ecs/include/psemek/ecs/detail/column.hpp b/libs/ecs/include/psemek/ecs/detail/column.hpp index b6bcf46c..44c3efbc 100644 --- a/libs/ecs/include/psemek/ecs/detail/column.hpp +++ b/libs/ecs/include/psemek/ecs/detail/column.hpp @@ -10,15 +10,16 @@ namespace psemek::ecs::detail struct column { - column(util::uuid const & uuid) - : uuid_(uuid) - {} - std::uint8_t * data() { return data_; } + std::size_t stride() const + { + return stride_; + } + util::uuid const & uuid() const { return uuid_; @@ -36,7 +37,13 @@ namespace psemek::ecs::detail protected: std::uint8_t * data_ = nullptr; + std::size_t stride_; util::uuid uuid_; + + column(std::size_t stride, util::uuid const & uuid) + : stride_(stride) + , uuid_(uuid) + {} }; template > @@ -81,7 +88,7 @@ namespace psemek::ecs::detail template column_impl::column_impl() - : column(Component::uuid()) + : column(detail::stride(), Component::uuid()) {} template @@ -177,7 +184,7 @@ namespace psemek::ecs::detail template column_impl::column_impl() - : column(Component::uuid()) + : column(detail::stride(), Component::uuid()) { data_ = reinterpret_cast(new Component[1]); } diff --git a/libs/ecs/include/psemek/ecs/detail/table.hpp b/libs/ecs/include/psemek/ecs/detail/table.hpp index ac00a186..afec0ac7 100644 --- a/libs/ecs/include/psemek/ecs/detail/table.hpp +++ b/libs/ecs/include/psemek/ecs/detail/table.hpp @@ -33,6 +33,11 @@ namespace psemek::ecs::detail return entity_handles_; } + util::span const> columns() const + { + return columns_; + } + std::size_t column_count() const { return component_uuid_to_column_.size(); @@ -44,6 +49,7 @@ namespace psemek::ecs::detail } std::size_t push_row(entity_handle handle); + std::size_t move_row(entity_handle handle, table * from, std::size_t from_row); void swap_rows(std::size_t row1, std::size_t row2); void pop_row(); void clear(); @@ -68,7 +74,8 @@ namespace psemek::ecs::detail protected: std::size_t hash_; - util::hash_map> component_uuid_to_column_; + std::vector> columns_; + util::hash_map component_uuid_to_column_; std::vector entity_handles_; diff --git a/libs/ecs/include/psemek/ecs/detail/table_container.hpp b/libs/ecs/include/psemek/ecs/detail/table_container.hpp index 72a4c332..ad26bdad 100644 --- a/libs/ecs/include/psemek/ecs/detail/table_container.hpp +++ b/libs/ecs/include/psemek/ecs/detail/table_container.hpp @@ -43,8 +43,8 @@ namespace psemek::ecs::detail // TODO: store tables in a bitmask trie balanced by subtree size struct table_container { - template - std::pair insert(util::span component_uuids); + table * get(util::span component_uuids); + table * insert(std::unique_ptr
table); template void apply(Function && function, util::span component_uuids); @@ -53,20 +53,19 @@ namespace psemek::ecs::detail util::hash_set, table_hashset_hash, table_hashset_equal> tables_; }; - template - std::pair
table_container::insert(util::span component_uuids) + inline table * table_container::get(util::span component_uuids) { auto it = tables_.find(component_uuids); if (it != tables_.end()) - return {it->get(), false}; + return it->get(); + return nullptr; + } - std::vector> columns; - (columns.push_back(std::make_unique>()), ...); - - auto table = std::make_unique(std::move(columns)); + inline table * table_container::insert(std::unique_ptr
table) + { auto result = table.get(); tables_.insert(std::move(table)); - return {result, true}; + return result; } template diff --git a/libs/ecs/include/psemek/ecs/entity_container.hpp b/libs/ecs/include/psemek/ecs/entity_container.hpp index 0c2dd065..cb703d38 100644 --- a/libs/ecs/include/psemek/ecs/entity_container.hpp +++ b/libs/ecs/include/psemek/ecs/entity_container.hpp @@ -122,23 +122,29 @@ namespace psemek::ecs entity_accessor get(entity_handle const & entity); /** Attach new components to an existing entity, or update existing - * components with new values. + * components with new values. Other components of this entity + * are left untouched. + * Attaching components invalidates all previously created entity accessors. + * For the purposes of `apply()` semantics, attaching behaves as if the + * entity was destroyed and then recreated with the same handle. * If any two of the passed component types are equal, the call fails with * a compilation error. - * Attaching components invalidates all previously created entity accessors. * If the handle wasn't previously obtained by a `create()` call, or * the refered entity was already destroyed, the behavior is undefined. */ - // TODO: implement template void attach(entity_handle const & entity, Components && ... components); - /** Detach (remove) components from an existing entity. + /** Detach (remove) components from an existing entity. Other components of this entity + * are left untouched. Detaching a component that doesn't exist does nothing. * Detaching components invalidates all previously created entity accessors. + * For the purposes of `apply()` semantics, detaching behaves as if the + * entity was destroyed and then recreated with the same handle. + * If any two of the passed component types are equal, the call fails with + * a compilation error. * If the handle wasn't previously obtained by a `create()` call, or * the refered entity was already destroyed, the behavior is undefined. */ - // TODO: implement template void detach(entity_handle const & entity); @@ -155,10 +161,11 @@ namespace psemek::ecs * void(entity_handle, components...) * void(entity_container, components...) * void(entity_container, entity_handle, components...) - * The function can freely create or destroy entities. It is unspecified - * whether the function will or will not visit newly created entities - * during this `apply()` call. The function is guaranteed not to visit - * destroyed entities (unless it did so before the entity was destroyed). + * The function can freely create or destroy entities, and or attach/detach + * components to existing entities. It is unspecified whether the function + * will or will not visit newly created entities during this `apply()` call. + * The function is guaranteed not to visit destroyed entities (unless it did + * so before the entity was destroyed). * An optional query cache can be supplied to speed up iteration. * If the query cache wasn't created with the exact sequence of component * types, the behavior is undefined. @@ -193,7 +200,10 @@ namespace psemek::ecs detail::query_cache_container query_cache_container_; std::vector uuid_helper_; + util::hash_set uuid_set_helper_; + detail::table * insert_table(std::vector> columns); + void do_destroy(entity_handle const & entity); void remove_row(detail::table & table, std::uint32_t row, util::span entities); }; @@ -204,15 +214,14 @@ namespace psemek::ecs detail::component_uuid_helper uuids; - auto insert_result = table_container_.insert(uuids.get()); - auto table = insert_result.first; - bool created = insert_result.second; + auto table = table_container_.get(uuids.get()); - if (created) + if (!table) { - query_cache_container_.apply([table](detail::query_cache & cache){ - cache.add(table); - }, [table](util::uuid const & uuid){ return table->column(uuid) != nullptr; }); + std::vector> columns; + (columns.push_back(std::make_unique>()), ...); + + table = insert_table(std::move(columns)); } if (table->get_iteration_data()) @@ -228,6 +237,88 @@ namespace psemek::ecs return handle; } + template + void entity_container::attach(entity_handle const & entity, Components && ... components) + { + static_assert(detail::all_different_types_v, "all component types must be different"); + + auto & data = entity_list_.get_entities()[entity.id]; + for (auto const & column : data.table->columns()) + uuid_helper_.push_back(column->uuid()); + + ((data.table->column(Components::uuid()) ? 0 : (uuid_helper_.push_back(Components::uuid()), 0)), ...); + + auto table = table_container_.get(uuid_helper_); + + if (!table) + { + std::vector> columns; + for (auto const & column : data.table->columns()) + columns.push_back(column->clone()); + + ((data.table->column(Components::uuid()) ? 0 : (columns.push_back(std::make_unique>()), 0)), ...); + + table = insert_table(std::move(columns)); + } + + if (table != data.table) + { + if (table->get_iteration_data()) + table = table->get_delayed_table(); + + auto new_row = table->move_row(entity, data.table, data.row); + do_destroy(entity); + + data.table = table; + data.row = new_row; + } + + auto accessor = get(entity); + ((accessor.get() = std::move(components)), ...); + + uuid_helper_.clear(); + } + + template + void entity_container::detach(entity_handle const & entity) + { + static_assert(detail::all_different_types_v, "all component types must be different"); + + (uuid_set_helper_.insert(Components::uuid()), ...); + + auto & data = entity_list_.get_entities()[entity.id]; + for (auto const & column : data.table->columns()) + if (!uuid_set_helper_.contains(column->uuid())) + uuid_helper_.push_back(column->uuid()); + + auto table = table_container_.get(uuid_helper_); + + if (!table) + { + std::vector> columns; + for (auto const & column : data.table->columns()) + if (!uuid_set_helper_.contains(column->uuid())) + columns.push_back(column->clone()); + + table = insert_table(std::move(columns)); + } + + if (table != data.table) + { + if (table->get_iteration_data()) + table = table->get_delayed_table(); + + auto new_row = table->move_row(entity, data.table, data.row); + do_destroy(entity); + + data.table = table; + data.row = new_row; + } + + uuid_set_helper_.clear(); + uuid_helper_.clear(); + } + template query_cache entity_container::cache() { diff --git a/libs/ecs/source/detail/table.cpp b/libs/ecs/source/detail/table.cpp index 71b2f808..ca9f70d5 100644 --- a/libs/ecs/source/detail/table.cpp +++ b/libs/ecs/source/detail/table.cpp @@ -11,44 +11,59 @@ namespace psemek::ecs::detail { auto uuid = column->uuid(); hasher(uuid); - component_uuid_to_column_.insert({uuid, std::move(column)}); + component_uuid_to_column_.insert({uuid, column.get()}); } hash_ = hasher.result; + + columns_ = std::move(columns); } detail::column * table::column(util::uuid const & uuid) const { if (auto it = component_uuid_to_column_.find(uuid); it != component_uuid_to_column_.end()) - return it->second.get(); + return it->second; return nullptr; } std::size_t table::push_row(entity_handle handle) { - for (auto & pair : component_uuid_to_column_) - pair.second->push_row(); + for (auto & column : columns_) + column->push_row(); + entity_handles_.push_back(handle); + return entity_handles_.size() - 1; + } + + std::size_t table::move_row(entity_handle handle, table * from, std::size_t from_row) + { + for (auto & column : columns_) + { + if (auto other_column = from->column(column->uuid())) + column->emplace_rows(other_column->data() + other_column->stride() * from_row, 1); + else + column->push_row(); + } entity_handles_.push_back(handle); return entity_handles_.size() - 1; } void table::swap_rows(std::size_t row1, std::size_t row2) { - for (auto & pair : component_uuid_to_column_) - pair.second->swap_rows(row1, row2); + for (auto & column : columns_) + column->swap_rows(row1, row2); std::swap(entity_handles_[row1], entity_handles_[row2]); } void table::pop_row() { - for (auto & pair : component_uuid_to_column_) - pair.second->pop_row(); + for (auto & column : columns_) + column->pop_row(); entity_handles_.pop_back(); } void table::clear() { - for (auto & pair : component_uuid_to_column_) - pair.second->clear(); + for (auto & column : columns_) + column->clear(); entity_handles_.clear(); } @@ -65,8 +80,8 @@ namespace psemek::ecs::detail std::unique_ptr
table::clone() const { std::vector> columns; - for (auto const & pair : component_uuid_to_column_) - columns.push_back(pair.second->clone()); + for (auto const & column : columns_) + columns.push_back(column->clone()); return std::make_unique
(std::move(columns)); } @@ -83,10 +98,10 @@ namespace psemek::ecs::detail return {}; std::size_t count = delayed_table_->row_count(); - for (auto & pair : component_uuid_to_column_) + for (std::size_t i = 0; i < columns_.size(); ++i) { - auto * src_column = delayed_table_->column(pair.second->uuid()); - pair.second->emplace_rows(src_column->data(), count); + auto * src_column = delayed_table_->columns_[i].get(); + columns_[i]->emplace_rows(src_column->data(), count); } entity_handles_.insert(entity_handles_.end(), delayed_table_->entity_handles_.begin(), delayed_table_->entity_handles_.end()); diff --git a/libs/ecs/source/entity_container.cpp b/libs/ecs/source/entity_container.cpp index e9bdaff0..d1cdcd89 100644 --- a/libs/ecs/source/entity_container.cpp +++ b/libs/ecs/source/entity_container.cpp @@ -10,20 +10,8 @@ namespace psemek::ecs void entity_container::destroy(entity_handle const & entity) { - auto entities = entity_list_.get_entities(); - auto & data = entities[entity.id]; - auto & iteration_data = data.table->get_iteration_data(); - - if (!iteration_data || iteration_data->current_row < data.row) - { - remove_row(*data.table, data.row, entities); - entity_list_.destroy(entity.id); - } - else - { - data.table->push_remove(data.row); - entity_list_.destroy(entity.id); - } + do_destroy(entity); + entity_list_.destroy(entity.id); } entity_accessor entity_container::get(entity_handle const & entity) @@ -32,6 +20,29 @@ namespace psemek::ecs return {data.table, data.row}; } + detail::table * entity_container::insert_table(std::vector> columns) + { + auto table = table_container_.insert(std::make_unique(std::move(columns))); + + query_cache_container_.apply([table](detail::query_cache & cache){ + cache.add(table); + }, [table](util::uuid const & uuid){ return table->column(uuid) != nullptr; }); + + return table; + } + + void entity_container::do_destroy(entity_handle const & entity) + { + auto entities = entity_list_.get_entities(); + auto & data = entities[entity.id]; + auto & iteration_data = data.table->get_iteration_data(); + + if (!iteration_data || iteration_data->current_row < data.row) + remove_row(*data.table, data.row, entities); + else + data.table->push_remove(data.row); + } + void entity_container::remove_row(detail::table & table, std::uint32_t row, util::span entities) { // Swap with the last row in that table diff --git a/libs/ecs/tests/attach.cpp b/libs/ecs/tests/attach.cpp new file mode 100644 index 00000000..b472166e --- /dev/null +++ b/libs/ecs/tests/attach.cpp @@ -0,0 +1,242 @@ +#include + +#include +#include +#include +#include + +using namespace psemek; +using namespace psemek::ecs; + +namespace +{ + + struct component_1 + { + int value; + + psemek_declare_uuid("component_1") + }; + + struct component_2 + { + int value; + + psemek_declare_uuid("component_2") + }; + + void check_impl(entity_container & container, int count0, int count1, int count2, int count12) + { + int call_count = 0; + container.apply<>([&]{ ++call_count; }); + expect_equal(call_count, count0 + count1 + count2 + count12); + + call_count = 0; + container.apply([&](component_1 const &){ ++call_count; }); + expect_equal(call_count, count1 + count12); + + call_count = 0; + container.apply([&](component_2 const &){ ++call_count; }); + expect_equal(call_count, count2 + count12); + + call_count = 0; + container.apply([&](component_1 const &, component_2 const &){ ++call_count; }); + expect_equal(call_count, count12); + } + +} + +test_case(ecs_attach_empty) +{ + entity_container container; + + auto h = container.create(); + + expect(container.get(h).get_if() == nullptr); + expect(container.get(h).get_if() == nullptr); + + check_impl(container, 1, 0, 0, 0); +} + +test_case(ecs_attach_one) +{ + entity_container container; + + auto h = container.create(); + + container.attach(h, component_1{10}); + + expect_equal(container.get(h).get().value, 10); + expect(container.get(h).get_if() == nullptr); + + check_impl(container, 0, 1, 0, 0); +} + +test_case(ecs_attach_twice) +{ + entity_container container; + + auto h = container.create(); + + container.attach(h, component_1{10}); + container.attach(h, component_1{11}); + + expect_equal(container.get(h).get().value, 11); + expect(container.get(h).get_if() == nullptr); + + check_impl(container, 0, 1, 0, 0); +} + +test_case(ecs_attach_two) +{ + entity_container container; + + auto h = container.create(); + + container.attach(h, component_1{10}); + container.attach(h, component_2{20}); + + expect_equal(container.get(h).get().value, 10); + expect_equal(container.get(h).get().value, 20); + + check_impl(container, 0, 0, 0, 1); +} + +test_case(ecs_detach_empty) +{ + entity_container container; + + auto h = container.create(); + + container.detach(h); + + expect(container.get(h).get_if() == nullptr); + expect(container.get(h).get_if() == nullptr); + + check_impl(container, 1, 0, 0, 0); +} + +test_case(ecs_detach_nonexistent) +{ + entity_container container; + + auto h = container.create(component_1{10}); + + container.detach(h); + + expect_equal(container.get(h).get().value, 10); + expect(container.get(h).get_if() == nullptr); + + check_impl(container, 0, 1, 0, 0); +} + +test_case(ecs_detach_one) +{ + entity_container container; + + auto h = container.create(component_1{10}, component_2{20}); + + container.detach(h); + + expect_equal(container.get(h).get().value, 10); + expect(container.get(h).get_if() == nullptr); + + check_impl(container, 0, 1, 0, 0); +} + +test_case(ecs_detach_two) +{ + entity_container container; + + auto h = container.create(component_1{10}, component_2{20}); + + container.detach(h); + container.detach(h); + + expect(container.get(h).get_if() == nullptr); + expect(container.get(h).get_if() == nullptr); + + check_impl(container, 1, 0, 0, 0); +} + +test_case(ecs_attach_random) +{ + random::generator rng; + entity_container container; + + int count0 = 0; + int count1 = 0; + int count2 = 0; + int count12 = 0; + + for (int i = 0; i < 1024 * 1024; ++i) + { + auto h = container.create(); + + bool has1 = random::uniform(rng); + bool has2 = random::uniform(rng); + + if (has1 && has2) + { + container.attach(h, component_1{i}, component_2{2 * i}); + ++count12; + } + else if (has1 && !has2) + { + container.attach(h, component_1{i}); + ++count1; + } + else if (!has1 && has2) + { + container.attach(h, component_2{2 * i}); + ++count2; + } + else + { + ++count0; + } + } + + check_impl(container, count0, count1, count2, count12); +} + +test_case(ecs_detach_random) +{ + random::generator rng; + entity_container container; + + int count0 = 0; + int count1 = 0; + int count2 = 0; + int count12 = 0; + + for (int i = 0; i < 1024 * 1024; ++i) + { + auto h = container.create(component_1{i}, component_2{2 * i}); + + bool has1 = random::uniform(rng); + bool has2 = random::uniform(rng); + + if (has1 && has2) + { + ++count12; + } + else if (has1 && !has2) + { + container.detach(h); + ++count1; + } + else if (!has1 && has2) + { + container.detach(h); + ++count2; + } + else + { + container.detach(h); + ++count0; + } + } + + check_impl(container, count0, count1, count2, count12); +}