Implement ecs::entity_container::attach/detach

This commit is contained in:
Nikita Lisitsa 2023-08-26 18:24:10 +03:00
parent 59c803d31c
commit 8b1157c641
7 changed files with 434 additions and 62 deletions

View file

@ -10,15 +10,16 @@ namespace psemek::ecs::detail
struct column struct column
{ {
column(util::uuid const & uuid)
: uuid_(uuid)
{}
std::uint8_t * data() std::uint8_t * data()
{ {
return data_; return data_;
} }
std::size_t stride() const
{
return stride_;
}
util::uuid const & uuid() const util::uuid const & uuid() const
{ {
return uuid_; return uuid_;
@ -36,7 +37,13 @@ namespace psemek::ecs::detail
protected: protected:
std::uint8_t * data_ = nullptr; std::uint8_t * data_ = nullptr;
std::size_t stride_;
util::uuid uuid_; util::uuid uuid_;
column(std::size_t stride, util::uuid const & uuid)
: stride_(stride)
, uuid_(uuid)
{}
}; };
template <typename Component, bool Empty = detail::is_empty_v<Component>> template <typename Component, bool Empty = detail::is_empty_v<Component>>
@ -81,7 +88,7 @@ namespace psemek::ecs::detail
template <typename Component, bool Empty> template <typename Component, bool Empty>
column_impl<Component, Empty>::column_impl() column_impl<Component, Empty>::column_impl()
: column(Component::uuid()) : column(detail::stride<Component>(), Component::uuid())
{} {}
template <typename Component, bool Empty> template <typename Component, bool Empty>
@ -177,7 +184,7 @@ namespace psemek::ecs::detail
template <typename Component> template <typename Component>
column_impl<Component, true>::column_impl() column_impl<Component, true>::column_impl()
: column(Component::uuid()) : column(detail::stride<Component>(), Component::uuid())
{ {
data_ = reinterpret_cast<std::uint8_t *>(new Component[1]); data_ = reinterpret_cast<std::uint8_t *>(new Component[1]);
} }

View file

@ -33,6 +33,11 @@ namespace psemek::ecs::detail
return entity_handles_; return entity_handles_;
} }
util::span<std::unique_ptr<detail::column> const> columns() const
{
return columns_;
}
std::size_t column_count() const std::size_t column_count() const
{ {
return component_uuid_to_column_.size(); 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 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 swap_rows(std::size_t row1, std::size_t row2);
void pop_row(); void pop_row();
void clear(); void clear();
@ -68,7 +74,8 @@ namespace psemek::ecs::detail
protected: protected:
std::size_t hash_; std::size_t hash_;
util::hash_map<util::uuid, std::unique_ptr<detail::column>> component_uuid_to_column_; std::vector<std::unique_ptr<detail::column>> columns_;
util::hash_map<util::uuid, detail::column *> component_uuid_to_column_;
std::vector<entity_handle> entity_handles_; std::vector<entity_handle> entity_handles_;

View file

@ -43,8 +43,8 @@ namespace psemek::ecs::detail
// TODO: store tables in a bitmask trie balanced by subtree size // TODO: store tables in a bitmask trie balanced by subtree size
struct table_container struct table_container
{ {
template <typename ... Components> table * get(util::span<util::uuid const> component_uuids);
std::pair<table *, bool> insert(util::span<util::uuid const> component_uuids); table * insert(std::unique_ptr<table> table);
template <typename Function> template <typename Function>
void apply(Function && function, util::span<util::uuid const> component_uuids); void apply(Function && function, util::span<util::uuid const> component_uuids);
@ -53,20 +53,19 @@ namespace psemek::ecs::detail
util::hash_set<std::unique_ptr<table>, table_hashset_hash, table_hashset_equal> tables_; util::hash_set<std::unique_ptr<table>, table_hashset_hash, table_hashset_equal> tables_;
}; };
template <typename ... Components> inline table * table_container::get(util::span<util::uuid const> component_uuids)
std::pair<table *, bool> table_container::insert(util::span<util::uuid const> component_uuids)
{ {
auto it = tables_.find(component_uuids); auto it = tables_.find(component_uuids);
if (it != tables_.end()) if (it != tables_.end())
return {it->get(), false}; return it->get();
return nullptr;
}
std::vector<std::unique_ptr<column>> columns; inline table * table_container::insert(std::unique_ptr<table> table)
(columns.push_back(std::make_unique<column_impl<Components>>()), ...); {
auto table = std::make_unique<detail::table>(std::move(columns));
auto result = table.get(); auto result = table.get();
tables_.insert(std::move(table)); tables_.insert(std::move(table));
return {result, true}; return result;
} }
template <typename Function> template <typename Function>

View file

@ -122,23 +122,29 @@ namespace psemek::ecs
entity_accessor get(entity_handle const & entity); entity_accessor get(entity_handle const & entity);
/** Attach new components to an existing entity, or update existing /** 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 * If any two of the passed component types are equal, the call fails with
* a compilation error. * a compilation error.
* Attaching components invalidates all previously created entity accessors.
* If the handle wasn't previously obtained by a `create()` call, or * If the handle wasn't previously obtained by a `create()` call, or
* the refered entity was already destroyed, the behavior is undefined. * the refered entity was already destroyed, the behavior is undefined.
*/ */
// TODO: implement
template <typename ... Components> template <typename ... Components>
void attach(entity_handle const & entity, Components && ... components); 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. * 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 * If the handle wasn't previously obtained by a `create()` call, or
* the refered entity was already destroyed, the behavior is undefined. * the refered entity was already destroyed, the behavior is undefined.
*/ */
// TODO: implement
template <typename ... Components> template <typename ... Components>
void detach(entity_handle const & entity); void detach(entity_handle const & entity);
@ -155,10 +161,11 @@ namespace psemek::ecs
* void(entity_handle, components...) * void(entity_handle, components...)
* void(entity_container, components...) * void(entity_container, components...)
* void(entity_container, entity_handle, components...) * void(entity_container, entity_handle, components...)
* The function can freely create or destroy entities. It is unspecified * The function can freely create or destroy entities, and or attach/detach
* whether the function will or will not visit newly created entities * components to existing entities. It is unspecified whether the function
* during this `apply()` call. The function is guaranteed not to visit * will or will not visit newly created entities during this `apply()` call.
* destroyed entities (unless it did so before the entity was destroyed). * 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. * An optional query cache can be supplied to speed up iteration.
* If the query cache wasn't created with the exact sequence of component * If the query cache wasn't created with the exact sequence of component
* types, the behavior is undefined. * types, the behavior is undefined.
@ -193,7 +200,10 @@ namespace psemek::ecs
detail::query_cache_container query_cache_container_; detail::query_cache_container query_cache_container_;
std::vector<util::uuid> uuid_helper_; std::vector<util::uuid> uuid_helper_;
util::hash_set<util::uuid> uuid_set_helper_;
detail::table * insert_table(std::vector<std::unique_ptr<detail::column>> columns);
void do_destroy(entity_handle const & entity);
void remove_row(detail::table & table, std::uint32_t row, util::span<detail::entity_data> entities); void remove_row(detail::table & table, std::uint32_t row, util::span<detail::entity_data> entities);
}; };
@ -204,15 +214,14 @@ namespace psemek::ecs
detail::component_uuid_helper<Components...> uuids; detail::component_uuid_helper<Components...> uuids;
auto insert_result = table_container_.insert<Components...>(uuids.get()); auto table = table_container_.get(uuids.get());
auto table = insert_result.first;
bool created = insert_result.second;
if (created) if (!table)
{ {
query_cache_container_.apply([table](detail::query_cache & cache){ std::vector<std::unique_ptr<detail::column>> columns;
cache.add(table); (columns.push_back(std::make_unique<detail::column_impl<Components>>()), ...);
}, [table](util::uuid const & uuid){ return table->column(uuid) != nullptr; });
table = insert_table(std::move(columns));
} }
if (table->get_iteration_data()) if (table->get_iteration_data())
@ -228,6 +237,88 @@ namespace psemek::ecs
return handle; return handle;
} }
template <typename ... Components>
void entity_container::attach(entity_handle const & entity, Components && ... components)
{
static_assert(detail::all_different_types_v<Components...>, "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<std::unique_ptr<detail::column>> 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<detail::column_impl<Components>>()), 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<Components>() = std::move(components)), ...);
uuid_helper_.clear();
}
template <typename ... Components>
void entity_container::detach(entity_handle const & entity)
{
static_assert(detail::all_different_types_v<Components...>, "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<std::unique_ptr<detail::column>> 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 <typename ... Components> template <typename ... Components>
query_cache entity_container::cache() query_cache entity_container::cache()
{ {

View file

@ -11,44 +11,59 @@ namespace psemek::ecs::detail
{ {
auto uuid = column->uuid(); auto uuid = column->uuid();
hasher(uuid); hasher(uuid);
component_uuid_to_column_.insert({uuid, std::move(column)}); component_uuid_to_column_.insert({uuid, column.get()});
} }
hash_ = hasher.result; hash_ = hasher.result;
columns_ = std::move(columns);
} }
detail::column * table::column(util::uuid const & uuid) const detail::column * table::column(util::uuid const & uuid) const
{ {
if (auto it = component_uuid_to_column_.find(uuid); it != component_uuid_to_column_.end()) if (auto it = component_uuid_to_column_.find(uuid); it != component_uuid_to_column_.end())
return it->second.get(); return it->second;
return nullptr; return nullptr;
} }
std::size_t table::push_row(entity_handle handle) std::size_t table::push_row(entity_handle handle)
{ {
for (auto & pair : component_uuid_to_column_) for (auto & column : columns_)
pair.second->push_row(); 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); entity_handles_.push_back(handle);
return entity_handles_.size() - 1; return entity_handles_.size() - 1;
} }
void table::swap_rows(std::size_t row1, std::size_t row2) void table::swap_rows(std::size_t row1, std::size_t row2)
{ {
for (auto & pair : component_uuid_to_column_) for (auto & column : columns_)
pair.second->swap_rows(row1, row2); column->swap_rows(row1, row2);
std::swap(entity_handles_[row1], entity_handles_[row2]); std::swap(entity_handles_[row1], entity_handles_[row2]);
} }
void table::pop_row() void table::pop_row()
{ {
for (auto & pair : component_uuid_to_column_) for (auto & column : columns_)
pair.second->pop_row(); column->pop_row();
entity_handles_.pop_back(); entity_handles_.pop_back();
} }
void table::clear() void table::clear()
{ {
for (auto & pair : component_uuid_to_column_) for (auto & column : columns_)
pair.second->clear(); column->clear();
entity_handles_.clear(); entity_handles_.clear();
} }
@ -65,8 +80,8 @@ namespace psemek::ecs::detail
std::unique_ptr<table> table::clone() const std::unique_ptr<table> table::clone() const
{ {
std::vector<std::unique_ptr<detail::column>> columns; std::vector<std::unique_ptr<detail::column>> columns;
for (auto const & pair : component_uuid_to_column_) for (auto const & column : columns_)
columns.push_back(pair.second->clone()); columns.push_back(column->clone());
return std::make_unique<table>(std::move(columns)); return std::make_unique<table>(std::move(columns));
} }
@ -83,10 +98,10 @@ namespace psemek::ecs::detail
return {}; return {};
std::size_t count = delayed_table_->row_count(); 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()); auto * src_column = delayed_table_->columns_[i].get();
pair.second->emplace_rows(src_column->data(), count); columns_[i]->emplace_rows(src_column->data(), count);
} }
entity_handles_.insert(entity_handles_.end(), delayed_table_->entity_handles_.begin(), delayed_table_->entity_handles_.end()); entity_handles_.insert(entity_handles_.end(), delayed_table_->entity_handles_.begin(), delayed_table_->entity_handles_.end());

View file

@ -10,20 +10,8 @@ namespace psemek::ecs
void entity_container::destroy(entity_handle const & entity) void entity_container::destroy(entity_handle const & entity)
{ {
auto entities = entity_list_.get_entities(); do_destroy(entity);
auto & data = entities[entity.id]; entity_list_.destroy(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);
}
} }
entity_accessor entity_container::get(entity_handle const & entity) entity_accessor entity_container::get(entity_handle const & entity)
@ -32,6 +20,29 @@ namespace psemek::ecs
return {data.table, data.row}; return {data.table, data.row};
} }
detail::table * entity_container::insert_table(std::vector<std::unique_ptr<detail::column>> columns)
{
auto table = table_container_.insert(std::make_unique<detail::table>(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<detail::entity_data> entities) void entity_container::remove_row(detail::table & table, std::uint32_t row, util::span<detail::entity_data> entities)
{ {
// Swap with the last row in that table // Swap with the last row in that table

242
libs/ecs/tests/attach.cpp Normal file
View file

@ -0,0 +1,242 @@
#include <psemek/test/test.hpp>
#include <psemek/ecs/entity_container.hpp>
#include <psemek/ecs/declare_uuid.hpp>
#include <psemek/random/generator.hpp>
#include <psemek/random/uniform.hpp>
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>([&](component_1 const &){ ++call_count; });
expect_equal(call_count, count1 + count12);
call_count = 0;
container.apply<component_2>([&](component_2 const &){ ++call_count; });
expect_equal(call_count, count2 + count12);
call_count = 0;
container.apply<component_1, component_2>([&](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<component_1>() == nullptr);
expect(container.get(h).get_if<component_2>() == 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<component_1>().value, 10);
expect(container.get(h).get_if<component_2>() == 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<component_1>().value, 11);
expect(container.get(h).get_if<component_2>() == 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<component_1>().value, 10);
expect_equal(container.get(h).get<component_2>().value, 20);
check_impl(container, 0, 0, 0, 1);
}
test_case(ecs_detach_empty)
{
entity_container container;
auto h = container.create();
container.detach<component_1>(h);
expect(container.get(h).get_if<component_1>() == nullptr);
expect(container.get(h).get_if<component_2>() == 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<component_2>(h);
expect_equal(container.get(h).get<component_1>().value, 10);
expect(container.get(h).get_if<component_2>() == 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<component_2>(h);
expect_equal(container.get(h).get<component_1>().value, 10);
expect(container.get(h).get_if<component_2>() == 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<component_1>(h);
container.detach<component_2>(h);
expect(container.get(h).get_if<component_1>() == nullptr);
expect(container.get(h).get_if<component_2>() == 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<bool>(rng);
bool has2 = random::uniform<bool>(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<bool>(rng);
bool has2 = random::uniform<bool>(rng);
if (has1 && has2)
{
++count12;
}
else if (has1 && !has2)
{
container.detach<component_2>(h);
++count1;
}
else if (!has1 && has2)
{
container.detach<component_1>(h);
++count2;
}
else
{
container.detach<component_1, component_2>(h);
++count0;
}
}
check_impl(container, count0, count1, count2, count12);
}