ECS library wip & tests

This commit is contained in:
Nikita Lisitsa 2023-08-22 20:30:04 +03:00
parent c6805dea21
commit e0e0df8128
10 changed files with 441 additions and 66 deletions

View file

@ -5,5 +5,5 @@ psemek_add_library(psemek-ecs ${PSEMEK_ECS_HEADERS} ${PSEMEK_ECS_SOURCES})
target_include_directories(psemek-ecs PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") target_include_directories(psemek-ecs PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(psemek-ecs PUBLIC psemek-util) target_link_libraries(psemek-ecs PUBLIC psemek-util)
#psemek_glob_tests(psemek-ecs tests) psemek_glob_tests(psemek-ecs tests)

View file

@ -12,7 +12,8 @@ namespace psemek::ecs::detail
std::size_t row_count; std::size_t row_count;
entity_id const * entity_id_pointer; entity_id const * entity_id_pointer;
entity_data const * entity_data_pointer; entity_data const * entity_data_pointer;
table::component_pointer pointers[sizeof...(Components)]; // (+1) to prevent zero-sized array
table::component_pointer pointers[sizeof...(Components) + 1];
static_apply_helper(util::span<entity_id const> entity_ids, util::span<entity_data const> entities) static_apply_helper(util::span<entity_id const> entity_ids, util::span<entity_data const> entities)
: row_count(entity_ids.size()) : row_count(entity_ids.size())

View file

@ -8,6 +8,26 @@
namespace psemek::ecs::detail namespace psemek::ecs::detail
{ {
template <typename ... Components>
struct component_uuid_holder
{
util::uuid uuids[sizeof...(Components)] { Components::uuid() ... };
auto get() const
{
return util::span<util::uuid const>(uuids);
}
};
template <>
struct component_uuid_holder<>
{
auto get() const
{
return util::span<util::uuid const>();
}
};
struct component_index struct component_index
{ {
template <typename ... UUIDS> template <typename ... UUIDS>

View file

@ -67,7 +67,6 @@ namespace psemek::ecs::detail
table_impl(util::span<util::uuid const> component_uuids); table_impl(util::span<util::uuid const> component_uuids);
std::size_t push_row(entity_id id) override; std::size_t push_row(entity_id id) override;
std::size_t push_row_with_components(entity_id id, Components && ... components);
void swap_rows(std::size_t row1, std::size_t row2) override; void swap_rows(std::size_t row1, std::size_t row2) override;
void pop_row() override; void pop_row() override;
@ -108,29 +107,6 @@ namespace psemek::ecs::detail
return row_count_ - 1; return row_count_ - 1;
} }
template <typename ... Components>
std::size_t table_impl<Components...>::push_row_with_components(entity_id id, Components && ... components)
{
if (row_count_ == capacity_)
reallocate();
[[maybe_unused]] auto push_row_impl = [&]<typename Component>(std::uint8_t * data, Component && value)
{
if constexpr (!detail::is_empty_v<Component>)
{
new (reinterpret_cast<Component *>(data) + row_count_) Component{std::move(value)};
}
};
std::size_t i = 0;
(push_row_impl.template operator()<Components>(component_pointers_[i++].data, std::move(components)), ...);
++row_count_;
entity_ids_.push_back(id);
return row_count_ - 1;
}
template <typename ... Components> template <typename ... Components>
void table_impl<Components...>::swap_rows(std::size_t row1, std::size_t row2) void table_impl<Components...>::swap_rows(std::size_t row1, std::size_t row2)
{ {
@ -198,6 +174,8 @@ namespace psemek::ecs::detail
std::size_t i = 0; std::size_t i = 0;
(reallocate_impl.template operator()<Components>(component_pointers_[i++].data), ...); (reallocate_impl.template operator()<Components>(component_pointers_[i++].data), ...);
capacity_ = new_capacity;
} }
} }

View file

@ -1,16 +1,36 @@
#pragma once #pragma once
#include <psemek/ecs/detail/table.hpp> #include <psemek/ecs/detail/table.hpp>
#include <psemek/util/assert.hpp>
namespace psemek::ecs namespace psemek::ecs
{ {
struct entity_accessor struct entity_accessor
{ {
entity_accessor(detail::table * table, std::uint32_t row)
: table_(table)
, row_(row)
{}
template <typename Component>
Component & get()
{
util::uuid const uuid = Component::uuid();
auto const component_uuids = table_->get_component_uuids();
for (std::size_t i = 0; i < component_uuids.size(); ++i)
if (uuid == component_uuids[i])
return *reinterpret_cast<Component *>(table_->get_component_pointers()[i].data + detail::stride<Component>() * row_);
assert(false);
__builtin_unreachable();
}
private: private:
detail::table * table_;
std::uint32_t row_;
}; };
} }

View file

@ -12,33 +12,22 @@ namespace psemek::ecs
struct entity_container struct entity_container
{ {
entity_handle create()
{
// Specialization for an empty entity to
// prevent creation of an empty uuid array
detail::component_mask mask;
auto & table = table_container_.insert<>(mask, {});
auto id = entity_list_.create(&table, table.row_count());
table.push_row(id);
return {id, 0};
}
template <typename ... Components> template <typename ... Components>
entity_handle create(Components && ... components) entity_handle create(Components && ... components)
{ {
util::uuid component_uuids[] { components.uuid()... }; detail::component_uuid_holder<Components...> uuids;
detail::component_mask mask = component_index_.make_component_mask(util::span<util::uuid const>(component_uuids)); detail::component_mask mask = component_index_.make_component_mask(uuids.get());
auto & table = table_container_.insert<Components...>(mask, component_uuids); auto & table = table_container_.insert<Components...>(mask, uuids.get());
auto id = entity_list_.create(&table, table.row_count()); auto id = entity_list_.create(&table, table.row_count());
table.push_row_with_components(id, std::move(components)...); entity_handle handle{id, entity_list_.get_entities()[id].epoch};
[[maybe_unused]] entity_accessor accessor = get(handle);
return {id, entity_list_.get_entities()[id].epoch}; table.push_row(id);
((accessor.get<Components>() = std::move(components)), ...);
return handle;
} }
bool alive(entity_handle const & entity) const bool alive(entity_handle const & entity) const
@ -63,16 +52,16 @@ namespace psemek::ecs
template <typename ... Components, typename Function> template <typename ... Components, typename Function>
void apply(Function && function) void apply(Function && function)
{ {
util::uuid const component_uuids[] { Components::uuid() ... }; detail::component_uuid_holder<Components...> uuids;
detail::component_mask mask = component_index_.make_component_mask(util::span<util::uuid const>(component_uuids)); detail::component_mask mask = component_index_.make_component_mask(uuids.get());
table_container_.apply([&](detail::table & table){ table_container_.apply([&](detail::table & table){
detail::static_apply_helper<Components...> apply_helper(table.get_entity_ids(), entity_list_.get_entities()); detail::static_apply_helper<Components...> apply_helper(table.get_entity_ids(), entity_list_.get_entities());
for (std::size_t i = 0; i < sizeof...(Components); ++i) for (std::size_t i = 0; i < sizeof...(Components); ++i)
for (std::size_t j = 0; j < table.component_count(); ++j) for (std::size_t j = 0; j < table.component_count(); ++j)
if (component_uuids[i] == table.get_component_uuids()[j]) if (uuids.get()[i] == table.get_component_uuids()[j])
apply_helper.pointers[i++] = table.get_component_pointers()[j]; apply_helper.pointers[i++] = table.get_component_pointers()[j];
for (std::size_t i = 0; i < apply_helper.size(); ++i) for (std::size_t i = 0; i < apply_helper.size(); ++i)
@ -84,21 +73,10 @@ namespace psemek::ecs
}, mask); }, mask);
} }
template <typename Component> entity_accessor get(entity_handle const & entity)
Component & get(entity_handle const & entity)
{ {
util::uuid const uuid = Component::uuid();
auto const & data = entity_list_.get_entities()[entity.id]; auto const & data = entity_list_.get_entities()[entity.id];
return {data.table, data.row};
auto const component_uuids = data.table->get_component_uuids();
for (std::size_t i = 0; i < component_uuids.size(); ++i)
if (uuid == component_uuids[i])
return *reinterpret_cast<Component *>(data.table->get_component_pointers()[i].data + detail::stride<Component>() * data.row);
assert(false);
__builtin_unreachable();
} }
private: private:

View file

@ -25,10 +25,8 @@ namespace psemek::ecs::detail
void entity_list::allocate_ids() void entity_list::allocate_ids()
{ {
static constexpr std::size_t batch_size = 1024;
auto old_size = entities_.size(); auto old_size = entities_.size();
entities_.resize(entities_.size() + batch_size); entities_.resize(std::max<std::size_t>(1024, entities_.size() * 2));
for (std::size_t id = entities_.size(); id --> old_size;) for (std::size_t id = entities_.size(); id --> old_size;)
free_ids_.push_back(id); free_ids_.push_back(id);
} }

113
libs/ecs/tests/apply.cpp Normal file
View file

@ -0,0 +1,113 @@
#include <psemek/test/test.hpp>
#include <psemek/ecs/entity_container.hpp>
#include <psemek/random/generator.hpp>
#include <psemek/random/uniform.hpp>
using namespace psemek;
using namespace psemek::ecs;
namespace
{
struct component_1
{
int value;
static constexpr util::uuid uuid()
{
return {1, 0};
}
};
struct component_2
{
int value;
static constexpr util::uuid uuid()
{
return {2, 0};
}
};
}
test_case(ecs_apply_empty)
{
entity_container container;
int const count = 2048;
for (int i = 0; i < count; ++i)
container.create();
int call_count = 0;
container.apply<>([&](ecs::entity_handle const &){ ++call_count; });
expect_equal(count, call_count);
}
test_case(ecs_apply_components_1)
{
entity_container container;
random::generator rng;
int const expected_count = 1024 * 1024;
int expected_sum = 0;
for (int i = 0; i < expected_count; ++i)
{
int value = random::uniform(rng, -1024, 1024);
expected_sum += value;
container.create(component_1{value});
}
int count = 0;
int sum = 0;
container.apply<component_1>([&](ecs::entity_handle const &, component_1 const & component){
++count;
sum += component.value;
});
expect_equal(count, expected_count);
expect_equal(sum, expected_sum);
}
test_case(ecs_apply_components_2)
{
entity_container container;
random::generator rng;
int const expected_count = 1024*1024;
int expected_sum = 0;
for (int i = 0; i < expected_count; ++i)
{
int value = random::uniform(rng, -1024, 1024);
int type = random::uniform(rng, 0, 2);
if (type == 0)
container.create(component_1{value});
else if (type == 1)
container.create(component_2{value});
else if (type == 2)
container.create(component_1{value}, component_2{value});
expected_sum += value;
}
int count = 0;
int sum = 0;
container.apply<component_1>([&](ecs::entity_handle const &, component_1 const & component){
++count;
sum += component.value;
});
container.apply<component_2>([&](ecs::entity_handle const &, component_2 const & component){
++count;
sum += component.value;
});
container.apply<component_1, component_2>([&](ecs::entity_handle const &, component_1 const & component1, component_2 const &){
--count;
sum -= component1.value;
});
expect_equal(count, expected_count);
expect_equal(sum, expected_sum);
}

View file

@ -0,0 +1,120 @@
#include <psemek/test/test.hpp>
#include <psemek/ecs/entity_container.hpp>
#include <psemek/random/generator.hpp>
#include <psemek/random/uniform.hpp>
using namespace psemek;
using namespace psemek::ecs;
namespace
{
struct component_small
{
int value;
static constexpr util::uuid uuid()
{
return {1, 0};
}
};
struct component_big
{
int values[16];
static constexpr util::uuid uuid()
{
return {2, 0};
}
};
struct component_noncopyable
{
std::unique_ptr<int> value;
static constexpr util::uuid uuid()
{
return {2, 0};
}
};
struct component_counter
{
std::shared_ptr<int> value;
static constexpr util::uuid uuid()
{
return {2, 0};
}
};
}
test_case(ecs_component_order)
{
entity_container container;
auto h0 = container.create(component_small{10}, component_big{});
expect(container.alive(h0));
expect_equal(container.get(h0).get<component_small>().value, 10);
auto h1 = container.create(component_big{}, component_small{20});
expect(container.alive(h0));
expect(container.alive(h1));
expect_equal(container.get(h0).get<component_small>().value, 10);
expect_equal(container.get(h1).get<component_small>().value, 20);
auto h2 = container.create(component_small{30});
expect(container.alive(h0));
expect(container.alive(h1));
expect(container.alive(h2));
expect_equal(container.get(h0).get<component_small>().value, 10);
expect_equal(container.get(h1).get<component_small>().value, 20);
expect_equal(container.get(h2).get<component_small>().value, 30);
}
test_case(ecs_component_noncopyable)
{
entity_container container;
auto h0 = container.create(component_noncopyable{std::make_unique<int>(10)});
expect(container.alive(h0));
expect_equal(*container.get(h0).get<component_noncopyable>().value, 10);
auto h1 = container.create(component_noncopyable{std::make_unique<int>(20)});
expect(container.alive(h0));
expect(container.alive(h1));
expect_equal(*container.get(h0).get<component_noncopyable>().value, 10);
expect_equal(*container.get(h1).get<component_noncopyable>().value, 20);
container.destroy(h0);
expect(!container.alive(h0));
expect(container.alive(h1));
expect_equal(*container.get(h1).get<component_noncopyable>().value, 20);
}
test_case(ecs_component_lifetime)
{
random::generator rng;
entity_container container;
std::vector<entity_handle> entities;
for (int i = 0; i < 1024 * 1024; ++i)
entities.push_back(container.create(component_counter{std::make_shared<int>(42)}));
container.apply<component_counter>([&](entity_handle const &, component_counter const & component){
expect_equal(component.value.use_count(), 1);
});
std::shuffle(entities.begin(), entities.end(), rng);
for (int i = 0; i < entities.size() / 2; ++i)
container.destroy(entities[i]);
container.apply<component_counter>([&](entity_handle const &, component_counter const & component){
expect_equal(component.value.use_count(), 1);
});
}

147
libs/ecs/tests/entity.cpp Normal file
View file

@ -0,0 +1,147 @@
#include <psemek/test/test.hpp>
#include <psemek/ecs/entity_container.hpp>
#include <psemek/random/generator.hpp>
#include <psemek/random/uniform.hpp>
using namespace psemek;
using namespace psemek::ecs;
namespace
{
struct component_1
{
int value;
static constexpr util::uuid uuid()
{
return {1, 0};
}
};
struct component_2
{
int value;
static constexpr util::uuid uuid()
{
return {2, 0};
}
};
}
test_case(ecs_entity_empty_single)
{
entity_container container;
auto h0 = container.create();
expect(container.alive(h0));
container.destroy(h0);
expect(!container.alive(h0));
}
test_case(ecs_entity_empty_multiple)
{
entity_container container;
auto h0 = container.create();
expect(container.alive(h0));
auto h1 = container.create();
expect(container.alive(h1));
auto h2 = container.create();
expect(container.alive(h2));
container.destroy(h1);
expect(!container.alive(h1));
container.destroy(h0);
expect(!container.alive(h0));
container.destroy(h2);
expect(!container.alive(h2));
}
test_case(ecs_entity_components_single)
{
entity_container container;
auto h0 = container.create(component_1{10}, component_2{20});
expect(container.alive(h0));
expect_equal(container.get(h0).get<component_1>().value, 10);
expect_equal(container.get(h0).get<component_2>().value, 20);
container.destroy(h0);
expect(!container.alive(h0));
h0 = container.create(component_2{200}, component_1{100});
expect(container.alive(h0));
expect_equal(container.get(h0).get<component_1>().value, 100);
expect_equal(container.get(h0).get<component_2>().value, 200);
}
test_case(ecs_entity_components_multiple)
{
entity_container container;
auto h0 = container.create(component_1{10}, component_2{20});
expect(container.alive(h0));
expect_equal(container.get(h0).get<component_1>().value, 10);
expect_equal(container.get(h0).get<component_2>().value, 20);
auto h1 = container.create(component_1{30}, component_2{40});
expect(container.alive(h1));
expect_equal(container.get(h1).get<component_1>().value, 30);
expect_equal(container.get(h1).get<component_2>().value, 40);
auto h2 = container.create(component_1{50}, component_2{60});
expect(container.alive(h2));
expect_equal(container.get(h2).get<component_1>().value, 50);
expect_equal(container.get(h2).get<component_2>().value, 60);
container.destroy(h0);
expect(!container.alive(h0));
container.destroy(h2);
expect(!container.alive(h2));
container.destroy(h1);
expect(!container.alive(h1));
}
test_case(ecs_entity_random)
{
random::generator rng;
entity_container container;
std::vector<std::pair<entity_handle, int>> entities;
for (int i = 0; i < 1024 * 1024; ++i)
{
int value = random::uniform(rng, -1024, 1024);
entity_handle handle;
if (random::uniform<bool>(rng))
handle = container.create(component_1{value});
else
handle = container.create(component_1{value}, component_2{-value});
entities.push_back({handle, value});
}
std::shuffle(entities.begin(), entities.end(), rng);
while (!entities.empty())
{
auto entity = entities.back();
entities.pop_back();
expect(container.alive(entity.first));
expect_equal(container.get(entity.first).get<component_1>().value, entity.second);
container.destroy(entity.first);
expect(!container.alive(entity.first));
}
}