Skip to content

Instantly share code, notes, and snippets.

@matthewjberger
Created May 16, 2026 03:04
Show Gist options
  • Select an option

  • Save matthewjberger/bbf7e0dc3fd5be28fe01f5c72cdaedb7 to your computer and use it in GitHub Desktop.

Select an option

Save matthewjberger/bbf7e0dc3fd5be28fe01f5c72cdaedb7 to your computer and use it in GitHub Desktop.
Build your own ECS in Rust, part 1: archetype storage
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