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
{
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 <typename Component, bool Empty = detail::is_empty_v<Component>>
@ -81,7 +88,7 @@ namespace psemek::ecs::detail
template <typename Component, bool Empty>
column_impl<Component, Empty>::column_impl()
: column(Component::uuid())
: column(detail::stride<Component>(), Component::uuid())
{}
template <typename Component, bool Empty>
@ -177,7 +184,7 @@ namespace psemek::ecs::detail
template <typename Component>
column_impl<Component, true>::column_impl()
: column(Component::uuid())
: column(detail::stride<Component>(), Component::uuid())
{
data_ = reinterpret_cast<std::uint8_t *>(new Component[1]);
}

View file

@ -33,6 +33,11 @@ namespace psemek::ecs::detail
return entity_handles_;
}
util::span<std::unique_ptr<detail::column> 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<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_;

View file

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

View file

@ -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 <typename ... 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.
* 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 <typename ... Components>
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<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);
};
@ -204,15 +214,14 @@ namespace psemek::ecs
detail::component_uuid_helper<Components...> uuids;
auto insert_result = table_container_.insert<Components...>(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<std::unique_ptr<detail::column>> columns;
(columns.push_back(std::make_unique<detail::column_impl<Components>>()), ...);
table = insert_table(std::move(columns));
}
if (table->get_iteration_data())
@ -228,6 +237,88 @@ namespace psemek::ecs
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>
query_cache entity_container::cache()
{

View file

@ -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> table::clone() const
{
std::vector<std::unique_ptr<detail::column>> 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<table>(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());

View file

@ -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<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)
{
// 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);
}