hyperion/entity.rs
1/// A generational entity identifier used by the ECS.
2///
3/// # Fields
4/// - `id`: The slot index assigned by the allocator identifying this entity.
5/// - `generation`: Monotonically increasing number preventing stale entity reuse.
6///
7/// # Safety
8/// - Two entities are considered identical only if both `id` and `generation` match.
9/// - When an entity is destroyed, its `generation` is incremented. Holding an old
10/// `Entity` value after destruction is safe: APIs validate the generation and will
11/// treat it as dead, preventing accidental access to a recycled `id`.
12#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
13pub struct Entity {
14 pub(crate) id: u32,
15 pub(crate) generation: u32,
16}
17
18/// Allocates and manages entity IDs and generations, allowing safe reuse of entity IDs.
19///
20/// # Fields
21/// - `generations`: Per-id generation counters used to validate entity liveness.
22/// - `free_ids`: Pool of reusable entity IDs.
23///
24/// # Safety
25/// - Reuse safety is enforced by bumping the generation on destroy, invalidating
26/// stale `Entity` values that refer to the same `id` with an older generation.
27#[derive(Default)]
28pub struct EntityAllocator {
29 pub(crate) generations: Vec<u32>,
30 pub(crate) free_ids: Vec<u32>,
31}
32
33impl EntityAllocator {
34 /// Creates a new entity, reusing an ID if possible, otherwise assigning a new one.
35 ///
36 /// # Returns
37 /// - `Entity`: The newly allocated entity identifier (with current generation for its id).
38 pub fn create(&mut self) -> Entity {
39 if let Some(id) = self.free_ids.pop() {
40 // Reuse a freed ID, returning current generation.
41 let g = self.generations[id as usize];
42 Entity { id, generation: g }
43 } else {
44 // No free IDs, create new ID with generation 0.
45 let id = self.generations.len() as u32;
46 self.generations.push(0);
47 Entity { id, generation: 0 }
48 }
49 }
50
51 /// Destroys an entity, marking it as dead and adding its ID back to free pool.
52 ///
53 /// # Parameters
54 /// - `entity`: The entity to destroy. If already dead or invalid, this is a no-op.
55 pub fn destroy(&mut self, entity: Entity) {
56 if self.is_alive(entity) {
57 // Increment generation to invalidate old references.
58 self.generations[entity.id as usize] += 1;
59 self.free_ids.push(entity.id);
60 }
61 }
62
63 /// Checks if an entity is alive by comparing its generation against stored generation.
64 ///
65 /// # Parameters
66 /// - `entity`: The entity identifier to check.
67 ///
68 /// # Returns
69 /// - `bool`: `true` if the entity is currently alive; `false` otherwise.
70 pub fn is_alive(&self, entity: Entity) -> bool {
71 self.generations
72 .get(entity.id as usize)
73 .map_or(false, |&g| g == entity.generation)
74 }
75}