Skip to content

Commit 32fd1dc

Browse files
authored
[Core]: Implement entities manager (#53)
- Implement entities central manager with type-indexed component_pool storage - Refactor component_pool to inherit basic_component_pool abstract base for type erasure - Add erase_at(dense_index) to sparse_set for index-based removal (eliminates double lookup in remove()) - Change component_pool::get() to return T& with assert (fail-fast over std::optional) - Remove entity::valid() (liveness check belongs to entities::valid()) - Add EntitiesTests suite covering lifecycle, components, for_each, and bulk performance - Consolidate integration tests into entities_tests.cpp, remove redundant test files
1 parent cfa4b5c commit 32fd1dc

13 files changed

Lines changed: 687 additions & 391 deletions

File tree

cmake/gamecoe_config.hpp.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
// gamecoe toolkit
3838
#define GAMECOE_USE_LOGCOE @GAMECOE_USE_LOGCOE@
3939
#define GAMECOE_USE_SOUNDCOE @GAMECOE_USE_SOUNDCOE@
40+
#define GAMECOE_USE_TESTCOE @GAMECOE_USE_TESTCOE@
4041

4142
#if GAMECOE_USE_SOUNDCOE
4243
#include <soundcoe_config.hpp>

include/gamecoe/entity/component_pool.hpp

Lines changed: 56 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,100 +4,113 @@
44
#include <gamecoe/entity/entity.hpp>
55
#include <gamecoe/entity/sparse_set.hpp>
66
#include <vector>
7-
#include <optional>
8-
#include <functional>
97
#include <cassert>
108

119
namespace gamecoe
1210
{
11+
class basic_component_pool
12+
{
13+
protected:
14+
sparse_set m_entities;
15+
16+
private:
17+
virtual void do_reserve(std::size_t capacity) = 0;
18+
19+
public:
20+
virtual ~basic_component_pool() = default;
21+
virtual void remove(entity e) = 0;
22+
virtual void clear() = 0;
23+
24+
bool contains(entity e) const noexcept { return m_entities.contains(e); }
25+
std::size_t size() const noexcept { return m_entities.size(); }
26+
bool empty() const noexcept { return m_entities.empty(); }
27+
void reserve(std::size_t capacity) { m_entities.reserve(capacity); do_reserve(capacity); }
28+
};
29+
1330
template <typename T>
14-
class component_pool : private sparse_set
31+
class component_pool : public basic_component_pool
1532
{
1633
std::vector<T> m_components;
1734

18-
public:
19-
using sparse_set::contains;
20-
using sparse_set::size;
21-
using sparse_set::empty;
35+
void do_reserve(std::size_t capacity) override
36+
{
37+
m_components.reserve(capacity);
38+
}
2239

40+
public:
2341
component_pool() noexcept = default;
2442
component_pool(const component_pool&) = delete;
2543
component_pool& operator=(const component_pool&) = delete;
2644
component_pool(component_pool&&) noexcept = default;
2745
component_pool& operator=(component_pool&&) noexcept = default;
2846

29-
~component_pool() = default;
47+
~component_pool() override = default;
3048

31-
void reserve(std::size_t capacity)
49+
void remove(entity e) override
3250
{
33-
sparse_set::reserve(capacity);
34-
m_components.reserve(capacity);
51+
auto index = m_entities.index(e);
52+
if(!index) return;
53+
54+
std::uint32_t i = index.value();
55+
m_entities.erase_at(i);
56+
57+
if (i != m_components.size() - 1)
58+
m_components[i] = std::move(m_components.back());
59+
60+
m_components.pop_back();
61+
}
62+
63+
void clear() override
64+
{
65+
m_entities.clear();
66+
m_components.clear();
3567
}
3668

3769
template <typename... Args>
38-
T& add(const entity &e, Args&&... args)
70+
T& add(entity e, Args&&... args)
3971
{
4072
if(contains(e))
4173
{
4274
assert(false && "component_pool::add(): entity already has this component");
43-
return m_components[sparse_set::index(e).value()];
75+
return m_components[m_entities.index(e).value()];
4476
}
4577

46-
sparse_set::insert(e);
78+
m_entities.insert(e);
4779
m_components.emplace_back(std::forward<Args>(args)...);
4880

4981
return m_components.back();
5082
}
5183

52-
void remove(const entity &e)
53-
{
54-
auto index = sparse_set::index(e);
55-
if(!index) return;
56-
57-
sparse_set::erase(e);
58-
59-
if (index.value() < m_components.size() - 1)
60-
m_components[index.value()] = std::move(m_components.back());
61-
62-
m_components.pop_back();
63-
}
64-
65-
std::optional<std::reference_wrapper<T>> get(const entity &e)
84+
T& get(entity e)
6685
{
67-
auto index = sparse_set::index(e);
68-
if(!index) return std::nullopt;
86+
auto index = m_entities.index(e);
87+
assert(index && "component_pool::get(): entity does not exist in the pool");
6988

70-
return std::ref(m_components[index.value()]);
89+
return m_components[index.value()];
7190
}
7291

73-
std::optional<std::reference_wrapper<const T>> get(const entity &e) const
92+
const T& get(entity e) const
7493
{
75-
auto index = sparse_set::index(e);
76-
if(!index) return std::nullopt;
94+
auto index = m_entities.index(e);
95+
assert(index && "component_pool::get(): entity does not exist in the pool");
7796

78-
return std::cref(m_components[index.value()]);
97+
return m_components[index.value()];
7998
}
8099

81100
template<typename Func>
82101
void for_each(Func &&func)
83102
{
84103
auto size = m_components.size();
85104
for(std::size_t i = 0; i < size; ++i)
86-
func(sparse_set::get_entity_at_index(i), m_components[i]);
105+
func(m_entities.get_entity_at_index(i), m_components[i]);
87106
}
88107

89108
template<typename Func>
90109
void for_each(Func &&func) const
91110
{
92111
auto size = m_components.size();
93112
for(std::size_t i = 0; i < size; ++i)
94-
func(sparse_set::get_entity_at_index(i), m_components[i]);
95-
}
96-
97-
void clear()
98-
{
99-
sparse_set::clear();
100-
m_components.clear();
113+
func(m_entities.get_entity_at_index(i), m_components[i]);
101114
}
102115

103116
// Iterators invalidated by add/remove (dense array reallocation)
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#pragma once
2+
3+
#include <cstddef>
4+
#include <gamecoe/entity/entity.hpp>
5+
#include <gamecoe/entity/component_pool.hpp>
6+
#include <utility>
7+
#include <vector>
8+
#include <memory>
9+
#include <cstdint>
10+
#include <cassert>
11+
12+
namespace gamecoe
13+
{
14+
class entities
15+
{
16+
static std::uint32_t s_component_id;
17+
18+
std::vector<std::unique_ptr<basic_component_pool>> m_pools;
19+
std::vector<std::uint32_t> m_recycle_ids;
20+
std::vector<std::uint16_t> m_generations;
21+
22+
std::uint32_t m_current_entity_id{0};
23+
24+
// Returns static and unique id for component T
25+
template <typename T>
26+
static std::uint32_t component_id()
27+
{
28+
static std::uint32_t s_componentT_id = s_component_id++;
29+
return s_componentT_id;
30+
}
31+
32+
// Returns a pointer to the realtime type component_pool<T> of the T component, creates a new pool if not exists
33+
template <typename T>
34+
component_pool<T>* get_pool()
35+
{
36+
std::uint32_t comp_id = component_id<T>();
37+
38+
if (comp_id >= m_pools.size()) m_pools.resize(comp_id + 1);
39+
if (!m_pools[comp_id]) m_pools[comp_id] = std::make_unique<component_pool<T>>();
40+
41+
return static_cast<component_pool<T>*>(m_pools[comp_id].get());
42+
}
43+
44+
public:
45+
entities() = default;
46+
entities(const entities&) = delete;
47+
entities(entities&&) = delete;
48+
entities &operator=(const entities&) = delete;
49+
entities &operator=(entities&&) = delete;
50+
51+
~entities() = default;
52+
53+
// Creates an entity (may be recycled id)
54+
entity create();
55+
56+
// Destroy an entity
57+
void destroy(entity e);
58+
59+
// Destroys all entities
60+
void clear();
61+
62+
// Returns if the entity handle given is valid
63+
bool valid(entity e) const;
64+
65+
// Reserves capacity for a number of entities
66+
void reserve(std::size_t capacity);
67+
68+
// Returns the number of alive entities
69+
std::size_t size() const;
70+
71+
// Emplaces a new T component to entity e
72+
template <typename T, typename... Args>
73+
T& add_component(entity e, Args&&... args)
74+
{
75+
assert(valid(e) && "entities::add_component(): entity is not valid");
76+
77+
auto pool = get_pool<T>();
78+
return pool->add(e, std::forward<Args>(args)...);
79+
}
80+
81+
// Returns if entity e has a component T
82+
template <typename T>
83+
bool has_component(entity e) const
84+
{
85+
if (!valid(e)) return false;
86+
87+
std::uint32_t comp_id = component_id<T>();
88+
if (comp_id >= m_pools.size() || !m_pools[comp_id]) return false;
89+
90+
return m_pools[comp_id]->contains(e);
91+
}
92+
93+
// Removes component T from entity e
94+
template <typename T>
95+
void remove_component(entity e)
96+
{
97+
if (!has_component<T>(e)) return;
98+
99+
m_pools[component_id<T>()]->remove(e);
100+
}
101+
102+
// Returns a pointer to component T of entity e (`nullptr` if entity doesn't have T component),
103+
// also note that the pointer may invalidated by any `add_component` call
104+
template <typename T>
105+
T* get_component(entity e)
106+
{
107+
if (!has_component<T>(e)) return nullptr;
108+
109+
auto pool = static_cast<component_pool<T>*>(m_pools[component_id<T>()].get());
110+
return &(pool->get(e));
111+
}
112+
113+
// Returns a pointer to component T of entity e (`nullptr` if entity doesn't have T component),
114+
// also note that the pointer may invalidated by any `add_component` call
115+
template <typename T>
116+
const T* get_component(entity e) const
117+
{
118+
if (!has_component<T>(e)) return nullptr;
119+
120+
auto pool = static_cast<const component_pool<T>*>(m_pools[component_id<T>()].get());
121+
return &(pool->get(e));
122+
}
123+
124+
// Iterates over all entities and their components and run func() on each of them
125+
template<typename T, typename Func>
126+
void for_each(Func &&func)
127+
{
128+
std::uint32_t comp_id = component_id<T>();
129+
if (comp_id >= m_pools.size() || !m_pools[comp_id]) return;
130+
131+
auto pool = static_cast<component_pool<T>*>(m_pools[comp_id].get());
132+
pool->for_each(std::forward<Func>(func));
133+
}
134+
135+
// Iterates over all entities and their components and run func() on each of them
136+
template<typename T, typename Func>
137+
void for_each(Func &&func) const
138+
{
139+
std::uint32_t comp_id = component_id<T>();
140+
if (comp_id >= m_pools.size() || !m_pools[comp_id]) return;
141+
142+
auto pool = static_cast<const component_pool<T>*>(m_pools[comp_id].get());
143+
pool->for_each(std::forward<Func>(func));
144+
}
145+
};
146+
} // namespace gamecoe

include/gamecoe/entity/entity.hpp

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ namespace gamecoe
1818

1919
static constexpr entity invalid() noexcept { return entity{std::numeric_limits<std::uint32_t>::max()}; }
2020

21+
auto operator<=>(const entity &other) const noexcept = default;
22+
bool operator==(const entity &other) const noexcept = default;
23+
24+
std::uint32_t id() const noexcept { return m_handle >> GEN_BITS; }
25+
std::uint16_t generation() const noexcept { return m_handle & GEN_MASK; }
26+
2127
static entity create(std::uint32_t id, std::uint16_t generation)
2228
{
2329
assert(id <= MAX_ENTITIES && "entity::create(): entity id exceeds maximum");
@@ -27,14 +33,6 @@ namespace gamecoe
2733
// [ 20-bit id ][12-bit generation]
2834
return entity((id << GEN_BITS) | generation);
2935
}
30-
31-
auto operator<=>(const entity &other) const noexcept = default;
32-
bool operator==(const entity &other) const noexcept = default;
33-
34-
bool valid() const noexcept { return *this != invalid(); }
35-
36-
std::uint32_t id() const noexcept { return m_handle >> GEN_BITS; }
37-
std::uint16_t generation() const noexcept { return m_handle & GEN_MASK; }
3836

3937
private:
4038
std::uint32_t m_handle;

0 commit comments

Comments
 (0)