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}