ECS library wip & tests
This commit is contained in:
parent
c6805dea21
commit
e0e0df8128
10 changed files with 441 additions and 66 deletions
|
|
@ -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_link_libraries(psemek-ecs PUBLIC psemek-util)
|
||||
|
||||
#psemek_glob_tests(psemek-ecs tests)
|
||||
psemek_glob_tests(psemek-ecs tests)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ namespace psemek::ecs::detail
|
|||
std::size_t row_count;
|
||||
entity_id const * entity_id_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)
|
||||
: row_count(entity_ids.size())
|
||||
|
|
|
|||
|
|
@ -8,6 +8,26 @@
|
|||
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
|
||||
{
|
||||
template <typename ... UUIDS>
|
||||
|
|
|
|||
|
|
@ -67,7 +67,6 @@ namespace psemek::ecs::detail
|
|||
table_impl(util::span<util::uuid const> component_uuids);
|
||||
|
||||
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 pop_row() override;
|
||||
|
||||
|
|
@ -108,29 +107,6 @@ namespace psemek::ecs::detail
|
|||
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>
|
||||
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;
|
||||
(reallocate_impl.template operator()<Components>(component_pointers_[i++].data), ...);
|
||||
|
||||
capacity_ = new_capacity;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,36 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/ecs/detail/table.hpp>
|
||||
#include <psemek/util/assert.hpp>
|
||||
|
||||
namespace psemek::ecs
|
||||
{
|
||||
|
||||
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:
|
||||
|
||||
detail::table * table_;
|
||||
std::uint32_t row_;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,33 +12,22 @@ namespace psemek::ecs
|
|||
|
||||
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>
|
||||
entity_handle create(Components && ... components)
|
||||
{
|
||||
util::uuid component_uuids[] { components.uuid()... };
|
||||
detail::component_mask mask = component_index_.make_component_mask(util::span<util::uuid const>(component_uuids));
|
||||
detail::component_uuid_holder<Components...> 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());
|
||||
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
|
||||
|
|
@ -63,16 +52,16 @@ namespace psemek::ecs
|
|||
template <typename ... Components, typename 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){
|
||||
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 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];
|
||||
|
||||
for (std::size_t i = 0; i < apply_helper.size(); ++i)
|
||||
|
|
@ -84,21 +73,10 @@ namespace psemek::ecs
|
|||
}, mask);
|
||||
}
|
||||
|
||||
template <typename Component>
|
||||
Component & get(entity_handle const & entity)
|
||||
entity_accessor get(entity_handle const & entity)
|
||||
{
|
||||
util::uuid const uuid = Component::uuid();
|
||||
|
||||
auto const & data = entity_list_.get_entities()[entity.id];
|
||||
|
||||
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();
|
||||
return {data.table, data.row};
|
||||
}
|
||||
|
||||
private:
|
||||
|
|
|
|||
|
|
@ -25,10 +25,8 @@ namespace psemek::ecs::detail
|
|||
|
||||
void entity_list::allocate_ids()
|
||||
{
|
||||
static constexpr std::size_t batch_size = 1024;
|
||||
|
||||
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;)
|
||||
free_ids_.push_back(id);
|
||||
}
|
||||
|
|
|
|||
113
libs/ecs/tests/apply.cpp
Normal file
113
libs/ecs/tests/apply.cpp
Normal 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);
|
||||
}
|
||||
120
libs/ecs/tests/component.cpp
Normal file
120
libs/ecs/tests/component.cpp
Normal 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
147
libs/ecs/tests/entity.cpp
Normal 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));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue