Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
manager.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <helios/assert.hpp>
4#include <helios/ecs/details/profile.hpp>
6
7#include <algorithm>
8#include <atomic>
9#include <concepts>
10#include <cstddef>
11#include <cstdint>
12#include <iterator>
13#include <ranges>
14#include <utility>
15#include <vector>
16
17namespace helios::ecs {
18
19/**
20 * @brief Entity manager responsible for entity creation, destruction, and
21 * validation.
22 * @details Manages entity lifecycle with generation counters to handle entity
23 * recycling safely.
24 * @note Partially thread-safe.
25 */
27public:
28 EntityManager() = default;
29 EntityManager(const EntityManager& other);
30 EntityManager(EntityManager&& other) noexcept;
31 ~EntityManager() = default;
32
34 EntityManager& operator=(EntityManager&& other) noexcept;
35
36 /**
37 * @brief Clears all entities.
38 * @details Destroys all entities and resets the manager state.
39 * @note Not thread-safe.
40 */
41 void Clear() noexcept;
42
43 /**
44 * @brief Creates reserved entities in the metadata.
45 * @details Processes all reserved entity IDs and creates their metadata.
46 * @note Not thread-safe.
47 */
48 void Flush() {
49 Flush([](Entity /*entity*/) {});
50 }
51
52 /**
53 * @brief Creates reserved entities and invokes callback for each flushed
54 * entity.
55 * @details Processes all reserved entity IDs, initializes their metadata,
56 * and invokes `callback` exactly once per newly flushed entity.
57 * @note Not thread-safe.
58 * @tparam F Callable type invocable with `Entity`
59 * @param callback Callback invoked for each newly flushed entity
60 */
61 template <typename F>
62 requires std::invocable<F&, Entity>
63 void Flush(const F& callback);
64
65 /**
66 * @brief Reserves space for entities to minimize allocations.
67 * @details Pre-allocates storage for the specified number of entities.
68 * @note Not thread-safe.
69 * @param count Number of entities to reserve space for
70 */
71 void Reserve(size_t count);
72
73 /**
74 * @brief Reserves an entity ID that can be used immediately.
75 * @details The actual entity creation is deferred until
76 * `Flush()` is called.
77 * @note Thread-safe.
78 * @return Reserved entity with valid index and generation
79 */
80 [[nodiscard]] Entity ReserveEntity();
81
82 /**
83 * @brief Creates a new entity.
84 * @details Reuses dead entity slots when available, otherwise creates new
85 * ones.
86 * @note Not thread-safe.
87 * @return Newly created entity with valid index and generation
88 */
89 [[nodiscard]] Entity Create();
90
91 /**
92 * @brief Creates multiple entities at once and outputs them via an output
93 * iterator.
94 * @details Batch creation is more efficient than individual calls.
95 * Entities are written to the provided output iterator, avoiding internal
96 * allocations.
97 * @note Not thread-safe.
98 * @tparam OutputIt Output iterator type that accepts Entity values
99 * @param count Number of entities to create
100 * @param out Output iterator to write created entities to
101 * @return Output iterator pointing past the last written entity
102 *
103 * @code
104 * std::vector<Entity> entities;
105 * entities.reserve(100);
106 * manager.Create(100, std::back_inserter(entities));
107 *
108 * // Or with pre-allocated array:
109 * std::array<Entity, 10> arr;
110 * manager.Create(10, arr.begin());
111 *
112 * // Or with span output:
113 * Entity buffer[50];
114 * manager.Create(50, std::begin(buffer));
115 * @endcode
116 */
117 template <typename OutputIt>
118 requires std::output_iterator<OutputIt, Entity>
119 OutputIt Create(size_t count, OutputIt&& out);
120
121 /**
122 * @brief Destroys an entity by incrementing its generation.
123 * @details Marks entity as dead and adds its index to the free list for
124 * reuse. Entities that do not exist or are already destroyed are ignored.
125 * @note Not thread-safe.
126 * @warning Triggers assertion if entity is invalid.
127 * @param entity Entity to destroy
128 */
129 void Destroy(Entity entity);
130
131 /**
132 * @brief Destroys an entity by incrementing its generation.
133 * @details Marks entity as dead and adds its index to the free list for
134 * reuse. Entities that do not exist or are already destroyed are ignored.
135 * @note Not thread-safe.
136 * @warning Triggers assertion if any entity is invalid.
137 * @tparam R Range type containing Entity elements
138 * @param entities Entities to destroy
139 */
140 template <std::ranges::range R>
141 requires std::same_as<std::ranges::range_value_t<R>, Entity>
142 void Destroy(const R& entities);
143
144 /**
145 * @brief Checks if an entity exists and is valid.
146 * @details Validates both the entity structure and its current generation.
147 * @note Thread-safe for read operations.
148 * @param entity Entity to validate
149 * @return True if entity exists and is valid, false otherwise
150 */
151 [[nodiscard]] bool Validate(Entity entity) const noexcept;
152
153 /**
154 * @brief Gets the current number of living entities.
155 * @details Returns count of entities that are currently alive.
156 * @note Thread-safe.
157 * @return Number of living entities
158 */
159 [[nodiscard]] size_t Count() const noexcept {
160 return entity_count_.load(std::memory_order_relaxed);
161 }
162
163 /**
164 * @brief Returns current generation value for a given index (or
165 * `kInvalidGeneration` if out of range).
166 * @warning Triggers assertion if index is invalid.
167 */
169 Entity::IndexType index) const noexcept;
170
171private:
172 [[nodiscard]] Entity CreateEntityWithId(Entity::IndexType index,
173 Entity::GenerationType generation);
174
175 /// Generation counter for each entity index
176 std::vector<Entity::GenerationType> generations_;
177 std::vector<Entity::IndexType> free_indices_; ///< Recycled entity indices
178 std::atomic<size_t> entity_count_{0}; ///< Number of living entities
179
180 /// Next available index (thread-safe)
181 std::atomic<Entity::IndexType> next_index_{0};
182
183 /// Cursor for free list (negative means reserved entities)
184 std::atomic<int64_t> free_cursor_{0};
185};
186
188 : generations_(other.generations_),
189 free_indices_(other.free_indices_),
190 entity_count_(other.entity_count_.load(std::memory_order_relaxed)),
191 next_index_(other.next_index_.load(std::memory_order_relaxed)),
192 free_cursor_(other.free_cursor_.load(std::memory_order_relaxed)) {}
193
195 : generations_(std::move(other.generations_)),
196 free_indices_(std::move(other.free_indices_)),
197 entity_count_(other.entity_count_.load(std::memory_order_relaxed)),
198 next_index_(other.next_index_.load(std::memory_order_relaxed)),
199 free_cursor_(other.free_cursor_.load(std::memory_order_relaxed)) {
200 other.entity_count_.store(0, std::memory_order_relaxed);
201 other.next_index_.store(0, std::memory_order_relaxed);
202 other.free_cursor_.store(0, std::memory_order_relaxed);
203}
204
206 if (this == &other) [[unlikely]] {
207 return *this;
208 }
209
210 generations_ = other.generations_;
211 free_indices_ = other.free_indices_;
212 entity_count_.store(other.entity_count_.load(std::memory_order_relaxed),
213 std::memory_order_relaxed);
214 next_index_.store(other.next_index_.load(std::memory_order_relaxed),
215 std::memory_order_relaxed);
216 free_cursor_.store(other.free_cursor_.load(std::memory_order_relaxed),
217 std::memory_order_relaxed);
218
219 return *this;
220}
221
223 if (this == &other) [[unlikely]] {
224 return *this;
225 }
226
227 generations_ = std::move(other.generations_);
228 free_indices_ = std::move(other.free_indices_);
229 entity_count_.store(other.entity_count_.load(std::memory_order_relaxed),
230 std::memory_order_relaxed);
231 next_index_.store(other.next_index_.load(std::memory_order_relaxed),
232 std::memory_order_relaxed);
233 free_cursor_.store(other.free_cursor_.load(std::memory_order_relaxed),
234 std::memory_order_relaxed);
235
236 other.entity_count_.store(0, std::memory_order_relaxed);
237 other.next_index_.store(0, std::memory_order_relaxed);
238 other.free_cursor_.store(0, std::memory_order_relaxed);
239
240 return *this;
241}
242
243inline void EntityManager::Clear() noexcept {
244 std::ranges::fill(generations_, Entity::kInvalidGeneration);
245 free_indices_.clear();
246 next_index_.store(0, std::memory_order_relaxed);
247 free_cursor_.store(0, std::memory_order_relaxed);
248 entity_count_.store(0, std::memory_order_relaxed);
249}
250
251template <typename F>
252 requires std::invocable<F&, Entity>
253inline void EntityManager::Flush(const F& callback) {
254 HELIOS_ECS_PROFILE_SCOPE_N("helios::ecs::EntityManager::Flush");
255
256 const auto current_next = next_index_.load(std::memory_order_relaxed);
257 if (current_next == 0) {
258 return;
259 }
260
261 // Ensure generations array covers all reserved indices.
262 if (current_next > generations_.size()) {
263 generations_.resize(current_next, Entity::kInvalidGeneration);
264 }
265
266 // Initialize any reserved-but-unflushed entries.
267 // Reserved entities get generation 1 (as returned by ReserveEntity).
268 // Entries that were already created via Create
269 // will already have a valid generation (!= kInvalidGeneration), so we skip
270 // them.
271 size_t new_entities_count = 0;
272 for (Entity::IndexType i = 0; i < current_next; ++i) {
273 if (generations_[i] == Entity::kInvalidGeneration) {
274 generations_[i] = 1;
275 ++new_entities_count;
276 callback(Entity{i, 1});
277 }
278 }
279
280 if (new_entities_count > 0) {
281 entity_count_.fetch_add(new_entities_count, std::memory_order_relaxed);
282 }
283
284 HELIOS_ECS_PROFILE_ZONE_VALUE(new_entities_count);
285}
286
287inline void EntityManager::Reserve(size_t count) {
288 if (count > generations_.size()) {
289 generations_.resize(count, Entity::kInvalidGeneration);
290 }
291 free_indices_.reserve(count);
292}
293
295 // Atomically reserve an index by incrementing the next available index.
296 // NOTE: Do NOT mutate metadata (e.g. `generations_` or `entity_count_`) here
297 // because this function is thread-safe and may be called concurrently. The
298 // actual metadata initialization for reserved indices is performed in
299 // `Flush()` which must be called from the main thread.
300 const Entity::IndexType index =
301 next_index_.fetch_add(1, std::memory_order_relaxed);
302
303 // Return a placeholder entity with generation 1.
304 return {index, 1};
305}
306
307inline void EntityManager::Destroy(Entity entity) {
308 HELIOS_ASSERT(entity.Valid(), "Entity '{}' is invalid!", entity);
309 if (!Validate(entity)) [[unlikely]] {
310 return;
311 }
312
313 const Entity::IndexType index = entity.Index();
314 ++generations_[index]; // Invalidate entity
315 free_indices_.push_back(index);
316
317 free_cursor_.store(static_cast<int64_t>(free_indices_.size()),
318 std::memory_order_relaxed);
319 entity_count_.fetch_sub(1, std::memory_order_relaxed);
320}
321
322template <std::ranges::range R>
323 requires std::same_as<std::ranges::range_value_t<R>, Entity>
324inline void EntityManager::Destroy(const R& entities) {
325 if constexpr (std::ranges::sized_range<R>) {
326 const size_t incoming = std::ranges::size(entities);
327 free_indices_.reserve(free_indices_.size() + incoming);
328 }
329
330 // Process each entity: validate, increment generation, collect index.
331 // We increment generation immediately so behaviour matches the single-entity
332 // Destroy (i.e. duplicates in the input will fail the second validation).
333 for (const auto& entity : entities) {
334 HELIOS_ASSERT(entity.Valid(), "Entity '{}' is invalid!", entity);
335 if (!Validate(entity)) [[unlikely]] {
336 // Skip entities already destroyed / wrong generation
337 continue;
338 }
339
340 const Entity::IndexType index = entity.Index();
341 ++generations_[index]; // Invalidate entity
342 free_indices_.push_back(index);
343 entity_count_.fetch_sub(1, std::memory_order_relaxed);
344 }
345}
346
347template <typename OutputIt>
348 requires std::output_iterator<OutputIt, Entity>
349inline OutputIt EntityManager::Create(size_t count, OutputIt&& out) {
350 if (count == 0) [[unlikely]] {
351 return out;
352 }
353
354 // Try to satisfy as many as possible from the free list first
355 size_t remaining = count;
356 int64_t cursor = free_cursor_.load(std::memory_order_relaxed);
357 // Use std::max with explicit signed type to avoid non-standard integer
358 // literal suffix
359 const size_t available_free =
360 static_cast<size_t>(std::max<int64_t>(int64_t{0}, cursor));
361 const size_t from_free_list = std::min(remaining, available_free);
362
363 if (from_free_list > 0) {
364 const int64_t new_cursor = cursor - static_cast<int64_t>(from_free_list);
365 if (free_cursor_.compare_exchange_strong(cursor, new_cursor,
366 std::memory_order_relaxed)) {
367 // Successfully claimed indices from free list
368 for (size_t i = 0; i < from_free_list; ++i) {
369 const size_t free_index = static_cast<size_t>(new_cursor) + i;
370 if (free_index >= free_indices_.size()) {
371 continue;
372 }
373
374 const Entity::IndexType index = free_indices_[free_index];
375 if (index >= generations_.size()) {
376 continue;
377 }
378
379 const Entity::GenerationType generation = generations_[index];
380 *out = CreateEntityWithId(index, generation);
381 ++out;
382 }
383 remaining -= from_free_list;
384 }
385 }
386
387 // Create new entities for remaining count
388 if (remaining > 0) {
389 const Entity::IndexType start_index = next_index_.fetch_add(
390 static_cast<Entity::IndexType>(remaining), std::memory_order_relaxed);
391 const Entity::IndexType end_index =
392 start_index + static_cast<Entity::IndexType>(remaining);
393
394 // Ensure generations array is large enough
395 if (end_index > generations_.size()) {
396 generations_.resize(end_index, Entity::kInvalidGeneration);
397 }
398
399 // Create entities with generation 1
400 for (Entity::IndexType index = start_index; index < end_index; ++index) {
401 generations_[index] = 1;
402 *out = CreateEntityWithId(index, 1);
403 ++out;
404 }
405 }
406
407 return out;
408}
409
410inline bool EntityManager::Validate(Entity entity) const noexcept {
411 if (!entity.Valid()) [[unlikely]] {
412 return false;
413 }
414
415 const Entity::IndexType index = entity.Index();
416 return index < generations_.size() &&
417 generations_[index] == entity.Generation() &&
418 generations_[index] != Entity::kInvalidGeneration;
419}
420
422 Entity::IndexType index) const noexcept {
423 HELIOS_ASSERT(index != Entity::kInvalidIndex, "Provided index is invalid!");
424 return index < generations_.size() ? generations_[index]
426}
427
429 // Reuse a free slot if available
430 const int64_t cursor = free_cursor_.load(std::memory_order_relaxed);
431 if (cursor > 0) {
432 const int64_t new_cursor = cursor - 1;
433 // Try to claim the top free slot
434 int64_t expected = cursor;
435 if (free_cursor_.compare_exchange_strong(expected, new_cursor,
436 std::memory_order_relaxed)) {
437 const Entity::IndexType index =
438 free_indices_[static_cast<size_t>(new_cursor)];
439 const Entity::GenerationType generation = generations_[index];
440 return CreateEntityWithId(index, generation);
441 }
442 }
443
444 // No free slot available — allocate a new index
445 const Entity::IndexType index =
446 next_index_.fetch_add(1, std::memory_order_relaxed);
447 return CreateEntityWithId(index, 1);
448}
449
450inline Entity EntityManager::CreateEntityWithId(
451 Entity::IndexType index, Entity::GenerationType generation) {
452 if (index >= generations_.size()) {
453 generations_.resize(index + 1, Entity::kInvalidGeneration);
454 }
455
456 generations_[index] = generation;
457 entity_count_.fetch_add(1, std::memory_order_relaxed);
458
459 return {index, generation};
460}
461
462} // namespace helios::ecs
#define HELIOS_ASSERT(condition,...)
Assertion macro that aborts execution in debug builds.
Definition assert.hpp:259
void Reserve(size_t count)
Reserves space for entities to minimize allocations.
Definition manager.hpp:287
void Clear() noexcept
Clears all entities.
Definition manager.hpp:243
void Flush()
Creates reserved entities in the metadata.
Definition manager.hpp:48
void Destroy(Entity entity)
Destroys an entity by incrementing its generation.
Definition manager.hpp:307
size_t Count() const noexcept
Gets the current number of living entities.
Definition manager.hpp:159
EntityManager & operator=(const EntityManager &other)
Definition manager.hpp:205
Entity ReserveEntity()
Reserves an entity ID that can be used immediately.
Definition manager.hpp:294
Entity Create()
Creates a new entity.
Definition manager.hpp:428
bool Validate(Entity entity) const noexcept
Checks if an entity exists and is valid.
Definition manager.hpp:410
Entity::GenerationType GetGeneration(Entity::IndexType index) const noexcept
Returns current generation value for a given index (or kInvalidGeneration if out of range).
Definition manager.hpp:421
Unique identifier for entities with generation counter to handle recycling.
Definition entity.hpp:22
constexpr bool Valid() const noexcept
Checks if the entity is valid.
Definition entity.hpp:64
uint32_t IndexType
Definition entity.hpp:24
uint32_t GenerationType
Definition entity.hpp:25
static constexpr GenerationType kInvalidGeneration
Definition entity.hpp:29
constexpr IndexType Index() const noexcept
Gets the index component of the entity.
Definition entity.hpp:82
static constexpr IndexType kInvalidIndex
Definition entity.hpp:27
STL namespace.