Helios Engine 0.1.0
A modular ECS based data-oriented C++23 game engine
 
Loading...
Searching...
No Matches
entities_manager.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <helios/core_pch.hpp>
4
8
9#include <algorithm>
10#include <atomic>
11#include <concepts>
12#include <cstddef>
13#include <cstdint>
14#include <iterator>
15#include <ranges>
16#include <utility>
17#include <vector>
18
19namespace helios::ecs::details {
20
21/**
22 * @brief Entity manager responsible for entity creation, destruction, and validation.
23 * @details Manages entity lifecycle with generation counters to handle entity recycling safely.
24 * @note Thread-safe only for validation operations.
25 */
26class Entities {
27public:
28 Entities() = default;
29 Entities(const Entities&) = delete;
30 Entities(Entities&& /*other*/) noexcept;
32
34 Entities& operator=(Entities&& /*other*/) noexcept;
35
36 /**
37 * @brief Creates reserved entities in the metadata.
38 * @details Processes all reserved entity IDs and creates their metadata.
39 * @note Not thread-safe, must be called from single thread only (typically during World::Update()).
40 */
42
43 /**
44 * @brief Clears all entities.
45 * @details Destroys all entities and resets the manager state.
46 * @note Not thread-safe, should be called from main thread only.
47 */
48 void Clear() noexcept;
49
50 /**
51 * @brief Reserves space for entities to minimize allocations.
52 * @details Pre-allocates storage for the specified number of entities.
53 * @note Not thread-safe, should be called from main thread only.
54 * @param count Number of entities to reserve space for
55 */
56 void Reserve(size_t count);
57
58 /**
59 * @brief Reserves an entity ID that can be used immediately.
60 * @details The actual entity creation is deferred until FlushReservedEntities() is called.
61 * @note Thread-safe.
62 * @return Reserved entity with valid index and generation
63 */
65
66 /**
67 * @brief Creates a new entity.
68 * @details Reuses dead entity slots when available, otherwise creates new ones.
69 * @note Not thread-safe, should be called from main thread only during World::Update().
70 * @return Newly created entity with valid index and generation
71 */
73
74 /**
75 * @brief Creates multiple entities at once and outputs them via an output iterator.
76 * @details Batch creation is more efficient than individual calls. Entities are written
77 * to the provided output iterator, avoiding internal allocations.
78 * @note Not thread-safe, should be called from main thread only during World::Update().
79 * @tparam OutputIt Output iterator type that accepts Entity values
80 * @param count Number of entities to create
81 * @param out Output iterator to write created entities to
82 * @return Output iterator pointing past the last written entity
83 *
84 * @example
85 * @code
86 * std::vector<Entity> entities;
87 * entities.reserve(100);
88 * manager.CreateEntities(100, std::back_inserter(entities));
89 *
90 * // Or with pre-allocated array:
91 * std::array<Entity, 10> arr;
92 * manager.CreateEntities(10, arr.begin());
93 *
94 * // Or with span output:
95 * Entity buffer[50];
96 * manager.CreateEntities(50, std::begin(buffer));
97 * @endcode
98 */
101
102 /**
103 * @brief Destroys an entity by incrementing its generation.
104 * @details Marks entity as dead and adds its index to the free list for reuse.
105 * @note Not thread-safe, should be called from main thread only during World::Update().
106 * Triggers assertion if entity is invalid.
107 * Ignored if entity do not exist.
108 * @param entity Entity to destroy
109 */
110 void Destroy(Entity entity);
111
112 /**
113 * @brief Destroys an entity by incrementing its generation.
114 * @details Marks entity as dead and adds its index to the free list for reuse.
115 * @note Not thread-safe, should be called from main thread only during World::Update().
116 * Triggers assertion if any entity is invalid.
117 * Entities that do not exist are ignored.
118 * @tparam R Range type containing Entity elements
119 * @param entities Entities to destroy
120 */
123 void Destroy(const R& entities);
124
125 /**
126 * @brief Checks if an entity exists and is valid.
127 * @details Validates both the entity structure and its current generation.
128 * @note Thread-safe for read operations.
129 * @param entity Entity to validate
130 * @return True if entity exists and is valid, false otherwise
131 */
132 [[nodiscard]] bool IsValid(Entity entity) const noexcept;
133
134 /**
135 * @brief Gets the current number of living entities.
136 * @details Returns count of entities that are currently alive.
137 * @note Thread-safe.
138 * @return Number of living entities
139 */
140 [[nodiscard]] size_t Count() const noexcept { return entity_count_.load(std::memory_order_relaxed); }
141
142 /**
143 * @brief Returns current generation value for a given index (or kInvalidGeneration if out of range).
144 * @warning Triggers assertion if index is invalid.
145 */
147
148private:
150
151 std::vector<Entity::GenerationType> generations_; ///< Generation counter for each entity index
152 std::vector<Entity::IndexType> free_indices_; ///< Recycled entity indices
153 std::atomic<size_t> entity_count_{0}; ///< Number of living entities
154 std::atomic<Entity::IndexType> next_index_{0}; ///< Next available index (thread-safe)
155 std::atomic<int64_t> free_cursor_{0}; ///< Cursor for free list (negative means reserved entities)
156};
157
159 : generations_(std::move(other.generations_)),
160 free_indices_(std::move(other.free_indices_)),
161 entity_count_(other.entity_count_.load(std::memory_order_relaxed)),
162 next_index_(other.next_index_.load(std::memory_order_relaxed)),
163 free_cursor_(other.free_cursor_.load(std::memory_order_relaxed)) {
164 other.entity_count_.store(0, std::memory_order_relaxed);
165 other.next_index_.store(0, std::memory_order_relaxed);
166 other.free_cursor_.store(0, std::memory_order_relaxed);
167}
168
169inline Entities& Entities::operator=(Entities&& other) noexcept {
170 if (this == &other) [[unlikely]] {
171 return *this;
172 }
173
174 generations_ = std::move(other.generations_);
175 free_indices_ = std::move(other.free_indices_);
176 entity_count_.store(other.entity_count_.load(std::memory_order_relaxed), std::memory_order_relaxed);
177 next_index_.store(other.next_index_.load(std::memory_order_relaxed), std::memory_order_relaxed);
178 free_cursor_.store(other.free_cursor_.load(std::memory_order_relaxed), std::memory_order_relaxed);
179
180 return *this;
181}
182
184 std::ranges::fill(generations_, Entity::kInvalidGeneration);
185 free_indices_.clear();
186 next_index_.store(0, std::memory_order_relaxed);
187 free_cursor_.store(0, std::memory_order_relaxed);
188 entity_count_.store(0, std::memory_order_relaxed);
189}
190
191inline void Entities::Reserve(size_t count) {
192 if (count > generations_.size()) {
193 generations_.resize(count, Entity::kInvalidGeneration);
194 }
195 free_indices_.reserve(count);
196}
197
199 // Atomically reserve an index by incrementing the next available index.
200 // NOTE: Do NOT mutate metadata (e.g. `generations_` or `entity_count_`) here because
201 // this function is thread-safe and may be called concurrently. The actual metadata
202 // initialization for reserved indices is performed in `FlushReservedEntities()` which
203 // must be called from the main thread.
204 const Entity::IndexType index = next_index_.fetch_add(1, std::memory_order_relaxed);
205
206 // Return a placeholder entity with generation 1. This is only a reservation handle;
207 // the real creation/metadata setup happens in FlushReservedEntities().
208 return {index, 1};
209}
210
211inline void Entities::Destroy(Entity entity) {
212 HELIOS_ASSERT(entity.Valid(), "Failed to destroy entity: Entity is invalid!");
213 if (!IsValid(entity)) [[unlikely]] {
214 return;
215 }
216
217 const Entity::IndexType index = entity.Index();
218 ++generations_[index]; // Invalidate entity
219 free_indices_.push_back(index);
220
221 free_cursor_.store(static_cast<int64_t>(free_indices_.size()), std::memory_order_relaxed);
222 entity_count_.fetch_sub(1, std::memory_order_relaxed);
223}
224
225template <std::ranges::range R>
226 requires std::same_as<std::ranges::range_value_t<R>, Entity>
227inline void Entities::Destroy(const R& entities) {
228 // If the incoming range is sized we can reserve capacity up front to avoid reallocations.
229 if constexpr (std::ranges::sized_range<R>) {
230 const auto incoming = std::ranges::size(entities);
231 free_indices_.reserve(free_indices_.size() + incoming);
232 }
233
234 // Small local buffer to collect indices we're going to free.
235 // We collect them locally so we can do a single insert into free_indices_,
236 // and do a single atomic update for free_cursor_ and entity_count_.
237 std::vector<Entity::IndexType> batch;
238 if constexpr (std::ranges::sized_range<R>) {
239 batch.reserve(std::ranges::size(entities));
240 } else {
241 batch.reserve(16); // small default to avoid too many reallocs for small ranges
242 }
243
244 // Process each entity: validate, increment generation, collect index.
245 // We increment generation immediately so behaviour matches the single-entity Destroy
246 // (i.e. duplicates in the input will fail the second validation).
247 for (const Entity& entity : entities) {
248 HELIOS_ASSERT(entity.Valid(), "Failed to destroy entities: Entity is invalid!");
249 if (!IsValid(entity)) {
250 // Skip entities already destroyed / wrong generation
251 continue;
252 }
253
254 const Entity::IndexType index = entity.Index();
255 ++generations_[index]; // Invalidate entity
256 batch.push_back(index);
257 }
258
259 if (!batch.empty()) {
260#ifdef HELIOS_CONTAINERS_RANGES_AVALIABLE
261 free_indices_.append_range(batch);
262#else
263 free_indices_.insert(free_indices_.end(), batch.begin(), batch.end());
264#endif
265 free_cursor_.store(static_cast<int64_t>(free_indices_.size()), std::memory_order_relaxed);
266 entity_count_.fetch_sub(batch.size(), std::memory_order_relaxed);
267 }
268}
269
270template <std::output_iterator<Entity> OutputIt>
272 if (count == 0) [[unlikely]] {
273 return out;
274 }
275
276 // Try to satisfy as many as possible from the free list first
277 size_t remaining = count;
278 int64_t cursor = free_cursor_.load(std::memory_order_relaxed);
279 // Use std::max with explicit signed type to avoid non-standard integer literal suffix
280 const size_t available_free = static_cast<size_t>(std::max<int64_t>(int64_t{0}, cursor));
281 const size_t from_free_list = std::min(remaining, available_free);
282
283 if (from_free_list > 0) {
284 const int64_t new_cursor = cursor - static_cast<int64_t>(from_free_list);
285 if (free_cursor_.compare_exchange_strong(cursor, new_cursor, std::memory_order_relaxed)) {
286 // Successfully claimed indices from free list
287 for (size_t i = 0; i < from_free_list; ++i) {
288 const size_t free_index = static_cast<size_t>(new_cursor) + i;
289 if (free_index >= free_indices_.size()) {
290 continue;
291 }
292
293 const Entity::IndexType index = free_indices_[free_index];
294 if (index >= generations_.size()) {
295 continue;
296 }
297
298 const Entity::GenerationType generation = generations_[index];
299 *out = CreateEntityWithId(index, generation);
300 ++out;
301 }
303 }
304 }
305
306 // Create new entities for remaining count
307 if (remaining > 0) {
309 next_index_.fetch_add(static_cast<Entity::IndexType>(remaining), std::memory_order_relaxed);
311
312 // Ensure generations array is large enough
313 if (end_index > generations_.size()) {
314 generations_.resize(end_index, Entity::kInvalidGeneration);
315 }
316
317 // Create entities with generation 1
318 for (Entity::IndexType index = start_index; index < end_index; ++index) {
319 generations_[index] = 1;
320 *out = CreateEntityWithId(index, 1);
321 ++out;
322 }
323 }
324
325 return out;
326}
327
328inline bool Entities::IsValid(Entity entity) const noexcept {
329 if (!entity.Valid()) [[unlikely]] {
330 return false;
331 }
332
333 const Entity::IndexType index = entity.Index();
334 return index < generations_.size() && generations_[index] == entity.Generation() &&
335 generations_[index] != Entity::kInvalidGeneration;
336}
337
339 HELIOS_ASSERT(index != Entity::kInvalidIndex, "Failed to get generation: index is invalid!");
340 return index < generations_.size() ? generations_[index] : Entity::kInvalidGeneration;
341}
342
343inline Entity Entities::CreateEntityWithId(Entity::IndexType index, Entity::GenerationType generation) {
344 if (index >= generations_.size()) {
345 generations_.resize(index + 1, Entity::kInvalidGeneration);
346 }
347
348 generations_[index] = generation;
349 entity_count_.fetch_add(1, std::memory_order_relaxed);
350
351 return {index, generation};
352}
353
354} // namespace helios::ecs::details
#define HELIOS_ASSERT(condition,...)
Assertion macro that aborts execution in debug builds.
Definition assert.hpp:140
iterator begin() const
Gets iterator to first matching entity.
Definition query.hpp:2317
iterator end() const noexcept
Gets iterator past the last matching entity.
Definition query.hpp:1603
Unique identifier for entities with generation counter to handle recycling.
Definition entity.hpp:21
constexpr bool Valid() const noexcept
Checks if the entity is valid.
Definition entity.hpp:58
uint32_t IndexType
Definition entity.hpp:23
uint32_t GenerationType
Definition entity.hpp:24
static constexpr GenerationType kInvalidGeneration
Definition entity.hpp:27
constexpr IndexType Index() const noexcept
Gets the index component of the entity.
Definition entity.hpp:75
static constexpr IndexType kInvalidIndex
Definition entity.hpp:26
Entity manager responsible for entity creation, destruction, and validation.
void Clear() noexcept
Clears all entities.
Entity ReserveEntity()
Reserves an entity ID that can be used immediately.
Entity CreateEntity()
Creates a new entity.
void FlushReservedEntities()
Creates reserved entities in the metadata.
void Destroy(Entity entity)
Destroys an entity by incrementing its generation.
void Reserve(size_t count)
Reserves space for entities to minimize allocations.
bool IsValid(Entity entity) const noexcept
Checks if an entity exists and is valid.
Entity::GenerationType GetGeneration(Entity::IndexType index) const noexcept
Returns current generation value for a given index (or kInvalidGeneration if out of range).
OutputIt CreateEntities(size_t count, OutputIt out)
Entities(const Entities &)=delete
Entities & operator=(const Entities &)=delete
size_t Count() const noexcept
Gets the current number of living entities.
BasicQuery< World, Allocator, Components... > Query
Type alias for query with mutable world access.
Definition query.hpp:2481
STL namespace.