ECS constructors wip

This commit is contained in:
Nikita Lisitsa 2023-12-18 12:38:58 +03:00
parent 0b1522722c
commit 028b4e1296
10 changed files with 192 additions and 30 deletions

View file

@ -19,7 +19,6 @@ namespace psemek::ecs
{
using query_cache = std::shared_ptr<detail::query_cache>;
using token = std::shared_ptr<void>;
// TODO:
// - Fully document which functions can be called from which callbacks
@ -283,12 +282,11 @@ namespace psemek::ecs
* When attaching components to an entity, the constructor is called
* exactly when the entity didn't match the constructor's component types
* before attaching new components, and does match them after attaching.
* TODO: implement this behavior
*
* The constructor function must have the same signature as a function
* passed to the `apply<Components...>()` call.
*
* The constructor can immediately destroy the created entity.
*
* The constuctor is not considered to be a modification of the entity, i.e. it
* doesn't trigger modification callbacks.
*
@ -298,13 +296,11 @@ namespace psemek::ecs
* @warning If any two of the passed component types are equal, the call fails with
* a compilation error
* @warning If the constructor modifies the entity's archetype (i.e. attaches or
* detaches components), it might be called recursively, leading to
* infinite recursion. It is best not to change the archetype from the
* constructor.
* detaches components), the behavior is undefined
* @warning If the constructor destroys the entity, the behavior is undefined
*/
// TODO: implement
template <typename ... Components, typename Function>
token constructor(Function && function);
void constructor(Function && function);
/** Register a destructor. Each time an entity is destroyed that has
* the specified set of components, the destructor is called for
@ -322,6 +318,7 @@ namespace psemek::ecs
* When detaching components from an entity, the destructor is called
* exactly when the entity did match the destructor's component types
* before detaching components, and doesn't match them after detaching.
* TODO: implement this behavior
*
* The destructor function must have the same signature as a function
* passed to the `apply<Components...>()` call.
@ -337,10 +334,11 @@ namespace psemek::ecs
* a compilation error
* @warning If the destructor modifies the entity's archetype (i.e. attaches or
* detaches components), the behavior is undefined
* @warning If the destructor destroys the entity recursively, the behavior is undefined
*/
// TODO: implement
template <typename ... Components, typename Function>
token destructor(Function && function);
void destructor(Function && function);
/** Register a component modification callback. Each time an entity that has
* the specified set of components is modified, and the modification affects
@ -376,7 +374,7 @@ namespace psemek::ecs
*/
// TODO: implement
template <typename ... Components, typename Function>
token watch(Function && function);
void watch(Function && function);
private:
detail::entity_list entity_list_;
@ -387,6 +385,8 @@ namespace psemek::ecs
std::vector<util::uuid> uuid_helper_;
util::hash_set<util::uuid> uuid_set_helper_;
std::vector<query_cache> callback_caches_;
detail::table * insert_table(std::vector<std::unique_ptr<detail::column>> columns);
void do_destroy(handle const & entity);
void remove_row(detail::table & table, std::uint32_t row, util::span<detail::entity_data> entities);
@ -420,13 +420,16 @@ namespace psemek::ecs
if (table->get_iteration_data())
table = table->get_delayed_table();
auto id = entity_list_.create(table, table->row_count());
auto row = table->row_count();
auto id = entity_list_.create(table, row);
handle handle{id, entity_list_.get_entities()[id].epoch};
[[maybe_unused]] accessor accessor = get(handle);
table->push_row(handle);
((accessor.get<std::remove_cvref_t<Components>>() = std::forward<Components>(components)), ...);
table->trigger_constructors(*this, row);
return handle;
}
@ -477,9 +480,9 @@ namespace psemek::ecs
template <typename ... Components>
void container::detach(handle const & entity)
{
static_assert(detail::all_different_types_v<std::remove_cvref_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_cvref_t<Components>::uuid()), ...);
(uuid_set_helper_.insert(std::remove_const_t<Components>::uuid()), ...);
auto & data = entity_list_.get_entities()[entity.id];
for (auto const & column : data.table->columns())
@ -534,7 +537,7 @@ namespace psemek::ecs
{
static_assert(detail::all_different_types_v<std::remove_const_t<Components>...>, "all component types must be different");
using invocable_type = typename detail::filter_with<detail::invocable, std::tuple<std::remove_cvref_t<Components>...>, Function>::type;
using invocable_type = typename detail::filter_with<detail::invocable, std::tuple<Components...>, Function>::type;
static_assert(invocable_type::value, "function is not invocable with these components");
@ -582,7 +585,7 @@ namespace psemek::ecs
{
static_assert(detail::all_different_types_v<std::remove_const_t<Components>...>, "all component types must be different");
using invocable_type = typename detail::filter_with<detail::batch_invocable, std::tuple<std::remove_cvref_t<Components>...>, Function>::type;
using invocable_type = typename detail::filter_with<detail::batch_invocable, std::tuple<Components...>, Function>::type;
static_assert(invocable_type::value, "function is not batch-invocable with these components");
@ -602,4 +605,30 @@ namespace psemek::ecs
return cache;
}
template <typename ... Components, typename Function>
void container::constructor(Function && function)
{
static_assert(detail::all_different_types_v<std::remove_const_t<Components>...>, "all component types must be different");
using invocable_type = typename detail::filter_with<detail::invocable, std::tuple<Components ...>, Function>::type;
static_assert(invocable_type::value, "function is not invocable with these components");
query_cache cache = this->cache<Components...>();
cache->constructor_factory = [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){
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)
apply_helper.pointers[i] = table.columns()[column_indices[i]]->data();
apply_helper.advance(row);
apply_helper.apply(function);
};
};
callback_caches_.push_back(cache);
}
}

View file

@ -84,6 +84,8 @@ namespace psemek::ecs::detail
template <typename ... Components>
struct static_apply_helper
{
static constexpr std::size_t column_count = sizeof...(Components);
container & parent;
std::size_t row_count;
handle const * entity_handles_pointer;
@ -132,6 +134,14 @@ namespace psemek::ecs::detail
std::size_t i = 0;
((pointers[i++] += stride<Components>()), ...);
}
void advance(std::size_t rows)
{
entity_handles_pointer += rows;
std::size_t i = 0;
((pointers[i++] += stride<Components>() * rows), ...);
}
};
}

View file

@ -0,0 +1,22 @@
#pragma once
#include <psemek/ecs/handle.hpp>
#include <psemek/util/function.hpp>
#include <memory>
namespace psemek::ecs
{
struct container;
namespace detail
{
struct table;
using table_callback = util::function<void(container &, table &, std::uint32_t)>;
}
}

View file

@ -1,5 +1,6 @@
#pragma once
#include <psemek/ecs/detail/callback.hpp>
#include <psemek/util/uuid.hpp>
#include <vector>
@ -13,7 +14,7 @@ namespace psemek::ecs::detail
struct query_cache_entry
{
struct table * table = nullptr;
std::vector<column *> columns;
std::vector<std::uint32_t> columns_indices;
};
struct query_cache
@ -22,6 +23,8 @@ namespace psemek::ecs::detail
std::vector<util::uuid> without_uuids;
std::vector<query_cache_entry> entries;
util::function<table_callback(std::vector<std::uint32_t> const &)> constructor_factory;
void add(table * table);
};

View file

@ -3,6 +3,7 @@
#include <psemek/ecs/handle.hpp>
#include <psemek/ecs/detail/component_hash.hpp>
#include <psemek/ecs/detail/column.hpp>
#include <psemek/ecs/detail/callback.hpp>
#include <psemek/util/uuid.hpp>
#include <psemek/util/span.hpp>
#include <psemek/util/hash_table.hpp>
@ -26,7 +27,8 @@ namespace psemek::ecs::detail
return hash_;
}
detail::column * column(util::uuid const & uuid) const;
std::optional<std::size_t> column_index(util::uuid const & uuid) const;
struct column * column(util::uuid const & uuid) const;
util::span<handle const> entity_handles() const
{
@ -38,11 +40,6 @@ namespace psemek::ecs::detail
return columns_;
}
std::size_t column_count() const
{
return component_uuid_to_column_.size();
}
std::size_t row_count() const
{
return entity_handles_.size();
@ -75,10 +72,14 @@ namespace psemek::ecs::detail
std::vector<std::type_index> const & non_copyable_components() const;
void add_constructor(table_callback callback);
void trigger_constructors(container & container, std::uint32_t row);
protected:
std::size_t hash_;
std::vector<std::unique_ptr<detail::column>> columns_;
util::hash_map<util::uuid, detail::column *> component_uuid_to_column_;
util::hash_map<util::uuid, std::uint32_t> component_uuid_to_column_index_;
std::vector<handle> entity_handles_;
@ -87,6 +88,8 @@ namespace psemek::ecs::detail
std::unique_ptr<table> delayed_table_;
std::vector<std::type_index> non_copyable_components_;
std::shared_ptr<std::vector<table_callback>> constructors_;
};
}

View file

@ -26,7 +26,7 @@ namespace psemek::ecs::detail
{
bool operator()(util::span<util::uuid const> const & uuids, std::unique_ptr<table> const & table) const
{
if (uuids.size() != table->column_count())
if (uuids.size() != table->columns().size())
return false;
for (auto const & uuid : uuids)
if (!table->column(uuid))

View file

@ -28,6 +28,12 @@ namespace psemek::ecs::detail
using type = typename filter_with_impl<MetaFunction, std::tuple<ExtraArgs...>, std::tuple<FilteredComponents...>, RemainingComponents...>::type;
};
template <template <typename ...> typename MetaFunction, typename ... ExtraArgs, typename ... FilteredComponents, typename Component, typename ... RemainingComponents>
struct filter_with_impl<MetaFunction, std::tuple<ExtraArgs...>, std::tuple<FilteredComponents...>, ecs::without<Component> const, RemainingComponents...>
{
using type = typename filter_with_impl<MetaFunction, std::tuple<ExtraArgs...>, std::tuple<FilteredComponents...>, RemainingComponents...>::type;
};
template <template <typename ...> typename MetaFunction, typename ExtraArgsTuple, typename FilteredComponentsTuple, typename ... Components>
struct filter_without_impl;
@ -49,6 +55,12 @@ namespace psemek::ecs::detail
using type = typename filter_without_impl<MetaFunction, std::tuple<ExtraArgs...>, std::tuple<FilteredComponents..., Component>, RemainingComponents...>::type;
};
template <template <typename ...> typename MetaFunction, typename ... ExtraArgs, typename ... FilteredComponents, typename Component, typename ... RemainingComponents>
struct filter_without_impl<MetaFunction, std::tuple<ExtraArgs...>, std::tuple<FilteredComponents...>, const ecs::without<Component>, RemainingComponents...>
{
using type = typename filter_without_impl<MetaFunction, std::tuple<ExtraArgs...>, std::tuple<FilteredComponents..., Component>, RemainingComponents...>::type;
};
template <template <typename ...> typename MetaFunction, typename ComponentsTuple, typename ... ExtraArgs>
struct filter_with;

View file

@ -10,7 +10,10 @@ namespace psemek::ecs::detail
entry.table = table;
for (auto const & uuid : with_uuids)
entry.columns.push_back(table->column(uuid));
entry.columns_indices.push_back(*table->column_index(uuid));
if (constructor_factory)
table->add_constructor(constructor_factory(entry.columns_indices));
}
}

View file

@ -7,23 +7,33 @@ namespace psemek::ecs::detail
table::table(std::vector<std::unique_ptr<detail::column>> columns)
{
component_hasher<true> hasher;
for (auto & column : columns)
for (std::size_t i = 0; i < columns.size(); ++i)
{
auto const & column = columns[i];
auto uuid = column->uuid();
hasher(uuid);
component_uuid_to_column_.insert({uuid, column.get()});
component_uuid_to_column_index_.insert({uuid, i});
if (!column->copy_constructible())
non_copyable_components_.push_back(column->type());
}
hash_ = hasher.result;
columns_ = std::move(columns);
constructors_ = std::make_shared<std::vector<table_callback>>();
}
detail::column * table::column(util::uuid const & uuid) const
std::optional<std::size_t> table::column_index(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_index_.find(uuid); it != component_uuid_to_column_index_.end())
return it->second;
return std::nullopt;
}
column * table::column(util::uuid const & uuid) const
{
if (auto index = column_index(uuid))
return columns_[*index].get();
return nullptr;
}
@ -95,7 +105,10 @@ namespace psemek::ecs::detail
std::vector<std::unique_ptr<detail::column>> columns;
for (auto const & column : columns_)
columns.push_back(column->clone());
return std::make_unique<table>(std::move(columns));
auto result = std::make_unique<table>(std::move(columns));
result->constructors_ = constructors_;
return result;
}
table * table::get_delayed_table()
@ -128,4 +141,17 @@ namespace psemek::ecs::detail
return non_copyable_components_;
}
void table::add_constructor(table_callback callback)
{
constructors_->push_back(std::move(callback));
}
void table::trigger_constructors(container & container, std::uint32_t row)
{
for (auto const & callback : *constructors_)
{
callback(container, *this, row);
}
}
}

View file

@ -0,0 +1,54 @@
#include <psemek/test/test.hpp>
#include <psemek/ecs/container.hpp>
#include <psemek/ecs/declare_uuid.hpp>
using namespace psemek;
using namespace psemek::ecs;
namespace
{
struct component_1
{
int value;
psemek_ecs_declare_uuid("component_1")
};
struct component_2
{
int value;
psemek_ecs_declare_uuid("component_2")
};
}
test_case(ecs_callback_constructor_create)
{
container container;
int value = 0;
container.constructor<component_1>([&value](component_1 const & c1){
value = c1.value;
});
container.create(component_1{10});
expect_equal(value, 10);
container.create(component_1{20});
expect_equal(value, 20);
container.create(component_2{100});
expect_equal(value, 20);
container.create();
expect_equal(value, 20);
container.create(component_1{30});
expect_equal(value, 30);
container.create(component_1{40}, component_2{200});
expect_equal(value, 40);
}