Add prototype ecs implementation with tests
This commit is contained in:
parent
fca22c93e2
commit
640bbf47c7
2 changed files with 595 additions and 0 deletions
310
libs/util/include/psemek/util/ecs.hpp
Normal file
310
libs/util/include/psemek/util/ecs.hpp
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/util/to_string.hpp>
|
||||
#include <psemek/util/type_name.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <typeindex>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <experimental/type_traits>
|
||||
|
||||
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 <typename Component>
|
||||
Component * get_species_component()
|
||||
{
|
||||
return reinterpret_cast<Component *>(get_species_component(typeid(Component)));
|
||||
}
|
||||
|
||||
template <typename Component>
|
||||
typename Component::data * get_entity_component()
|
||||
{
|
||||
return reinterpret_cast<typename Component::data *>(get_entity_component(typeid(Component)));
|
||||
}
|
||||
|
||||
virtual handle entity_count() const = 0;
|
||||
|
||||
virtual handle add_entity() = 0;
|
||||
|
||||
template <typename Behavior>
|
||||
void apply(Behavior & behavior)
|
||||
{
|
||||
apply_impl(behavior, typename Behavior::components{});
|
||||
}
|
||||
|
||||
virtual ~species_base() {}
|
||||
|
||||
private:
|
||||
|
||||
template <typename Behavior, typename ... Components>
|
||||
void apply_impl(Behavior & behavior, std::tuple<Components...>)
|
||||
{
|
||||
std::tuple<typename Components::data * ...> cptrs;
|
||||
|
||||
((std::get<typename Components::data *>(cptrs) = get_entity_component<Components>()), ...);
|
||||
|
||||
auto all_nonzero = [](auto * ... ptrs)
|
||||
{
|
||||
return ((ptrs != nullptr) && ...);
|
||||
};
|
||||
|
||||
if (!std::apply(all_nonzero, cptrs))
|
||||
return;
|
||||
|
||||
std::tuple<Components *...> cs;
|
||||
|
||||
((std::get<Components *>(cs) = get_species_component<Components>()), ...);
|
||||
|
||||
auto visit = [&](auto * ... ptrs)
|
||||
{
|
||||
if constexpr (std::is_invocable_v<Behavior, typename Components::data & ..., std::tuple<Components *...> 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 <typename ... Components>
|
||||
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<sizeof...(Components)>{});
|
||||
}
|
||||
|
||||
void * get_entity_component(std::type_index component_type) override
|
||||
{
|
||||
return get_entity_component_impl(component_type, std::make_index_sequence<sizeof...(Components)>{});
|
||||
}
|
||||
|
||||
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<sizeof...(Components)>{});
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
std::tuple<Components...> species_components_;
|
||||
std::tuple<std::vector<typename Components::data>...> entity_components_;
|
||||
|
||||
template <std::size_t ... I>
|
||||
void * get_species_component_impl(std::type_index component_type, std::index_sequence<I...>)
|
||||
{
|
||||
void * result = nullptr;
|
||||
|
||||
((result = (component_type == typeid(std::tuple_element_t<I, std::tuple<Components...>>)) ? &std::get<I>(species_components_) : result), ...);
|
||||
return result;
|
||||
}
|
||||
|
||||
template <std::size_t ... I>
|
||||
void * get_entity_component_impl(std::type_index component_type, std::index_sequence<I...>)
|
||||
{
|
||||
void * result = nullptr;
|
||||
|
||||
((result = (component_type == typeid(std::tuple_element_t<I, std::tuple<Components...>>)) ? std::get<I>(entity_components_).data() : result), ...);
|
||||
return result;
|
||||
}
|
||||
|
||||
template <std::size_t ... I>
|
||||
void add_entity_impl(std::index_sequence<I...>)
|
||||
{
|
||||
((std::get<I>(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 <typename Behavior>
|
||||
using prepare_helper = decltype(std::declval<Behavior>().prepare());
|
||||
|
||||
template <typename Behavior>
|
||||
using has_prepare = std::experimental::is_detected<prepare_helper, Behavior>;
|
||||
|
||||
}
|
||||
|
||||
struct ecs
|
||||
{
|
||||
template <typename ... Components>
|
||||
struct behavior
|
||||
{
|
||||
using components = std::tuple<Components...>;
|
||||
using component_ptrs = std::tuple<Components *...>;
|
||||
};
|
||||
|
||||
using handle = ecs_detail::handle;
|
||||
|
||||
template <typename ... Components>
|
||||
handle register_species(std::string name, Components && ... components);
|
||||
|
||||
handle add_entity(handle species);
|
||||
handle entity_count(handle species) const;
|
||||
|
||||
template <typename Component>
|
||||
Component & get(handle species);
|
||||
|
||||
template <typename Component>
|
||||
Component const & get(handle species) const;
|
||||
|
||||
template <typename Component>
|
||||
typename Component::data & get(handle species, handle entity);
|
||||
|
||||
template <typename Component>
|
||||
typename Component::data const & get(handle species, handle entity) const;
|
||||
|
||||
template <typename Behavior>
|
||||
void apply(Behavior && behavior) const;
|
||||
|
||||
private:
|
||||
std::vector<std::unique_ptr<ecs_detail::species_base>> species_;
|
||||
};
|
||||
|
||||
template <typename ... Components>
|
||||
ecs::handle ecs::register_species(std::string name, Components && ... components)
|
||||
{
|
||||
handle result = species_.size();
|
||||
species_.push_back(std::make_unique<ecs_detail::species_impl<Components...>>(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 <typename Component>
|
||||
Component & ecs::get(handle species)
|
||||
{
|
||||
auto p = species_[species]->get_species_component<Component>();
|
||||
if (!p)
|
||||
throw std::runtime_error(util::to_string("Component ", type_name<Component>(), " is not present in species ", species_[species]->name()));
|
||||
return *p;
|
||||
}
|
||||
|
||||
template <typename Component>
|
||||
Component const & ecs::get(handle species) const
|
||||
{
|
||||
return const_cast<typename Component::data const &>(const_cast<ecs *>(this)->get<Component>(species));
|
||||
}
|
||||
|
||||
template <typename Component>
|
||||
typename Component::data & ecs::get(handle species, handle entity)
|
||||
{
|
||||
auto p = species_[species]->get_entity_component<Component>();
|
||||
if (!p)
|
||||
throw std::runtime_error(util::to_string("Component ", type_name<Component>(), " is not present in species ", species_[species]->name()));
|
||||
return p[entity];
|
||||
}
|
||||
|
||||
template <typename Component>
|
||||
typename Component::data const & ecs::get(handle species, handle entity) const
|
||||
{
|
||||
return const_cast<typename Component::data const &>(const_cast<ecs *>(this)->get<Component>(species, entity));
|
||||
}
|
||||
|
||||
template <typename Behavior>
|
||||
void ecs::apply(Behavior && behavior) const
|
||||
{
|
||||
if constexpr (ecs_detail::has_prepare<Behavior>::value)
|
||||
{
|
||||
behavior.prepare();
|
||||
}
|
||||
|
||||
for (auto & s : species_)
|
||||
s->apply(behavior);
|
||||
}
|
||||
|
||||
}
|
||||
285
libs/util/tests/ecs.cpp
Normal file
285
libs/util/tests/ecs.cpp
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
#include <psemek/test/test.hpp>
|
||||
|
||||
#include <psemek/util/ecs.hpp>
|
||||
|
||||
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<test_component_1>
|
||||
{
|
||||
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<test_component_1, test_component_2>
|
||||
{
|
||||
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<test_component_1, test_component_2>
|
||||
{
|
||||
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<test_component_1>(cs).species_value_1, expected_value_1);
|
||||
expect_equal(std::get<test_component_2>(cs).species_value_2, expected_value_2);
|
||||
|
||||
++call_count;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
test_case(util_ecs_species__impl_component)
|
||||
{
|
||||
using namespace psemek::util::ecs_detail;
|
||||
|
||||
species_impl<test_component_1> species("species 1", test_component_1{10});
|
||||
|
||||
expect_different_ptr(species.get_species_component<test_component_1>(), nullptr);
|
||||
expect_equal(species.get_species_component<test_component_1>()->species_value_1, 10);
|
||||
expect_equal_ptr(species.get_species_component<test_component_2>(), nullptr);
|
||||
|
||||
species_impl<test_component_2> species_2("species 2", test_component_2{20});
|
||||
|
||||
expect_equal_ptr(species_2.get_species_component<test_component_1>(), nullptr);
|
||||
expect_different_ptr(species_2.get_species_component<test_component_2>(), nullptr);
|
||||
expect_equal(species_2.get_species_component<test_component_2>()->species_value_2, 20);
|
||||
|
||||
species_impl<test_component_1, test_component_2> species_12("species 12", test_component_1{100}, test_component_2{200});
|
||||
|
||||
expect_different_ptr(species_12.get_species_component<test_component_1>(), nullptr);
|
||||
expect_equal(species_12.get_species_component<test_component_1>()->species_value_1, 100);
|
||||
expect_different_ptr(species_12.get_species_component<test_component_2>(), nullptr);
|
||||
expect_equal(species_12.get_species_component<test_component_2>()->species_value_2, 200);
|
||||
}
|
||||
|
||||
test_case(util_ecs_species__impl_entity)
|
||||
{
|
||||
using namespace psemek::util::ecs_detail;
|
||||
|
||||
species_impl<test_component_1> 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<test_component_1>();
|
||||
|
||||
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<test_component_1>();
|
||||
|
||||
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<test_component_1>(species, entity).entity_value_1 = 100;
|
||||
|
||||
expect_equal(ecs.get<test_component_1>(species, entity).entity_value_1, 100);
|
||||
|
||||
expect_throw(ecs.get<test_component_2>(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<test_component_1>(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<test_component_1>(species_1, entity).entity_value_1 = i;
|
||||
}
|
||||
|
||||
for (int i = 0; i < N; ++i)
|
||||
{
|
||||
auto entity = ecs.add_entity(species_2);
|
||||
ecs.get<test_component_2>(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<test_component_1>(species, entity).entity_value_1 = i;
|
||||
ecs.get<test_component_2>(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<test_component_1>(species, entity).entity_value_1 = 0;
|
||||
ecs.get<test_component_2>(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);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue