diff --git a/libs/util/include/psemek/util/ecs.hpp b/libs/util/include/psemek/util/ecs.hpp new file mode 100644 index 00000000..7c74d3b4 --- /dev/null +++ b/libs/util/include/psemek/util/ecs.hpp @@ -0,0 +1,310 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace psemek::util +{ + + namespace ecs_detail + { + + using handle = std::uint32_t; + + struct species_base + { + virtual std::string_view name() const = 0; + + virtual void * get_species_component(std::type_index component_type) = 0; + virtual void * get_entity_component(std::type_index component_type) = 0; + + template + Component * get_species_component() + { + return reinterpret_cast(get_species_component(typeid(Component))); + } + + template + typename Component::data * get_entity_component() + { + return reinterpret_cast(get_entity_component(typeid(Component))); + } + + virtual handle entity_count() const = 0; + + virtual handle add_entity() = 0; + + template + void apply(Behavior & behavior) + { + apply_impl(behavior, typename Behavior::components{}); + } + + virtual ~species_base() {} + + private: + + template + void apply_impl(Behavior & behavior, std::tuple) + { + std::tuple cptrs; + + ((std::get(cptrs) = get_entity_component()), ...); + + auto all_nonzero = [](auto * ... ptrs) + { + return ((ptrs != nullptr) && ...); + }; + + if (!std::apply(all_nonzero, cptrs)) + return; + + std::tuple cs; + + ((std::get(cs) = get_species_component()), ...); + + auto visit = [&](auto * ... ptrs) + { + if constexpr (std::is_invocable_v const &>) + { + behavior(*ptrs..., cs); + } + else + { + behavior(*ptrs...); + } + }; + + auto increment = [](auto * & ... ptrs) + { + ((++ptrs), ...); + }; + + std::size_t const count = entity_count(); + + for (std::size_t i = 0; i < count; ++i) + { + std::apply(visit, cptrs); + std::apply(increment, cptrs); + } + } + }; + + template + struct species_impl + : species_base + { + species_impl(std::string name, Components && ... components) + : name_(std::move(name)) + , species_components_{std::move(components)...} + {} + + std::string_view name() const override + { + return name_; + } + + using species_base::get_species_component; + using species_base::get_entity_component; + + void * get_species_component(std::type_index component_type) override + { + return get_species_component_impl(component_type, std::make_index_sequence{}); + } + + void * get_entity_component(std::type_index component_type) override + { + return get_entity_component_impl(component_type, std::make_index_sequence{}); + } + + handle entity_count() const override + { + return std::get<0>(entity_components_).size(); + } + + handle add_entity() override + { + handle result = entity_count(); + add_entity_impl(std::make_index_sequence{}); + return result; + } + + private: + std::string name_; + std::tuple species_components_; + std::tuple...> entity_components_; + + template + void * get_species_component_impl(std::type_index component_type, std::index_sequence) + { + void * result = nullptr; + + ((result = (component_type == typeid(std::tuple_element_t>)) ? &std::get(species_components_) : result), ...); + return result; + } + + template + void * get_entity_component_impl(std::type_index component_type, std::index_sequence) + { + void * result = nullptr; + + ((result = (component_type == typeid(std::tuple_element_t>)) ? std::get(entity_components_).data() : result), ...); + return result; + } + + template + void add_entity_impl(std::index_sequence) + { + ((std::get(entity_components_).emplace_back()), ...); + } + }; + + template <> + struct species_impl<> + : species_base + { + species_impl(std::string name) + : name_(std::move(name)) + {} + + std::string_view name() const override + { + return name_; + } + + using species_base::get_species_component; + using species_base::get_entity_component; + + void * get_species_component(std::type_index) override { return nullptr; } + void * get_entity_component(std::type_index) override { return nullptr; } + + virtual handle entity_count() const + { + return entity_count_; + } + + virtual handle add_entity() + { + return entity_count_++; + } + + private: + std::string name_; + std::size_t entity_count_ = 0; + }; + + template + using prepare_helper = decltype(std::declval().prepare()); + + template + using has_prepare = std::experimental::is_detected; + + } + + struct ecs + { + template + struct behavior + { + using components = std::tuple; + using component_ptrs = std::tuple; + }; + + using handle = ecs_detail::handle; + + template + handle register_species(std::string name, Components && ... components); + + handle add_entity(handle species); + handle entity_count(handle species) const; + + template + Component & get(handle species); + + template + Component const & get(handle species) const; + + template + typename Component::data & get(handle species, handle entity); + + template + typename Component::data const & get(handle species, handle entity) const; + + template + void apply(Behavior && behavior) const; + + private: + std::vector> species_; + }; + + template + ecs::handle ecs::register_species(std::string name, Components && ... components) + { + handle result = species_.size(); + species_.push_back(std::make_unique>(std::move(name), std::move(components)...)); + return result; + } + + inline ecs::handle ecs::add_entity(handle species) + { + return species_[species]->add_entity(); + } + + inline ecs::handle ecs::entity_count(handle species) const + { + return species_[species]->entity_count(); + } + + template + Component & ecs::get(handle species) + { + auto p = species_[species]->get_species_component(); + if (!p) + throw std::runtime_error(util::to_string("Component ", type_name(), " is not present in species ", species_[species]->name())); + return *p; + } + + template + Component const & ecs::get(handle species) const + { + return const_cast(const_cast(this)->get(species)); + } + + template + typename Component::data & ecs::get(handle species, handle entity) + { + auto p = species_[species]->get_entity_component(); + if (!p) + throw std::runtime_error(util::to_string("Component ", type_name(), " is not present in species ", species_[species]->name())); + return p[entity]; + } + + template + typename Component::data const & ecs::get(handle species, handle entity) const + { + return const_cast(const_cast(this)->get(species, entity)); + } + + template + void ecs::apply(Behavior && behavior) const + { + if constexpr (ecs_detail::has_prepare::value) + { + behavior.prepare(); + } + + for (auto & s : species_) + s->apply(behavior); + } + +} diff --git a/libs/util/tests/ecs.cpp b/libs/util/tests/ecs.cpp new file mode 100644 index 00000000..980f2d49 --- /dev/null +++ b/libs/util/tests/ecs.cpp @@ -0,0 +1,285 @@ +#include + +#include + +namespace +{ + + using namespace psemek::util; + + struct test_component_1 + { + int species_value_1; + + struct data + { + int entity_value_1; + }; + }; + + struct test_component_2 + { + int species_value_2; + + struct data + { + int entity_value_2; + }; + }; + + struct test_behavior_1 + : ecs::behavior + { + int call_count = 0; + int value_sum = 0; + + void operator()(test_component_1::data & c) + { + call_count += 1; + value_sum += c.entity_value_1; + } + }; + + struct test_behavior_12 + : ecs::behavior + { + int call_count = 0; + int value_sum_1 = 0; + int value_sum_2 = 0; + + void operator()(test_component_1::data & c1, test_component_2::data & c2) + { + call_count += 1; + value_sum_1 += c1.entity_value_1; + value_sum_2 += c2.entity_value_2; + } + }; + + struct test_behavior_species_12 + : ecs::behavior + { + int expected_value_1; + int expected_value_2; + + int call_count = 0; + + void operator()(test_component_1::data &, test_component_2::data &, components const & cs) + { + expect_equal(std::get(cs).species_value_1, expected_value_1); + expect_equal(std::get(cs).species_value_2, expected_value_2); + + ++call_count; + } + }; + +} + +test_case(util_ecs_species__impl_component) +{ + using namespace psemek::util::ecs_detail; + + species_impl species("species 1", test_component_1{10}); + + expect_different_ptr(species.get_species_component(), nullptr); + expect_equal(species.get_species_component()->species_value_1, 10); + expect_equal_ptr(species.get_species_component(), nullptr); + + species_impl species_2("species 2", test_component_2{20}); + + expect_equal_ptr(species_2.get_species_component(), nullptr); + expect_different_ptr(species_2.get_species_component(), nullptr); + expect_equal(species_2.get_species_component()->species_value_2, 20); + + species_impl species_12("species 12", test_component_1{100}, test_component_2{200}); + + expect_different_ptr(species_12.get_species_component(), nullptr); + expect_equal(species_12.get_species_component()->species_value_1, 100); + expect_different_ptr(species_12.get_species_component(), nullptr); + expect_equal(species_12.get_species_component()->species_value_2, 200); +} + +test_case(util_ecs_species__impl_entity) +{ + using namespace psemek::util::ecs_detail; + + species_impl species("species", test_component_1{10}); + + expect_equal(species.entity_count(), 0); + + int const N = 10; + + for (std::size_t i = 0; i < N; ++i) + species.add_entity(); + + expect_equal(species.entity_count(), N); + + test_component_1::data * cptr_1 = species.get_entity_component(); + + expect_different(cptr_1, nullptr); + + for (std::size_t i = 0; i < N; ++i) + cptr_1[i].entity_value_1 = i; + + for (std::size_t i = 0; i < N; ++i) + species.add_entity(); + + expect_equal(species.entity_count(), 2 * N); + + cptr_1 = species.get_entity_component(); + + expect_different(cptr_1, nullptr); + + for (std::size_t i = 0; i < N; ++i) + expect_equal(cptr_1[i].entity_value_1, i); +} + +test_case(util_ecs_entity) +{ + using namespace psemek; + + util::ecs ecs; + + auto species = ecs.register_species("species", test_component_1{10}); + + expect_equal(ecs.entity_count(species), 0); + + auto entity = ecs.add_entity(species); + + ecs.get(species, entity).entity_value_1 = 100; + + expect_equal(ecs.get(species, entity).entity_value_1, 100); + + expect_throw(ecs.get(species, entity), std::exception); +} + +test_case(util_ecs_behavior_1) +{ + using namespace psemek; + + util::ecs ecs; + + auto species = ecs.register_species("species", test_component_1{10}); + + expect_equal(ecs.entity_count(species), 0); + + int N = 100; + + for (int i = 0; i < N; ++i) + { + auto entity = ecs.add_entity(species); + ecs.get(species, entity).entity_value_1 = i; + } + + test_behavior_1 behavior_1; + + ecs.apply(behavior_1); + + expect_equal(behavior_1.call_count, N); + expect_equal(behavior_1.value_sum, (N * (N - 1)) / 2); + + test_behavior_12 behavior_12; + + ecs.apply(behavior_12); + + expect_equal(behavior_12.call_count, 0); + expect_equal(behavior_12.value_sum_1, 0); + expect_equal(behavior_12.value_sum_2, 0); +} + +test_case(util_ecs_behavior_1__2) +{ + using namespace psemek; + + util::ecs ecs; + + auto species_1 = ecs.register_species("species 1", test_component_1{10}); + auto species_2 = ecs.register_species("species 2", test_component_2{20}); + + int N = 100; + + for (int i = 0; i < N; ++i) + { + auto entity = ecs.add_entity(species_1); + ecs.get(species_1, entity).entity_value_1 = i; + } + + for (int i = 0; i < N; ++i) + { + auto entity = ecs.add_entity(species_2); + ecs.get(species_2, entity).entity_value_2 = -i; + } + + test_behavior_1 behavior; + + ecs.apply(behavior); + + expect_equal(behavior.call_count, N); + expect_equal(behavior.value_sum, (N * (N - 1)) / 2); + + test_behavior_12 behavior_12; + + ecs.apply(behavior_12); + + expect_equal(behavior_12.call_count, 0); + expect_equal(behavior_12.value_sum_1, 0); + expect_equal(behavior_12.value_sum_2, 0); +} + +test_case(util_ecs_behavior_12) +{ + using namespace psemek; + + util::ecs ecs; + + auto species = ecs.register_species("species", test_component_1{10}, test_component_2{20}); + + int N = 100; + + for (int i = 0; i < N; ++i) + { + auto entity = ecs.add_entity(species); + ecs.get(species, entity).entity_value_1 = i; + ecs.get(species, entity).entity_value_2 = -i; + } + + test_behavior_1 behavior; + + ecs.apply(behavior); + + expect_equal(behavior.call_count, N); + expect_equal(behavior.value_sum, (N * (N - 1)) / 2); + + test_behavior_12 behavior_12; + + ecs.apply(behavior_12); + + expect_equal(behavior_12.call_count, N); + expect_equal(behavior_12.value_sum_1, (N * (N - 1)) / 2); + expect_equal(behavior_12.value_sum_2, - (N * (N - 1)) / 2); +} + +test_case(util_ecs_behavior_species) +{ + using namespace psemek; + + util::ecs ecs; + + auto species = ecs.register_species("species", test_component_1{10}, test_component_2{20}); + + int N = 100; + + for (int i = 0; i < N; ++i) + { + auto entity = ecs.add_entity(species); + ecs.get(species, entity).entity_value_1 = 0; + ecs.get(species, entity).entity_value_2 = 0; + } + + test_behavior_species_12 behavior; + behavior.expected_value_1 = 10; + behavior.expected_value_2 = 20; + + ecs.apply(behavior); + + expect_equal(behavior.call_count, N); +}