Created
May 16, 2026 03:04
-
-
Save matthewjberger/bbf7e0dc3fd5be28fe01f5c72cdaedb7 to your computer and use it in GitHub Desktop.
Build your own ECS in Rust, part 1: archetype storage
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| use std::collections::HashMap; | |
| #[derive(Default, Copy, Clone, Debug, Eq, PartialEq, Hash)] | |
| pub struct Entity { | |
| pub id: u32, | |
| pub generation: u32, | |
| } | |
| #[derive(Default)] | |
| pub struct EntityAllocator { | |
| pub next_id: u32, | |
| pub free: Vec<(u32, u32)>, | |
| } | |
| impl EntityAllocator { | |
| pub fn allocate(&mut self) -> Entity { | |
| if let Some((id, generation)) = self.free.pop() { | |
| Entity { id, generation } | |
| } else { | |
| let id = self.next_id; | |
| self.next_id += 1; | |
| Entity { id, generation: 0 } | |
| } | |
| } | |
| pub fn deallocate(&mut self, entity: Entity) { | |
| self.free.push((entity.id, entity.generation.wrapping_add(1))); | |
| } | |
| } | |
| #[derive(Default, Copy, Clone)] | |
| pub struct EntityLocation { | |
| pub generation: u32, | |
| pub table_index: u32, | |
| pub array_index: u32, | |
| pub allocated: bool, | |
| } | |
| #[derive(Default)] | |
| pub struct EntityLocations { | |
| pub locations: Vec<EntityLocation>, | |
| } | |
| impl EntityLocations { | |
| pub fn ensure_slot(&mut self, id: u32) { | |
| let needed = id as usize + 1; | |
| if needed > self.locations.len() { | |
| self.locations.resize(needed, EntityLocation::default()); | |
| } | |
| } | |
| pub fn get(&self, entity: Entity) -> Option<(usize, usize)> { | |
| let location = self.locations.get(entity.id as usize)?; | |
| if !location.allocated || location.generation != entity.generation { | |
| return None; | |
| } | |
| Some((location.table_index as usize, location.array_index as usize)) | |
| } | |
| pub fn set(&mut self, entity: Entity, table_index: usize, array_index: usize) { | |
| self.ensure_slot(entity.id); | |
| self.locations[entity.id as usize] = EntityLocation { | |
| generation: entity.generation, | |
| table_index: table_index as u32, | |
| array_index: array_index as u32, | |
| allocated: true, | |
| }; | |
| } | |
| pub fn mark_deallocated(&mut self, id: u32) { | |
| if let Some(location) = self.locations.get_mut(id as usize) { | |
| location.allocated = false; | |
| } | |
| } | |
| } | |
| #[derive(Default, Clone, Debug)] | |
| pub struct Position { | |
| pub x: f32, | |
| pub y: f32, | |
| } | |
| #[derive(Default, Clone, Debug)] | |
| pub struct Velocity { | |
| pub x: f32, | |
| pub y: f32, | |
| } | |
| pub const POSITION: u64 = 1 << 0; | |
| pub const VELOCITY: u64 = 1 << 1; | |
| #[derive(Default)] | |
| pub struct ComponentArrays { | |
| pub mask: u64, | |
| pub entities: Vec<Entity>, | |
| pub positions: Vec<Position>, | |
| pub velocities: Vec<Velocity>, | |
| } | |
| #[derive(Default)] | |
| pub struct World { | |
| pub allocator: EntityAllocator, | |
| pub entity_locations: EntityLocations, | |
| pub tables: Vec<ComponentArrays>, | |
| pub table_lookup: HashMap<u64, usize>, | |
| } | |
| impl World { | |
| pub fn get_or_create_table(&mut self, mask: u64) -> usize { | |
| if let Some(&index) = self.table_lookup.get(&mask) { | |
| return index; | |
| } | |
| let index = self.tables.len(); | |
| self.tables.push(ComponentArrays { | |
| mask, | |
| ..Default::default() | |
| }); | |
| self.table_lookup.insert(mask, index); | |
| index | |
| } | |
| pub fn spawn(&mut self, mask: u64) -> Entity { | |
| let entity = self.allocator.allocate(); | |
| let table_index = self.get_or_create_table(mask); | |
| let table = &mut self.tables[table_index]; | |
| let array_index = table.entities.len(); | |
| table.entities.push(entity); | |
| if mask & POSITION != 0 { | |
| table.positions.push(Position::default()); | |
| } | |
| if mask & VELOCITY != 0 { | |
| table.velocities.push(Velocity::default()); | |
| } | |
| self.entity_locations.set(entity, table_index, array_index); | |
| entity | |
| } | |
| pub fn get_position(&self, entity: Entity) -> Option<&Position> { | |
| let (table_index, array_index) = self.entity_locations.get(entity)?; | |
| let table = &self.tables[table_index]; | |
| if table.mask & POSITION == 0 { | |
| return None; | |
| } | |
| Some(&table.positions[array_index]) | |
| } | |
| pub fn get_position_mut(&mut self, entity: Entity) -> Option<&mut Position> { | |
| let (table_index, array_index) = self.entity_locations.get(entity)?; | |
| let table = &mut self.tables[table_index]; | |
| if table.mask & POSITION == 0 { | |
| return None; | |
| } | |
| Some(&mut table.positions[array_index]) | |
| } | |
| pub fn get_velocity(&self, entity: Entity) -> Option<&Velocity> { | |
| let (table_index, array_index) = self.entity_locations.get(entity)?; | |
| let table = &self.tables[table_index]; | |
| if table.mask & VELOCITY == 0 { | |
| return None; | |
| } | |
| Some(&table.velocities[array_index]) | |
| } | |
| pub fn get_velocity_mut(&mut self, entity: Entity) -> Option<&mut Velocity> { | |
| let (table_index, array_index) = self.entity_locations.get(entity)?; | |
| let table = &mut self.tables[table_index]; | |
| if table.mask & VELOCITY == 0 { | |
| return None; | |
| } | |
| Some(&mut table.velocities[array_index]) | |
| } | |
| pub fn despawn(&mut self, entity: Entity) -> bool { | |
| let Some((table_index, array_index)) = self.entity_locations.get(entity) else { | |
| return false; | |
| }; | |
| self.entity_locations.mark_deallocated(entity.id); | |
| self.allocator.deallocate(entity); | |
| let table = &mut self.tables[table_index]; | |
| let last_index = table.entities.len() - 1; | |
| let swapped = if array_index < last_index { | |
| Some(table.entities[last_index]) | |
| } else { | |
| None | |
| }; | |
| table.entities.swap_remove(array_index); | |
| if table.mask & POSITION != 0 { | |
| table.positions.swap_remove(array_index); | |
| } | |
| if table.mask & VELOCITY != 0 { | |
| table.velocities.swap_remove(array_index); | |
| } | |
| if let Some(moved) = swapped { | |
| self.entity_locations.set(moved, table_index, array_index); | |
| } | |
| true | |
| } | |
| } | |
| fn main() { | |
| let mut world = World::default(); | |
| let mover = world.spawn(POSITION | VELOCITY); | |
| *world.get_position_mut(mover).unwrap() = Position { x: 1.0, y: 2.0 }; | |
| *world.get_velocity_mut(mover).unwrap() = Velocity { x: 0.5, y: 0.0 }; | |
| let landmark = world.spawn(POSITION); | |
| *world.get_position_mut(landmark).unwrap() = Position { x: 10.0, y: 10.0 }; | |
| let position = world.get_position(mover).unwrap(); | |
| let velocity = world.get_velocity(mover).unwrap(); | |
| println!( | |
| "mover: pos=({}, {}) vel=({}, {})", | |
| position.x, position.y, velocity.x, velocity.y | |
| ); | |
| assert!(world.get_velocity(landmark).is_none()); | |
| world.despawn(mover); | |
| assert!(world.get_position(mover).is_none()); | |
| let recycled = world.spawn(POSITION); | |
| assert_eq!(recycled.id, mover.id); | |
| assert_eq!(recycled.generation, mover.generation + 1); | |
| assert!(world.get_position(mover).is_none()); | |
| assert!(world.get_position(recycled).is_some()); | |
| println!("ok"); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment