Support ecs::without

This commit is contained in:
Nikita Lisitsa 2023-12-16 23:01:06 +03:00
parent 08b14ded93
commit 340a5f4254
12 changed files with 260 additions and 57 deletions

View file

@ -6,7 +6,9 @@
#include <psemek/ecs/detail/apply_helper.hpp>
#include <psemek/ecs/detail/all_different_types.hpp>
#include <psemek/ecs/detail/component_uuid_helper.hpp>
#include <psemek/ecs/detail/without.hpp>
#include <psemek/ecs/accessor.hpp>
#include <psemek/ecs/without.hpp>
#include <psemek/util/span.hpp>
#include <psemek/util/range.hpp>
#include <psemek/util/exception.hpp>
@ -23,8 +25,6 @@ namespace psemek::ecs
// TODO:
// - Fully document which functions can be called from which callbacks
// - Const-qualified component access
// - Negated component access
// - Constructors & destructors implementation
// - Modification callbacks implementation
// - Refactor query caches
@ -125,6 +125,8 @@ namespace psemek::ecs
*
* The constness of the component types is ignored.
*
* The component types can be equal to ecs::without<Component>.
*
* @tparam Components The component types matching the corresponding `apply()` call
* @return A query cache
*/
@ -144,6 +146,10 @@ namespace psemek::ecs
* that this function doesn't modify specific components, to prevent modification callbacks
* from being triggered.
*
* The component types can be equal to ecs::without<Component>, indicating that entities having
* this component type will not be visited by this function. These component types are not
* included in the called function signature.
*
* 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.
@ -185,6 +191,10 @@ namespace psemek::ecs
* that this function doesn't modify specific components, to prevent modification callbacks
* from being triggered.
*
* The component types can be equal to ecs::without<Component>, indicating that entities having
* this component type will not be visited by this function. These component types are not
* included in the called function signature.
*
* The sizes of all spans within a single function call are the same,
* except for empty component types (i.e. std::is_empty_v<Component> is true),
* each having an unspecified non-zero size. If all components are empty types,
@ -218,6 +228,10 @@ namespace psemek::ecs
* The component types can be const-qualified, in which case the corresponding function must
* also accept the corresponding components by a const reference.
*
* The component types can be equal to ecs::without<Component>, indicating that entities having
* this component type will not be visited by this constructor. These component types are not
* included in the called function signature.
*
* 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.
@ -253,6 +267,10 @@ namespace psemek::ecs
* The component types can be const-qualified, in which case the corresponding function must
* also accept the corresponding components by a const reference.
*
* The component types can be equal to ecs::without<Component>, indicating that entities having
* this component type will not be visited by this destructor. These component types are not
* included in the called function signature.
*
* 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.
@ -283,6 +301,10 @@ namespace psemek::ecs
* The component types can be const-qualified, in which case the corresponding function must
* also accept the corresponding components by a const reference.
*
* The component types can be equal to ecs::without<Component>, indicating that entities having
* this component type will not be watched by this callback. These component types are not
* included in the called function signature.
*
* If the modification occurred via an accessor, the callback is called
* after the accessor is destroyed, allowing for transaction-like modification.
*
@ -436,13 +458,14 @@ namespace psemek::ecs
template <typename ... Components>
query_cache container::cache()
{
detail::component_uuid_helper<std::remove_cvref_t<Components>...> uuids;
typename detail::filter_with <detail::component_uuid_helper, std::tuple<std::remove_cvref_t<Components>...>>::type with_uuids;
typename detail::filter_without<detail::component_uuid_helper, std::tuple<std::remove_cvref_t<Components>...>>::type without_uuids;
auto result = query_cache_container_.create(uuids.get());
auto result = query_cache_container_.create(with_uuids.get(), without_uuids.get());
table_container_.apply([&](detail::table & table){
result->add(&table);
}, uuids.get());
}, with_uuids.get(), without_uuids.get());
return result;
}
@ -451,7 +474,10 @@ namespace psemek::ecs
query_cache container::apply(Function && function, query_cache cache)
{
static_assert(detail::all_different_types_v<std::remove_const_t<Components>...>, "all component types must be different");
static_assert(detail::invocable<Function, Components...>, "function is not invocable with these components");
using invocable_type = typename detail::filter_with<detail::invocable, std::tuple<std::remove_cvref_t<Components>...>, Function>::type;
static_assert(invocable_type::value, "function is not invocable with these components");
if (!cache)
cache = this->cache<Components...>();
@ -460,10 +486,10 @@ namespace psemek::ecs
{
auto & iteration_data = entry.table->get_iteration_data();
iteration_data.emplace();
detail::static_apply_helper<Components...> apply_helper(*this, entry.table->entity_handles());
typename detail::filter_with<detail::static_apply_helper, std::tuple<Components...>>::type apply_helper(*this, entry.table->entity_handles());
for (std::size_t i = 0; i < sizeof...(Components); ++i)
apply_helper.pointers[i] = entry.table->column(cache->component_uuids[i])->data();
for (std::size_t i = 0; i < cache->with_uuids.size(); ++i)
apply_helper.pointers[i] = entry.table->column(cache->with_uuids[i])->data();
for (std::size_t i = 0; i < entry.table->row_count(); ++i)
{
@ -496,17 +522,20 @@ namespace psemek::ecs
query_cache container::batch_apply(Function && function, query_cache cache)
{
static_assert(detail::all_different_types_v<std::remove_const_t<Components>...>, "all component types must be different");
static_assert(detail::batch_invocable<Function, Components...>, "function is not batch-invocable with these components");
using invocable_type = typename detail::filter_with<detail::batch_invocable, std::tuple<std::remove_cvref_t<Components>...>, Function>::type;
static_assert(invocable_type::value, "function is not batch-invocable with these components");
if (!cache)
cache = this->cache<Components...>();
for (auto const & entry : cache->entries)
{
detail::static_apply_helper<Components...> apply_helper(*this, entry.table->entity_handles());
typename detail::filter_with<detail::static_apply_helper, std::tuple<Components...>>::type apply_helper(*this, entry.table->entity_handles());
for (std::size_t i = 0; i < sizeof...(Components); ++i)
apply_helper.pointers[i] = entry.table->column(cache->component_uuids[i])->data();
for (std::size_t i = 0; i < cache->with_uuids.size(); ++i)
apply_helper.pointers[i] = entry.table->column(cache->with_uuids[i])->data();
apply_helper.batch_apply(function);
}

View file

@ -16,12 +16,15 @@ namespace psemek::ecs::detail
{
template <typename Function, typename ... Components>
constexpr bool invocable = false
|| std::invocable<Function, container &, handle, Components & ...>
|| std::invocable<Function, container &, Components & ...>
|| std::invocable<Function, handle, Components & ...>
|| std::invocable<Function, Components & ...>
;
struct invocable
{
static constexpr bool value = false
|| std::invocable<Function, container &, handle, Components & ...>
|| std::invocable<Function, container &, Components & ...>
|| std::invocable<Function, handle, Components & ...>
|| std::invocable<Function, Components & ...>
;
};
template <typename Function, typename ... Components>
void invoke(Function && function, container & parent, handle const & handle, Components & ... components)
@ -45,12 +48,15 @@ namespace psemek::ecs::detail
}
template <typename Function, typename ... Components>
constexpr bool batch_invocable = false
|| std::invocable<Function, container &, util::span<handle const>, util::span<Components> ...>
|| std::invocable<Function, container &, util::span<Components> ...>
|| std::invocable<Function, util::span<handle const>, util::span<Components> ...>
|| std::invocable<Function, util::span<Components> ...>
;
struct batch_invocable
{
static constexpr bool value = false
|| std::invocable<Function, container &, util::span<handle const>, util::span<Components> ...>
|| std::invocable<Function, container &, util::span<Components> ...>
|| std::invocable<Function, util::span<handle const>, util::span<Components> ...>
|| std::invocable<Function, util::span<Components> ...>
;
};
template <typename Function, typename ... Components>
void batch_invoke(Function && function, container & parent, std::size_t count, handle const * handles, Components * ... components)

View file

@ -6,23 +6,33 @@
namespace psemek::ecs::detail
{
template <bool With>
struct component_hasher
{
std::size_t result = 0xcd5694d2b3f3443eull;
void operator()(util::uuid const & uuid)
{
result ^= std::hash<util::uuid>{}(uuid);
if constexpr (With)
result ^= std::hash<util::uuid>{}(uuid);
else
result ^= ~std::hash<util::uuid>{}(uuid);
}
};
template <typename UUIDs>
template <bool With, typename UUIDs>
std::size_t component_hash(UUIDs const & uuids)
{
component_hasher hasher;
component_hasher<With> hasher;
for (auto const & uuid : uuids)
hasher(uuid);
return hasher.result;
}
template <typename WithUUIDs, typename WithoutUUIDs>
std::size_t component_hash(WithUUIDs const & with_uuids, WithoutUUIDs const & without_uuids)
{
return component_hash<true>(with_uuids) ^ component_hash<false>(without_uuids);
}
}

View file

@ -18,7 +18,8 @@ namespace psemek::ecs::detail
struct query_cache
{
std::vector<util::uuid> component_uuids;
std::vector<util::uuid> with_uuids;
std::vector<util::uuid> without_uuids;
std::vector<query_cache_entry> entries;
void add(table * table);

View file

@ -12,15 +12,16 @@ namespace psemek::ecs::detail
struct query_cache_set
{
std::size_t hash;
util::hash_set<util::uuid> uuids;
util::hash_set<util::uuid> with_uuids;
util::hash_set<util::uuid> without_uuids;
std::vector<std::weak_ptr<query_cache>> caches;
};
struct query_cache_hash
{
std::size_t operator()(util::span<util::uuid const> const & uuids) const
std::size_t operator()(std::pair<util::span<util::uuid const>, util::span<util::uuid const>> const & uuids) const
{
return component_hash(uuids);
return component_hash(uuids.first, uuids.second);
}
std::size_t operator()(std::unique_ptr<query_cache_set> const & set) const
@ -31,13 +32,20 @@ namespace psemek::ecs::detail
struct query_cache_equal
{
bool operator()(util::span<util::uuid const> const & uuids, std::unique_ptr<query_cache_set> const & set) const
bool operator()(std::pair<util::span<util::uuid const>, util::span<util::uuid const>> const & uuids, std::unique_ptr<query_cache_set> const & set) const
{
if (uuids.size() != set->uuids.size())
if (uuids.first.size() != set->with_uuids.size())
return false;
for (auto const & uuid : uuids)
if (!set->uuids.contains(uuid))
if (uuids.second.size() != set->without_uuids.size())
return false;
for (auto const & uuid : uuids.first)
if (!set->with_uuids.contains(uuid))
return false;
for (auto const & uuid : uuids.second)
if (!set->without_uuids.contains(uuid))
return false;
return true;
@ -47,23 +55,25 @@ namespace psemek::ecs::detail
{
return set1.get() == set2.get();
}
};
// TODO: store query caches in a bitmask trie balanced by subtree size
struct query_cache_container
{
std::shared_ptr<query_cache> create(util::span<util::uuid const> component_uuids)
std::shared_ptr<query_cache> create(util::span<util::uuid const> with_uuids, util::span<util::uuid const> without_uuids)
{
auto result = std::make_shared<query_cache>();
result->component_uuids.assign(component_uuids.begin(), component_uuids.end());
auto it = caches_.find(component_uuids);
result->with_uuids.assign(with_uuids.begin(), with_uuids.end());
result->without_uuids.assign(without_uuids.begin(), without_uuids.end());
auto it = caches_.find(std::pair{with_uuids, without_uuids});
if (it == caches_.end())
{
auto value = std::make_unique<query_cache_set>();
for (auto const & uuid : component_uuids)
value->uuids.insert(uuid);
value->hash = component_hash(component_uuids);
for (auto const & uuid : with_uuids)
value->with_uuids.insert(uuid);
for (auto const & uuid : without_uuids)
value->without_uuids.insert(uuid);
value->hash = component_hash(with_uuids, without_uuids);
it = caches_.insert(std::move(value)).first;
}
it->get()->caches.push_back(result);
@ -76,7 +86,7 @@ namespace psemek::ecs::detail
for (auto & cache_set : caches_)
{
bool good = true;
for (auto const & uuid : cache_set->uuids)
for (auto const & uuid : cache_set->with_uuids)
if (!contains_uuid(uuid))
{
good = false;

View file

@ -13,7 +13,7 @@ namespace psemek::ecs::detail
{
std::size_t operator()(util::span<util::uuid const> const & uuids) const
{
return component_hash(uuids);
return component_hash<true>(uuids);
}
std::size_t operator()(std::unique_ptr<table> const & table) const
@ -47,7 +47,7 @@ namespace psemek::ecs::detail
table * insert(std::unique_ptr<table> table);
template <typename Function>
void apply(Function && function, util::span<util::uuid const> component_uuids);
void apply(Function && function, util::span<util::uuid const> with_uuids, util::span<util::uuid const> without_uuids);
private:
util::hash_set<std::unique_ptr<table>, table_hashset_hash, table_hashset_equal> tables_;
@ -69,18 +69,26 @@ namespace psemek::ecs::detail
}
template <typename Function>
void table_container::apply(Function && function, util::span<util::uuid const> component_uuids)
void table_container::apply(Function && function, util::span<util::uuid const> with_uuids, util::span<util::uuid const> without_uuids)
{
for (auto & table : tables_)
{
bool good = true;
for (auto const & uuid : component_uuids)
for (auto const & uuid : with_uuids)
if (!table->column(uuid))
{
good = false;
break;
}
for (auto const & uuid : without_uuids)
if (table->column(uuid))
{
good = false;
break;
}
if (good) function(*table);
}
}

View file

@ -0,0 +1,70 @@
#pragma once
#include <psemek/ecs/without.hpp>
#include <tuple>
namespace psemek::ecs::detail
{
template <template <typename ...> typename MetaFunction, typename ExtraArgsTuple, typename FilteredComponentsTuple, typename ... Components>
struct filter_with_impl;
template <template <typename ...> typename MetaFunction, typename ... ExtraArgs, typename ... FilteredComponents>
struct filter_with_impl<MetaFunction, std::tuple<ExtraArgs...>, std::tuple<FilteredComponents...>>
{
using type = MetaFunction<ExtraArgs..., FilteredComponents...>;
};
template <template <typename ...> typename MetaFunction, typename ... ExtraArgs, typename ... FilteredComponents, typename Component, typename ... RemainingComponents>
struct filter_with_impl<MetaFunction, std::tuple<ExtraArgs...>, std::tuple<FilteredComponents...>, Component, RemainingComponents...>
{
using type = typename filter_with_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_with_impl<MetaFunction, std::tuple<ExtraArgs...>, std::tuple<FilteredComponents...>, ecs::without<Component>, 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;
template <template <typename ...> typename MetaFunction, typename ... ExtraArgs, typename ... FilteredComponents>
struct filter_without_impl<MetaFunction, std::tuple<ExtraArgs...>, std::tuple<FilteredComponents...>>
{
using type = MetaFunction<ExtraArgs..., FilteredComponents...>;
};
template <template <typename ...> typename MetaFunction, typename ... ExtraArgs, typename ... FilteredComponents, typename Component, typename ... RemainingComponents>
struct filter_without_impl<MetaFunction, std::tuple<ExtraArgs...>, std::tuple<FilteredComponents...>, Component, RemainingComponents...>
{
using type = typename filter_without_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_without_impl<MetaFunction, std::tuple<ExtraArgs...>, std::tuple<FilteredComponents...>, 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;
template <template <typename ...> typename MetaFunction, typename ComponentsTuple, typename ... ExtraArgs>
struct filter_without;
template <template <typename ...> typename MetaFunction, typename ... Components, typename ... ExtraArgs>
struct filter_with<MetaFunction, std::tuple<Components...>, ExtraArgs...>
{
using type = typename filter_with_impl<MetaFunction, std::tuple<ExtraArgs...>, std::tuple<>, Components...>::type;
};
template <template <typename ...> typename MetaFunction, typename ... Components, typename ... ExtraArgs>
struct filter_without<MetaFunction, std::tuple<Components...>, ExtraArgs...>
{
using type = typename filter_without_impl<MetaFunction, std::tuple<ExtraArgs...>, std::tuple<>, Components...>::type;
};
}

View file

@ -0,0 +1,16 @@
#pragma once
#include <type_traits>
namespace psemek::ecs
{
template <typename Component>
struct without
{
static_assert(!std::is_reference_v<Component>, "a component cannot be a reference");
using component_type = std::remove_const_t<Component>;
};
}

View file

@ -9,7 +9,7 @@ namespace psemek::ecs::detail
auto & entry = entries.emplace_back();
entry.table = table;
for (auto const & uuid : component_uuids)
for (auto const & uuid : with_uuids)
entry.columns.push_back(table->column(uuid));
}

View file

@ -6,7 +6,7 @@ namespace psemek::ecs::detail
table::table(std::vector<std::unique_ptr<detail::column>> columns)
{
component_hasher hasher;
component_hasher<true> hasher;
for (auto & column : columns)
{
auto uuid = column->uuid();

View file

@ -110,6 +110,59 @@ test_case(ecs_apply_components_2)
expect_equal(sum, expected_sum);
}
test_case(ecs_apply_without)
{
container container;
int const count = 1024;
int call_count = 0;
for (int i = 0; i < count; ++i)
container.create();
call_count = 0;
container.apply<without<component_2>>([&]{ ++call_count; });
expect_equal(count, call_count);
call_count = 0;
container.apply<component_1, without<component_2>>([&](component_1 const &){ ++call_count; });
expect_equal(0, call_count);
for (int i = 0; i < count; ++i)
container.create(component_1{i});
call_count = 0;
container.apply<without<component_2>>([&]{ ++call_count; });
expect_equal(count * 2, call_count);
call_count = 0;
container.apply<component_1, without<component_2>>([&](component_1 const &){ ++call_count; });
expect_equal(count, call_count);
for (int i = 0; i < count; ++i)
container.create(component_2{i});
call_count = 0;
container.apply<without<component_2>>([&]{ ++call_count; });
expect_equal(count * 2, call_count);
call_count = 0;
container.apply<component_1, without<component_2>>([&](component_1 const &){ ++call_count; });
expect_equal(count, call_count);
for (int i = 0; i < count; ++i)
container.create(component_1{i}, component_2{i});
call_count = 0;
container.apply<without<component_2>>([&]{ ++call_count; });
expect_equal(count * 2, call_count);
call_count = 0;
container.apply<component_1, without<component_2>>([&](component_1 const &){ ++call_count; });
expect_equal(count, call_count);
}
test_case(ecs_apply_batch_invoke)
{
container container;

View file

@ -44,7 +44,7 @@ test_case(ecs_cache_empty)
auto cache = container.cache();
expect_different_ptr(cache.get(), nullptr);
expect(cache->component_uuids.empty());
expect(cache->with_uuids.empty());
expect_equal(cache->entries.size(), 4);
}
@ -60,7 +60,7 @@ test_case(ecs_cache_components)
auto cache = container.cache<component_1>();
expect_different_ptr(cache.get(), nullptr);
expect_equal(cache->component_uuids.size(), 1);
expect_equal(cache->with_uuids.size(), 1);
expect_equal(cache->entries.size(), 2);
}
@ -71,23 +71,23 @@ test_case(ecs_cache_update)
auto cache = container.cache<component_1>();
expect_different_ptr(cache.get(), nullptr);
expect_equal(cache->component_uuids.size(), 1);
expect_equal(cache->with_uuids.size(), 1);
expect_equal(cache->entries.size(), 0);
container.create();
expect_equal(cache->component_uuids.size(), 1);
expect_equal(cache->with_uuids.size(), 1);
expect_equal(cache->entries.size(), 0);
container.create(component_1{10});
expect_equal(cache->component_uuids.size(), 1);
expect_equal(cache->with_uuids.size(), 1);
expect_equal(cache->entries.size(), 1);
container.create(component_2{20});
expect_equal(cache->component_uuids.size(), 1);
expect_equal(cache->with_uuids.size(), 1);
expect_equal(cache->entries.size(), 1);
container.create(component_1{100}, component_2{200});
expect_equal(cache->component_uuids.size(), 1);
expect_equal(cache->with_uuids.size(), 1);
expect_equal(cache->entries.size(), 2);
}