ECS library wip

This commit is contained in:
Nikita Lisitsa 2023-08-22 14:53:03 +03:00
parent 190fd5e51e
commit 63008d62ff
12 changed files with 465 additions and 217 deletions

View file

@ -0,0 +1,51 @@
#pragma once
#include <psemek/ecs/detail/table.hpp>
#include <psemek/ecs/detail/entity_list.hpp>
namespace psemek::ecs::detail
{
template <typename ... Components>
struct static_apply_helper
{
std::size_t row_count;
entity_id const * entity_id_pointer;
entity_data const * entity_data_pointer;
table::component_pointer pointers[sizeof...(Components)];
static_apply_helper(util::span<entity_id const> entity_ids, util::span<entity_data const> entities)
: row_count(entity_ids.size())
, entity_id_pointer(entity_ids.data())
, entity_data_pointer(entities.data())
{}
template <typename Function>
void apply(Function && function)
{
apply_impl(function, std::make_index_sequence<sizeof...(Components)>{});
}
template <typename Function, std::size_t ... I>
void apply_impl(Function && function, std::index_sequence<I...>)
{
auto id = *entity_id_pointer;
auto epoch = entity_data_pointer[id].epoch;
function(entity_handle{id, epoch}, *reinterpret_cast<Components *>(pointers[I].data) ...);
}
std::size_t size() const
{
return row_count;
}
void advance()
{
++entity_id_pointer;
std::size_t i = 0;
((pointers[i++].data += stride<Components>()), ...);
}
};
}

View file

@ -1,11 +1,11 @@
#pragma once
#include <psemek/ecs/component_mask.hpp>
#include <psemek/ecs/detail/component_mask.hpp>
#include <psemek/util/uuid.hpp>
#include <psemek/util/span.hpp>
#include <psemek/util/unique_sequential_storage.hpp>
namespace psemek::ecs
namespace psemek::ecs::detail
{
struct component_index

View file

@ -2,7 +2,7 @@
#include <psemek/util/dynamic_bitset.hpp>
namespace psemek::ecs
namespace psemek::ecs::detail
{
using component_mask = util::dynamic_bitset;

View file

@ -0,0 +1,42 @@
#pragma once
#include <psemek/ecs/entity_handle.hpp>
#include <psemek/util/span.hpp>
#include <vector>
namespace psemek::ecs::detail
{
struct table;
struct entity_data
{
struct table * table = nullptr;
std::uint32_t row = 0;
entity_epoch epoch = 0;
};
struct entity_list
{
entity_id create(table * table, std::uint32_t row);
void destroy(entity_id id);
util::span<entity_data const> get_entities() const
{
return entities_;
}
util::span<entity_data> get_entities()
{
return entities_;
}
private:
std::vector<entity_data> entities_;
std::vector<entity_id> free_ids_;
void allocate_ids();
};
}

View file

@ -0,0 +1,17 @@
#pragma once
#include <type_traits>
namespace psemek::ecs::detail
{
template <typename T>
constexpr bool is_empty_v = std::is_empty_v<T>;
template <typename T>
constexpr std::size_t stride()
{
return is_empty_v<T> ? 0 : sizeof(T);
}
}

View file

@ -0,0 +1,203 @@
#pragma once
#include <psemek/ecs/entity_handle.hpp>
#include <psemek/ecs/detail/stride.hpp>
#include <psemek/util/uuid.hpp>
#include <psemek/util/span.hpp>
#include <psemek/util/assert.hpp>
#include <memory>
#include <type_traits>
#include <vector>
#include <array>
namespace psemek::ecs::detail
{
struct table
{
struct component_pointer
{
std::uint8_t * data = nullptr;
};
util::span<util::uuid const> get_component_uuids() const
{
return component_uuids_;
}
util::span<component_pointer const> get_component_pointers() const
{
return component_pointers_;
}
std::size_t component_count() const
{
return component_uuids_.size();
}
std::size_t row_count() const
{
return row_count_;
}
virtual std::size_t push_row(entity_id id) = 0;
virtual void swap_rows(std::size_t row1, std::size_t row2) = 0;
virtual void pop_row() = 0;
util::span<entity_id const> get_entity_ids() const
{
return entity_ids_;
}
virtual ~table() = default;
protected:
std::vector<util::uuid> component_uuids_;
std::vector<component_pointer> component_pointers_;
std::size_t row_count_ = 0;
std::vector<entity_id> entity_ids_;
};
template <typename ... Components>
struct table_impl
: table
{
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;
private:
std::size_t capacity_ = 0;
void reallocate();
};
template <typename ... Components>
table_impl<Components...>::table_impl(util::span<util::uuid const> component_uuids)
{
assert(sizeof...(Components) == component_uuids.size());
component_uuids_.assign(component_uuids.begin(), component_uuids.end());
component_pointers_.resize(sizeof...(Components));
}
template <typename ... Components>
std::size_t table_impl<Components...>::push_row(entity_id id)
{
if (row_count_ == capacity_)
reallocate();
[[maybe_unused]] auto push_row_impl = [&]<typename Component>(std::uint8_t * data)
{
if constexpr (!detail::is_empty_v<Component>)
{
new (reinterpret_cast<Component *>(data) + row_count_) Component{};
}
};
std::size_t i = 0;
(push_row_impl.template operator()<Components>(component_pointers_[i++].data), ...);
++row_count_;
entity_ids_.push_back(id);
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)
{
[[maybe_unused]] auto swap_rows_impl = [&]<typename Component>(std::uint8_t * data)
{
if constexpr (!detail::is_empty_v<Component>)
{
auto cdata = reinterpret_cast<Component *>(data);
std::iter_swap(cdata + row1, cdata + row2);
}
};
std::size_t i = 0;
(swap_rows_impl.template operator()<Components>(component_pointers_[i++].data), ...);
std::swap(entity_ids_[row1], entity_ids_[row2]);
}
template <typename ... Components>
void table_impl<Components...>::pop_row()
{
--row_count_;
[[maybe_unused]] auto pop_row_impl = [&]<typename Component>(std::uint8_t * data)
{
if constexpr (!detail::is_empty_v<Component>)
{
(reinterpret_cast<Component *>(data) + row_count_)->~Component();
}
};
std::size_t i = 0;
(pop_row_impl.template operator()<Components>(component_pointers_[i++].data), ...);
entity_ids_.pop_back();
}
template <typename ... Components>
void table_impl<Components...>::reallocate()
{
std::size_t const new_capacity = capacity_ == 0 ? 64 : capacity_ * 2;
[[maybe_unused]] auto reallocate_impl = [&]<typename Component>(std::uint8_t * & data)
{
if constexpr (detail::is_empty_v<Component>)
{
if (!data)
data = reinterpret_cast<std::uint8_t *>(new Component[1]);
}
else
{
auto new_data = new (std::align_val_t(alignof(Component))) std::uint8_t[new_capacity * sizeof(Component)];
auto old_begin = reinterpret_cast<Component *>(data);
auto old_end = old_begin + row_count_;
auto new_begin = reinterpret_cast<Component *>(new_data);
for (; old_begin != old_end; ++old_begin, ++new_begin)
{
new (new_begin) Component{std::move(*old_begin)};
old_begin->~Component();
}
delete [] data;
data = new_data;
}
};
std::size_t i = 0;
(reallocate_impl.template operator()<Components>(component_pointers_[i++].data), ...);
}
}

View file

@ -1,11 +1,11 @@
#pragma once
#include <psemek/ecs/component_mask.hpp>
#include <psemek/ecs/table.hpp>
#include <psemek/ecs/detail/component_mask.hpp>
#include <psemek/ecs/detail/table.hpp>
#include <vector>
namespace psemek::ecs
namespace psemek::ecs::detail
{
// TODO: store tables in a bitmask trie balanced by subtree size
@ -13,7 +13,7 @@ namespace psemek::ecs
struct table_container
{
template <typename ... Components>
table_impl<Components...> & insert(component_mask const & mask);
table_impl<Components...> & insert(component_mask const & mask, util::span<util::uuid const> component_uuids);
template <typename Function>
void apply(Function && function, component_mask const & mask);
@ -23,11 +23,11 @@ namespace psemek::ecs
};
template <typename ... Components>
table_impl<Components...> & table_container::insert(component_mask const & mask)
table_impl<Components...> & table_container::insert(component_mask const & mask, util::span<util::uuid const> component_uuids)
{
auto & result = tables_[mask];
if (!result)
result = std::make_unique<table_impl<Components...>>();
result = std::make_unique<table_impl<Components...>>(component_uuids);
return *static_cast<table_impl<Components...> *>(result.get());
}

View file

@ -0,0 +1,16 @@
#pragma once
#include <psemek/ecs/detail/table.hpp>
namespace psemek::ecs
{
struct entity_accessor
{
private:
};
}

View file

@ -1,9 +1,10 @@
#pragma once
#include <psemek/ecs/component_index.hpp>
#include <psemek/ecs/component_mask.hpp>
#include <psemek/ecs/entity_handle.hpp>
#include <psemek/ecs/table_container.hpp>
#include <psemek/ecs/detail/component_index.hpp>
#include <psemek/ecs/detail/entity_list.hpp>
#include <psemek/ecs/detail/table_container.hpp>
#include <psemek/ecs/detail/apply_helper.hpp>
#include <psemek/ecs/entity_accessor.hpp>
#include <psemek/util/span.hpp>
namespace psemek::ecs
@ -11,43 +12,99 @@ 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)
{
component_mask mask = component_index_.make_component_mask(components.uuid()...);
// TODO
return {};
util::uuid component_uuids[] { components.uuid()... };
detail::component_mask mask = component_index_.make_component_mask(util::span<util::uuid const>(component_uuids));
auto & table = table_container_.insert<Components...>(mask, component_uuids);
auto id = entity_list_.create(&table, table.row_count());
table.push_row_with_components(id, std::move(components)...);
return {id, entity_list_.get_entities()[id].epoch};
}
bool alive(entity_handle const & entity) const;
void destroy(entity_handle const & entity);
template <typename Function>
void apply(Function && function, util::span<util::uuid const> component_uuids)
bool alive(entity_handle const & entity) const
{
component_mask mask = component_index_.make_component_mask(component_uuids);
table_container_.apply([&](table & table){
// TODO: extract specific component pointers and apply the function
// to each element, using stride to advance pointers
// TODO: maybe store UUIDS or component indices in the table to simplify
// extracting the component pointers
}, mask);
return entity_list_.get_entities()[entity.id].epoch == entity.epoch;
}
void destroy(entity_handle const & entity)
{
// Swap with the last row in that table
auto entities = entity_list_.get_entities();
auto & data = entities[entity.id];
auto table_entity_ids = data.table->get_entity_ids();
data.table->swap_rows(data.row, table_entity_ids.size() - 1);
data.table->pop_row();
auto swap_id = table_entity_ids[data.row];
auto & swap_data = entities[swap_id];
swap_data.row = data.row;
entity_list_.destroy(entity.id);
}
template <typename ... Components, typename Function>
void apply(Function && function)
{
util::uuid component_uuids[] { Components::uuid() ... };
// TODO: call function, casting the raw uint8_t component pointers
// to actual component types
util::uuid const component_uuids[] { Components::uuid() ... };
detail::component_mask mask = component_index_.make_component_mask(util::span<util::uuid const>(component_uuids));
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])
apply_helper.pointers[i++] = table.get_component_pointers()[j];
for (std::size_t i = 0; i < apply_helper.size(); ++i)
{
apply_helper.apply(function);
apply_helper.advance();
}
}, mask);
}
template <typename Component>
Component & 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();
}
private:
mutable component_index component_index_;
table_container table_container_;
// TODO: store entity epochs
// TODO: store entity id -> table * mapping
detail::entity_list entity_list_;
mutable detail::component_index component_index_;
detail::table_container table_container_;
};
}

View file

@ -1,6 +1,7 @@
#pragma once
#include <cstdint>
#include <iostream>
namespace psemek::ecs
{
@ -14,4 +15,10 @@ namespace psemek::ecs
entity_epoch epoch;
};
inline std::ostream & operator << (std::ostream & out, entity_handle const & handle)
{
out << '(' << handle.id << ',' << handle.epoch << ')';
return out;
}
}

View file

@ -1,181 +0,0 @@
#pragma once
#include <psemek/ecs/entity_handle.hpp>
#include <memory>
#include <type_traits>
#include <vector>
namespace psemek::ecs
{
struct table
{
struct component_pointer
{
std::uint8_t * data = nullptr;
std::size_t stride = 0;
};
component_pointer get_component(std::size_t i)
{
return component_pointers_[i];
}
std::size_t component_count() const
{
return component_count_;
}
std::size_t size() const
{
return size_;
}
virtual std::size_t push_row(entity_id id) = 0;
virtual void swap_rows(std::size_t row1, std::size_t row2) = 0;
virtual void pop_row() = 0;
entity_id row_entity_id(std::size_t row) const
{
return entity_ids_[row];
}
virtual ~table() = default;
protected:
component_pointer const * component_pointers_ = nullptr;
std::size_t component_count_ = 0;
std::size_t size_ = 0;
std::vector<entity_id> entity_ids_;
};
template <typename ... Components>
struct table_impl
: table
{
table_impl();
std::size_t push_row(entity_id id) override;
void swap_rows(std::size_t row1, std::size_t row2) override;
void pop_row() override;
private:
component_pointer component_pointers_storage_[sizeof...(Components)];
std::size_t capacity_ = 0;
void init_components_stride();
void reallocate();
};
template <typename ... Components>
table_impl<Components...>::table_impl()
{
component_pointers_ = component_pointers_storage_;
component_count_ = sizeof...(Components);
init_components_stride();
}
template <typename ... Components>
std::size_t table_impl<Components...>::push_row(entity_id id)
{
if (size_ == capacity_)
reallocate();
auto push_row_impl = [&]<typename Component>(std::uint8_t * data)
{
if constexpr (!std::is_empty_v<Component>)
{
new (reinterpret_cast<Component *>(data) + size_) Component{};
}
};
std::size_t i = 0;
(push_row_impl.template operator()<Components>(component_pointers_storage_[i++].data), ...);
++size_;
entity_ids_.push_back(id);
return size_ - 1;
}
template <typename ... Components>
void table_impl<Components...>::swap_rows(std::size_t row1, std::size_t row2)
{
auto swap_rows_impl = [&]<typename Component>(std::uint8_t * data)
{
if constexpr (!std::is_empty_v<Component>)
{
auto cdata = reinterpret_cast<Component *>(data);
std::iter_swap(cdata + row1, cdata + row2);
}
};
std::size_t i = 0;
(swap_rows_impl.template operator()<Components>(component_pointers_storage_[i++].data), ...);
std::swap(entity_ids_[row1], entity_ids_[row2]);
}
template <typename ... Components>
void table_impl<Components...>::pop_row()
{
auto row = size_ - 1;
auto pop_row_impl = [&]<typename Component>(std::uint8_t * data)
{
if constexpr (!std::is_empty_v<Component>)
{
(reinterpret_cast<Component *>(data) + size_)->~Component();
}
};
std::size_t i = 0;
(pop_row_impl.template operator()<Components>(component_pointers_storage_[i++].data), ...);
entity_ids_.pop_back();
--size_;
}
template <typename ... Components>
void table_impl<Components...>::init_components_stride()
{
std::size_t i = 0;
((component_pointers_storage_[i].stride = std::is_empty_v<Components> ? 0 : sizeof(Components), ++i), ...);
}
template <typename ... Components>
void table_impl<Components...>::reallocate()
{
std::size_t const new_capacity = capacity_ == 0 ? 64 : capacity_ * 2;
auto reallocate_impl = [&]<typename Component>(std::uint8_t * & data)
{
if constexpr (std::is_empty_v<Component>)
{
if (!data)
data = reinterpret_cast<std::uint8_t *>(new Component[1]);
}
else
{
auto new_data = new std::uint8_t[new_capacity * sizeof(Component)];
auto old_begin = reinterpret_cast<Component *>(data);
auto old_end = old_begin + size_;
auto new_begin = reinterpret_cast<Component *>(new_data);
for (; old_begin != old_end; ++old_begin, ++new_begin)
{
new (new_begin) Component{std::move(*old_begin)};
old_begin->~Component();
}
delete [] data;
data = new_data;
}
};
std::size_t i = 0;
(reallocate_impl.template operator()<Components>(component_pointers_storage_[i++].data), ...);
}
}

View file

@ -0,0 +1,36 @@
#include <psemek/ecs/detail/entity_list.hpp>
namespace psemek::ecs::detail
{
entity_id entity_list::create(table * table, std::uint32_t row)
{
if (free_ids_.empty())
allocate_ids();
auto id = free_ids_.back();
free_ids_.pop_back();
entities_[id].table = table;
entities_[id].row = row;
return id;
}
void entity_list::destroy(entity_id id)
{
entities_[id].epoch += 1;
free_ids_.push_back(id);
}
void entity_list::allocate_ids()
{
static constexpr std::size_t batch_size = 1024;
auto old_size = entities_.size();
entities_.resize(entities_.size() + batch_size);
for (std::size_t id = entities_.size(); id --> old_size;)
free_ids_.push_back(id);
}
}