Properly call constructors & destructors when attaching/detaching ECS components

This commit is contained in:
Nikita Lisitsa 2024-05-23 13:28:48 +03:00
parent cedc29f39a
commit b9b18b800d
7 changed files with 116 additions and 41 deletions

View file

@ -12,6 +12,7 @@
#include <psemek/ecs/accessor.hpp> #include <psemek/ecs/accessor.hpp>
#include <psemek/ecs/exceptions.hpp> #include <psemek/ecs/exceptions.hpp>
#include <psemek/util/range.hpp> #include <psemek/util/range.hpp>
#include <psemek/util/object_pool.hpp>
namespace psemek::ecs namespace psemek::ecs
{ {
@ -56,7 +57,6 @@ namespace psemek::ecs
* @warning If any two of the passed component types are equal, the call fails with * @warning If any two of the passed component types are equal, the call fails with
* a compilation error * a compilation error
* @warning Creating a new entity invalidates all previously created accessors * @warning Creating a new entity invalidates all previously created accessors
* @warning It is an error to create or destroy entities during `batch_apply()`
*/ */
template <typename ... Components> template <typename ... Components>
handle create(Components && ... components); handle create(Components && ... components);
@ -78,7 +78,6 @@ namespace psemek::ecs
* *
* @param entity A handle to the entity to be destroyed * @param entity A handle to the entity to be destroyed
* @pre The entity was previously obtained by a `create()` call and is `alive()` * @pre The entity was previously obtained by a `create()` call and is `alive()`
* @warning It is an error to create or destroy entities during `batch_apply()`
*/ */
void destroy(handle const & entity); void destroy(handle const & entity);
@ -142,7 +141,6 @@ namespace psemek::ecs
* @warning If any two of the passed component types are equal, the call fails with * @warning If any two of the passed component types are equal, the call fails with
* a compilation error * a compilation error
* @warning Attaching components invalidates all previously created accessors * @warning Attaching components invalidates all previously created accessors
* @warning It is an error to attach or detach components during `batch_apply()`
*/ */
template <typename ... Components> template <typename ... Components>
void attach(handle const & entity, Components && ... components); void attach(handle const & entity, Components && ... components);
@ -160,7 +158,6 @@ namespace psemek::ecs
* @warning If any two of the passed component types are equal, the call fails with * @warning If any two of the passed component types are equal, the call fails with
* a compilation error * a compilation error
* @warning Detaching components invalidates all previously created accessors * @warning Detaching components invalidates all previously created accessors
* @warning It is an error to attach or detach components during `batch_apply()`
*/ */
template <typename ... Components> template <typename ... Components>
void detach(handle const & entity); void detach(handle const & entity);
@ -283,7 +280,8 @@ namespace psemek::ecs
* When attaching components to an entity, the constructor is called * When attaching components to an entity, the constructor is called
* exactly when the entity didn't match the constructor's component types * exactly when the entity didn't match the constructor's component types
* before attaching new components, and does match them after attaching. * before attaching new components, and does match them after attaching.
* TODO: implement this behavior * However, this matching check ignores the constructor's 'without' parameters.
* TODO: Figure out the behavior concerning 'without' parameters
* *
* The constructor function must have the same signature as a function * The constructor function must have the same signature as a function
* passed to the `apply<Components...>()` call. * passed to the `apply<Components...>()` call.
@ -319,7 +317,8 @@ namespace psemek::ecs
* When detaching components from an entity, the destructor is called * When detaching components from an entity, the destructor is called
* exactly when the entity did match the destructor's component types * exactly when the entity did match the destructor's component types
* before detaching components, and doesn't match them after detaching. * before detaching components, and doesn't match them after detaching.
* TODO: implement this behavior * However, this matching check ignores the destructors's 'without' parameters.
* TODO: Figure out the behavior concerning 'without' parameters
* *
* The destructor function must have the same signature as a function * The destructor function must have the same signature as a function
* passed to the `apply<Components...>()` call. * passed to the `apply<Components...>()` call.
@ -386,8 +385,8 @@ namespace psemek::ecs
detail::component_registry component_registry_; detail::component_registry component_registry_;
detail::index_container index_container_; detail::index_container index_container_;
std::vector<util::uuid> uuid_helper_; util::object_pool<std::vector<util::uuid>> uuid_list_pool_;
util::hash_set<util::uuid> uuid_set_helper_; util::object_pool<util::hash_set<util::uuid>> uuid_set_pool_;
std::vector<query_cache> callback_caches_; std::vector<query_cache> callback_caches_;
@ -395,7 +394,7 @@ namespace psemek::ecs
query_cache cache_impl(Constructor && constructor, Destructor && destructor); query_cache cache_impl(Constructor && constructor, Destructor && destructor);
detail::table * insert_table(std::vector<std::unique_ptr<detail::column>> columns); detail::table * insert_table(std::vector<std::unique_ptr<detail::column>> columns);
void do_destroy(handle const & entity); void do_destroy(handle const & entity, bool trigger_destructors);
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);
void finilize_iteration(detail::table & table); void finilize_iteration(detail::table & table);
}; };
@ -448,17 +447,20 @@ namespace psemek::ecs
(register_component<Components>(), ...); (register_component<Components>(), ...);
auto uuids = uuid_list_pool_.get();
auto attached_uuid_set = uuid_set_pool_.get();
auto & data = entity_list_.get_entities()[entity.id]; auto & data = entity_list_.get_entities()[entity.id];
for (auto const & column : data.table->columns()) for (auto const & column : data.table->columns())
uuid_helper_.push_back(column->uuid()); uuids.push_back(column->uuid());
bool archetype_changed = false; bool archetype_changed = false;
((data.table->column(std::remove_cvref_t<Components>::uuid()) ? 0 : (archetype_changed = true, uuid_helper_.push_back(std::remove_cvref_t<Components>::uuid()), 0)), ...); ((data.table->column(std::remove_cvref_t<Components>::uuid()) ? 0 : (archetype_changed = true, attached_uuid_set.insert(std::remove_cvref_t<Components>::uuid()), 0)), ...);
if (archetype_changed) for (auto const & uuid : attached_uuid_set)
data.table->trigger_destructors(*this, data.row); uuids.push_back(uuid);
auto table = table_container_.get(uuid_helper_); auto table = table_container_.get(uuids);
if (!table) if (!table)
{ {
@ -477,7 +479,7 @@ namespace psemek::ecs
table = table->get_delayed_table(); table = table->get_delayed_table();
auto new_row = table->move_row(entity, data.table, data.row); auto new_row = table->move_row(entity, data.table, data.row);
do_destroy(entity); do_destroy(entity, false);
data.table = table; data.table = table;
data.row = new_row; data.row = new_row;
@ -486,10 +488,14 @@ namespace psemek::ecs
auto accessor = get(entity); auto accessor = get(entity);
((accessor.get<std::remove_cvref_t<Components>>() = std::forward<Components>(components)), ...); ((accessor.get<std::remove_cvref_t<Components>>() = std::forward<Components>(components)), ...);
uuid_helper_.clear();
if (archetype_changed) if (archetype_changed)
table->trigger_constructors(*this, data.row); table->trigger_constructors(*this, data.row, attached_uuid_set);
attached_uuid_set.clear();
uuids.clear();
uuid_set_pool_.put(std::move(attached_uuid_set));
uuid_list_pool_.put(std::move(uuids));
} }
template <typename ... Components> template <typename ... Components>
@ -497,25 +503,32 @@ namespace psemek::ecs
{ {
static_assert(detail::all_different_types_v<std::remove_const_t<Components>...>, "all component types must be different"); static_assert(detail::all_different_types_v<std::remove_const_t<Components>...>, "all component types must be different");
(uuid_set_helper_.insert(std::remove_const_t<Components>::uuid()), ...); auto detached_uuid_set = uuid_set_pool_.get();
(detached_uuid_set.insert(std::remove_const_t<Components>::uuid()), ...);
auto uuids = uuid_list_pool_.get();
auto & data = entity_list_.get_entities()[entity.id]; auto & data = entity_list_.get_entities()[entity.id];
for (auto const & column : data.table->columns()) for (auto const & column : data.table->columns())
if (!uuid_set_helper_.contains(column->uuid())) {
uuid_helper_.push_back(column->uuid()); if (detached_uuid_set.contains(column->uuid()))
detached_uuid_set.insert(column->uuid());
else
uuids.push_back(column->uuid());
}
bool const archetype_changed = sizeof...(Components) > 0; bool const archetype_changed = sizeof...(Components) > 0;
auto table = table_container_.get(uuid_helper_); auto table = table_container_.get(uuids);
if (archetype_changed) if (archetype_changed)
data.table->trigger_destructors(*this, data.row); data.table->trigger_destructors(*this, data.row, detached_uuid_set);
if (!table) if (!table)
{ {
std::vector<std::unique_ptr<detail::column>> columns; std::vector<std::unique_ptr<detail::column>> columns;
for (auto const & column : data.table->columns()) for (auto const & column : data.table->columns())
if (!uuid_set_helper_.contains(column->uuid())) if (!detached_uuid_set.contains(column->uuid()))
columns.push_back(column->clone()); columns.push_back(column->clone());
table = insert_table(std::move(columns)); table = insert_table(std::move(columns));
@ -527,17 +540,18 @@ namespace psemek::ecs
table = table->get_delayed_table(); table = table->get_delayed_table();
auto new_row = table->move_row(entity, data.table, data.row); auto new_row = table->move_row(entity, data.table, data.row);
do_destroy(entity); do_destroy(entity, false);
data.table = table; data.table = table;
data.row = new_row; data.row = new_row;
} }
uuid_set_helper_.clear(); detached_uuid_set.clear();
uuid_helper_.clear(); uuids.clear();
detached_uuid_set.clear();
if (archetype_changed) uuid_list_pool_.put(std::move(uuids));
table->trigger_constructors(*this, data.row); uuid_set_pool_.put(std::move(detached_uuid_set));
} }
template <typename ... Components> template <typename ... Components>
@ -623,7 +637,12 @@ namespace psemek::ecs
static_assert(invocable_type::value, "function is not invocable with these components"); static_assert(invocable_type::value, "function is not invocable with these components");
auto constructor = [function = std::move(function)](std::vector<std::uint32_t> const & column_indices) -> detail::table_callback { auto constructor = [function = std::move(function)](std::vector<std::uint32_t> const & column_indices) -> detail::table_callback {
return [function, column_indices](container & container, detail::table & table, std::uint32_t row){ return [function, column_indices](container & container, detail::table & table, std::uint32_t row, util::hash_set<util::uuid> const & attached_components, bool force){
bool const invoke = force || (detail::contains_helper<Components>::contains(attached_components) || ...);
if (!invoke)
return;
typename detail::filter_with<detail::static_apply_helper, std::tuple<Components ...>>::type apply_helper(container, table.entity_handles()); typename detail::filter_with<detail::static_apply_helper, std::tuple<Components ...>>::type apply_helper(container, table.entity_handles());
for (std::size_t i = 0; i < apply_helper.column_count; ++i) for (std::size_t i = 0; i < apply_helper.column_count; ++i)
@ -649,7 +668,12 @@ namespace psemek::ecs
static_assert(invocable_type::value, "function is not invocable with these components"); static_assert(invocable_type::value, "function is not invocable with these components");
auto destructor = [function = std::move(function)](std::vector<std::uint32_t> const & column_indices) -> detail::table_callback { auto destructor = [function = std::move(function)](std::vector<std::uint32_t> const & column_indices) -> detail::table_callback {
return [function, column_indices](container & container, detail::table & table, std::uint32_t row){ return [function, column_indices](container & container, detail::table & table, std::uint32_t row, util::hash_set<util::uuid> const & detached_components, bool force){
bool const invoke = force || (detail::contains_helper<Components>::contains(detached_components) || ...);
if (!invoke)
return;
typename detail::filter_with<detail::static_apply_helper, std::tuple<Components ...>>::type apply_helper(container, table.entity_handles()); typename detail::filter_with<detail::static_apply_helper, std::tuple<Components ...>>::type apply_helper(container, table.entity_handles());
for (std::size_t i = 0; i < apply_helper.column_count; ++i) for (std::size_t i = 0; i < apply_helper.column_count; ++i)

View file

@ -2,8 +2,9 @@
#include <psemek/ecs/handle.hpp> #include <psemek/ecs/handle.hpp>
#include <psemek/util/function.hpp> #include <psemek/util/function.hpp>
#include <psemek/util/uuid.hpp>
#include <memory> #include <psemek/util/span.hpp>
#include <psemek/util/hash_table.hpp>
namespace psemek::ecs namespace psemek::ecs
{ {
@ -15,7 +16,7 @@ namespace psemek::ecs
struct table; struct table;
using table_callback = util::function<void(container &, table &, std::uint32_t)>; using table_callback = util::function<void(container &, table &, std::uint32_t, util::hash_set<util::uuid> const &, bool)>;
} }

View file

@ -10,9 +10,7 @@
#include <psemek/util/assert.hpp> #include <psemek/util/assert.hpp>
#include <memory> #include <memory>
#include <type_traits>
#include <vector> #include <vector>
#include <array>
#include <optional> #include <optional>
namespace psemek::ecs::detail namespace psemek::ecs::detail
@ -76,11 +74,15 @@ namespace psemek::ecs::detail
void add_destructor(table_callback callback); void add_destructor(table_callback callback);
void trigger_constructors(container & container, std::uint32_t row); void trigger_constructors(container & container, std::uint32_t row);
void trigger_constructors(container & container, std::uint32_t row, util::hash_set<util::uuid> const & attached_components);
void trigger_destructors(container & container, std::uint32_t row); void trigger_destructors(container & container, std::uint32_t row);
void trigger_destructors(container & container, std::uint32_t row, util::hash_set<util::uuid> const & detached_components);
protected: protected:
std::size_t hash_; std::size_t hash_;
std::vector<std::unique_ptr<detail::column>> columns_; std::vector<std::unique_ptr<detail::column>> columns_;
util::hash_set<util::uuid> column_uuid_set_;
util::hash_map<util::uuid, std::uint32_t> component_uuid_to_column_index_; util::hash_map<util::uuid, std::uint32_t> component_uuid_to_column_index_;
std::vector<handle> entity_handles_; std::vector<handle> entity_handles_;

View file

@ -79,4 +79,24 @@ namespace psemek::ecs::detail
using type = typename filter_without_impl<MetaFunction, std::tuple<ExtraArgs...>, std::tuple<>, Components...>::type; using type = typename filter_without_impl<MetaFunction, std::tuple<ExtraArgs...>, std::tuple<>, Components...>::type;
}; };
template <typename Component>
struct contains_helper
{
template <typename Container>
static bool contains(Container const & container)
{
return container.contains(Component::uuid());
}
};
template <typename Component>
struct contains_helper<without<Component>>
{
template <typename Container>
static bool contains(Container const &)
{
return false;
}
};
} }

View file

@ -23,3 +23,17 @@ namespace psemek::ecs
} }
} }
namespace std
{
template <>
struct hash<::psemek::ecs::handle>
{
size_t operator()(::psemek::ecs::handle const & h) const noexcept
{
return (static_cast<size_t>(h.id) << 32) | h.epoch;
}
};
}

View file

@ -12,7 +12,7 @@ namespace psemek::ecs
void container::destroy(handle const & entity) void container::destroy(handle const & entity)
{ {
assert(alive(entity)); assert(alive(entity));
do_destroy(entity); do_destroy(entity, true);
entity_list_.destroy(entity.id); entity_list_.destroy(entity.id);
} }
@ -69,13 +69,14 @@ namespace psemek::ecs
return table; return table;
} }
void container::do_destroy(handle const & entity) void container::do_destroy(handle const & entity, bool trigger_destructors)
{ {
auto entities = entity_list_.get_entities(); auto entities = entity_list_.get_entities();
auto & data = entities[entity.id]; auto & data = entities[entity.id];
auto & iteration_data = data.table->get_iteration_data(); auto & iteration_data = data.table->get_iteration_data();
data.table->trigger_destructors(*this, data.row); if (trigger_destructors)
data.table->trigger_destructors(*this, data.row);
if (!iteration_data || iteration_data->current_row < data.row) if (!iteration_data || iteration_data->current_row < data.row)
remove_row(*data.table, data.row, entities); remove_row(*data.table, data.row, entities);

View file

@ -12,6 +12,7 @@ namespace psemek::ecs::detail
auto const & column = columns[i]; auto const & column = columns[i];
auto uuid = column->uuid(); auto uuid = column->uuid();
hasher(uuid); hasher(uuid);
column_uuid_set_.insert(uuid);
component_uuid_to_column_index_.insert({uuid, i}); component_uuid_to_column_index_.insert({uuid, i});
if (!column->copy_constructible()) if (!column->copy_constructible())
non_copyable_components_.push_back(column->type()); non_copyable_components_.push_back(column->type());
@ -156,13 +157,25 @@ namespace psemek::ecs::detail
void table::trigger_constructors(container & container, std::uint32_t row) void table::trigger_constructors(container & container, std::uint32_t row)
{ {
for (auto const & callback : *constructors_) for (auto const & callback : *constructors_)
callback(container, *this, row); callback(container, *this, row, column_uuid_set_, true);
}
void table::trigger_constructors(container & container, std::uint32_t row, util::hash_set<util::uuid> const & attached_components)
{
for (auto const & callback : *constructors_)
callback(container, *this, row, attached_components, false);
} }
void table::trigger_destructors(container & container, std::uint32_t row) void table::trigger_destructors(container & container, std::uint32_t row)
{ {
for (auto const & callback : *destructors_) for (auto const & callback : *destructors_)
callback(container, *this, row); callback(container, *this, row, column_uuid_set_, true);
}
void table::trigger_destructors(container & container, std::uint32_t row, util::hash_set<util::uuid> const & detached_components)
{
for (auto const & callback : *destructors_)
callback(container, *this, row, detached_components, false);
} }
} }