Created
May 27, 2026 12:53
-
-
Save matthewjberger/ff6fc2aba33d94330fdadbbb99d45563 to your computer and use it in GitHub Desktop.
An archetype ECS kernel in ~320 lines of Rust (no proc-macros, no unsafe, std only)
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
| // An archetype ECS kernel in ~320 lines of Rust. | |
| // | |
| // An ECS (Entity Component System) organises game/simulation state as three | |
| // things: | |
| // | |
| // Entities -- opaque handles that identify a "thing" in the world. | |
| // Components -- plain data attached to entities (position, velocity, ...). | |
| // Systems -- functions that iterate over entities with a given set of | |
| // components and transform their data. | |
| // | |
| // This implementation uses the *archetype* storage strategy: entities that | |
| // share exactly the same set of components are grouped together in a table, | |
| // with one contiguous array per component type. That layout is cache-friendly | |
| // for iteration (systems read one array at a time, never chasing pointers) and | |
| // makes adding/removing components a well-defined migration between tables. | |
| // | |
| // The five load-bearing ideas, in the order they appear below: | |
| // | |
| // 1. Generational handles -- stale entity references are detectable. | |
| // 2. Dense location Vec -- O(1) entity-to-table-row lookup. | |
| // 3. Archetype tables (SoA) -- one Vec per component, shared row index. | |
| // 4. Runtime structural change -- migrate() moves a row between tables. | |
| // 5. Two caches -- edge graph (migration fast-path) and | |
| // query cache (system iteration fast-path). | |
| // -- Component types ---------------------------------------------------------- | |
| // | |
| // Components are plain data structs with no methods. Default is required | |
| // because new rows are zero-initialised when an entity gains a component it | |
| // did not previously have (see table_move_row). Clone is required because the | |
| // edge graph stores component data that must survive a table resize. | |
| #[derive(Default, Clone, Debug)] | |
| struct Position { x: f32, y: f32 } | |
| #[derive(Default, Clone, Debug)] | |
| struct Velocity { dx: f32, dy: f32 } | |
| #[derive(Default, Clone, Debug)] | |
| struct Vitality { hp: f32 } | |
| // Each component type gets a unique power-of-two bit. An archetype's identity | |
| // is the bitwise OR of all its component bits -- its "mask". Two entities with | |
| // the same mask live in the same table. | |
| const POSITION: u64 = 1 << 0; | |
| const VELOCITY: u64 = 1 << 1; | |
| const VITALITY: u64 = 1 << 2; | |
| // Used to size the fixed edge arrays inside each table. Must equal the number | |
| // of component constants above. | |
| const COMPONENT_COUNT: usize = 3; | |
| // -- Entity handle ------------------------------------------------------------ | |
| // | |
| // An Entity is a (index, generation) pair. The index is a slot number in the | |
| // allocator's generations Vec and in the locations Vec -- a dense array, not a | |
| // HashMap. The generation distinguishes successive occupants of the same slot: | |
| // when an entity is despawned its slot's generation is bumped, so any handle | |
| // that survived with the old generation is detectably stale. | |
| // | |
| // This is the "generational index" pattern (also called a "slot map handle"). | |
| // It gives O(1) liveness checks and O(1) location lookups with no hashing. | |
| #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| struct Entity { index: u32, generation: u32 } | |
| // -- Location ----------------------------------------------------------------- | |
| // | |
| // For every live entity we record exactly where its component data lives: | |
| // which table (by index into World::tables) and which row within that table. | |
| // Every component Vec in the table shares the same row index, so reading | |
| // table.positions[loc.row] and table.velocities[loc.row] gives the same | |
| // entity's data. | |
| #[derive(Clone, Copy, Debug)] | |
| struct Location { table: usize, row: usize } | |
| // -- Archetype table ---------------------------------------------------------- | |
| // | |
| // One Table exists per unique component combination ever seen. Its mask is the | |
| // bitwise OR of the components it holds. The component Vecs are the actual | |
| // storage; they are always the same length as entities and share a row index. | |
| // | |
| // The edge graph fields cache migration destinations so that add/remove | |
| // component calls do not need to hash the table_map on every call: | |
| // | |
| // add_single / remove_single -- fixed arrays indexed by component_index(). | |
| // Single-component transitions are the hot path (one status effect, one | |
| // flag), so they get an array probe: one index load, no hashing. Slot ci | |
| // in add_single holds the index of the table reached by adding component ci | |
| // from this archetype. None means the transition has not been taken yet. | |
| // | |
| // add_multi / remove_multi -- HashMap keyed on the changed-bit delta. | |
| // Multi-component transitions are rarer (e.g. spawning with three | |
| // components at once, or stripping two at once). They fall back to a map | |
| // keyed on the XOR of old and new masks. | |
| // | |
| // Edges are filled lazily on first traversal and never evicted -- tables are | |
| // never removed, so a cached destination index never goes stale. | |
| use std::collections::HashMap; | |
| struct Table { | |
| mask: u64, | |
| entities: Vec<Entity>, | |
| positions: Vec<Position>, | |
| velocities: Vec<Velocity>, | |
| vitalities: Vec<Vitality>, | |
| add_single: [Option<usize>; COMPONENT_COUNT], | |
| remove_single: [Option<usize>; COMPONENT_COUNT], | |
| add_multi: HashMap<u64, usize>, | |
| remove_multi: HashMap<u64, usize>, | |
| } | |
| impl Default for Table { | |
| fn default() -> Self { | |
| Self { | |
| mask: 0, | |
| entities: Vec::new(), | |
| positions: Vec::new(), | |
| velocities: Vec::new(), | |
| vitalities: Vec::new(), | |
| add_single: [None; COMPONENT_COUNT], | |
| remove_single: [None; COMPONENT_COUNT], | |
| add_multi: HashMap::new(), | |
| remove_multi: HashMap::new(), | |
| } | |
| } | |
| } | |
| // -- World -------------------------------------------------------------------- | |
| // | |
| // World is a plain data struct -- a bag of Vecs and HashMaps with no hidden | |
| // state. All operations are free functions that take &mut World (or the | |
| // relevant sub-fields) as arguments. | |
| // | |
| // tables -- the archetype tables; indexed by table index. | |
| // table_map -- mask -> table index; O(1) lookup by component set. | |
| // query_cache -- required_mask -> Vec of matching table indices. | |
| // Systems call this every frame; it must be fast. | |
| // locations -- Entity::index -> Option<Location>; dense Vec, O(1) lookup. | |
| // generations -- Entity::index -> current generation; drives is_live(). | |
| // free_ids -- recycled slot indices waiting for reuse. | |
| #[derive(Default)] | |
| struct World { | |
| tables: Vec<Table>, | |
| table_map: HashMap<u64, usize>, | |
| query_cache: HashMap<u64, Vec<usize>>, | |
| locations: Vec<Option<Location>>, | |
| generations: Vec<u32>, | |
| free_ids: Vec<u32>, | |
| } | |
| // -- Allocator operations ----------------------------------------------------- | |
| // | |
| // alloc_entity pops a recycled slot from free_ids if one exists, reusing its | |
| // index with the generation that was written when it was freed. Otherwise it | |
| // extends the generations Vec with a fresh slot at generation 0. | |
| // | |
| // free_entity bumps the generation *before* pushing the index onto free_ids. | |
| // Any handle that was alive before the bump now has a generation that no longer | |
| // matches generations[index], so is_live returns false for it. The slot is then | |
| // safe to reuse for a new entity. | |
| // | |
| // is_live does a single indexed read and an equality check -- no hashing, | |
| // no pointer chasing. It is called on every accessor and on every | |
| // add/remove/despawn to guard against stale handles. | |
| fn alloc_entity(generations: &mut Vec<u32>, free_ids: &mut Vec<u32>) -> Entity { | |
| if let Some(index) = free_ids.pop() { | |
| Entity { index, generation: generations[index as usize] } | |
| } else { | |
| let index = generations.len() as u32; | |
| generations.push(0); | |
| Entity { index, generation: 0 } | |
| } | |
| } | |
| fn free_entity(generations: &mut Vec<u32>, free_ids: &mut Vec<u32>, e: Entity) { | |
| generations[e.index as usize] = generations[e.index as usize].wrapping_add(1); | |
| free_ids.push(e.index); | |
| } | |
| fn is_live(generations: &[u32], e: Entity) -> bool { | |
| generations.get(e.index as usize).copied() == Some(e.generation) | |
| } | |
| // -- Location operations ------------------------------------------------------ | |
| // | |
| // locations is a Vec<Option<Location>> indexed by Entity::index. Using a Vec | |
| // instead of a HashMap is intentional: entity indices come from a dense | |
| // counter, so the Vec stays compact and every access is a single indexed load. | |
| // | |
| // set_location extends the Vec if the new index is beyond its current length. | |
| // clear_location writes None to mark the slot as empty after despawn; this is | |
| // belt-and-suspenders -- is_live already rejects stale handles before the | |
| // location is read, but clearing keeps the Vec tidy. | |
| fn get_location(locations: &[Option<Location>], e: Entity) -> Option<Location> { | |
| locations.get(e.index as usize).and_then(|l| *l) | |
| } | |
| fn set_location(locations: &mut Vec<Option<Location>>, e: Entity, loc: Location) { | |
| let i = e.index as usize; | |
| if i >= locations.len() { locations.resize(i + 1, None); } | |
| locations[i] = Some(loc); | |
| } | |
| fn clear_location(locations: &mut Vec<Option<Location>>, e: Entity) { | |
| if let Some(s) = locations.get_mut(e.index as usize) { *s = None; } | |
| } | |
| // -- Query cache operations --------------------------------------------------- | |
| // | |
| // query_tables returns the list of table indices whose mask is a superset of | |
| // `required`. Systems call this every frame, so the result is memoised in a | |
| // HashMap keyed on the required mask. | |
| // | |
| // The first call for a given mask scans all tables once and stores the result. | |
| // Subsequent calls return the cached slice immediately -- one HashMap probe, | |
| // no scanning. | |
| // | |
| // on_new_table is called whenever get_or_create_table adds a new table. It | |
| // patches every existing cache entry whose required mask is a subset of the | |
| // new table's mask, appending the new table index to those entries. New tables | |
| // are rare (at most one per unique component combination ever used) so this | |
| // patch loop is cold. | |
| fn query_tables<'a>( | |
| cache: &'a mut HashMap<u64, Vec<usize>>, | |
| tables: &[Table], | |
| required: u64, | |
| ) -> &'a [usize] { | |
| cache.entry(required).or_insert_with(|| { | |
| tables.iter().enumerate() | |
| .filter(|(_, t)| t.mask & required == required) | |
| .map(|(i, _)| i) | |
| .collect() | |
| }) | |
| } | |
| fn on_new_table(cache: &mut HashMap<u64, Vec<usize>>, new_mask: u64, new_index: usize) { | |
| for (required, indices) in cache.iter_mut() { | |
| if new_mask & required == *required { | |
| indices.push(new_index); | |
| } | |
| } | |
| } | |
| // -- Table operations --------------------------------------------------------- | |
| // | |
| // component_index maps a single-bit mask to its array index (0, 1, 2, ...). | |
| // It is only called in the delta.count_ones() == 1 branch of migrate, where | |
| // the single-bit precondition is already established. | |
| fn component_index(single_bit: u64) -> Option<usize> { | |
| match single_bit { | |
| POSITION => Some(0), | |
| VELOCITY => Some(1), | |
| VITALITY => Some(2), | |
| _ => None, | |
| } | |
| } | |
| // table_push appends a new row of default-initialised components and returns | |
| // the row index. Only the Vecs for components present in table.mask grow; | |
| // absent components have no storage in this table. | |
| fn table_push(table: &mut Table, e: Entity) -> usize { | |
| let row = table.entities.len(); | |
| table.entities.push(e); | |
| if table.mask & POSITION != 0 { table.positions.push(Position::default()); } | |
| if table.mask & VELOCITY != 0 { table.velocities.push(Velocity::default()); } | |
| if table.mask & VITALITY != 0 { table.vitalities.push(Vitality::default()); } | |
| row | |
| } | |
| // table_swap_remove removes row `idx` in O(1) by overwriting it with the last | |
| // row and truncating. All component Vecs are kept in sync. Returns the entity | |
| // that was moved from the last slot into `idx` (if any) so the caller can | |
| // update its location record. Returns None if `idx` was already the last row. | |
| fn table_swap_remove(table: &mut Table, idx: usize) -> Option<Entity> { | |
| let last = table.entities.len().saturating_sub(1); | |
| let moved = if idx < last { Some(table.entities[last]) } else { None }; | |
| table.entities.swap_remove(idx); | |
| if table.mask & POSITION != 0 { table.positions.swap_remove(idx); } | |
| if table.mask & VELOCITY != 0 { table.velocities.swap_remove(idx); } | |
| if table.mask & VITALITY != 0 { table.vitalities.swap_remove(idx); } | |
| moved | |
| } | |
| // table_move_row is the migration primitive. It copies one row from src into | |
| // dst, preserving component data for components present in both tables and | |
| // default-initialising components that are new in dst. Components that exist | |
| // in src but not in dst are simply dropped (they are being removed). | |
| // | |
| // mem::take replaces the source slot with Default::default() in-place before | |
| // swap_remove cleans it up. This avoids cloning: the data moves out of src | |
| // and into dst without copying. | |
| // | |
| // Returns (dst_row, back_filled): dst_row is the new row index in dst; | |
| // back_filled is the entity that swap_remove moved into src[idx] (if any). | |
| fn table_move_row(src: &mut Table, idx: usize, dst: &mut Table) -> (usize, Option<Entity>) { | |
| let entity = src.entities[idx]; | |
| dst.entities.push(entity); | |
| if dst.mask & POSITION != 0 { | |
| dst.positions.push(if src.mask & POSITION != 0 { | |
| std::mem::take(&mut src.positions[idx]) | |
| } else { Position::default() }); | |
| } | |
| if dst.mask & VELOCITY != 0 { | |
| dst.velocities.push(if src.mask & VELOCITY != 0 { | |
| std::mem::take(&mut src.velocities[idx]) | |
| } else { Velocity::default() }); | |
| } | |
| if dst.mask & VITALITY != 0 { | |
| dst.vitalities.push(if src.mask & VITALITY != 0 { | |
| std::mem::take(&mut src.vitalities[idx]) | |
| } else { Vitality::default() }); | |
| } | |
| let dst_row = dst.entities.len() - 1; | |
| let back_filled = table_swap_remove(src, idx); | |
| (dst_row, back_filled) | |
| } | |
| // -- Table registry ----------------------------------------------------------- | |
| // | |
| // get_or_create_table returns the index of the table for `mask`, creating it | |
| // if it does not yet exist. table_map gives O(1) lookup by mask. When a new | |
| // table is created, on_new_table patches the query cache so existing system | |
| // queries immediately see the new table without a full rescan. | |
| fn get_or_create_table( | |
| tables: &mut Vec<Table>, | |
| table_map: &mut HashMap<u64, usize>, | |
| query_cache: &mut HashMap<u64, Vec<usize>>, | |
| mask: u64, | |
| ) -> usize { | |
| if let Some(&i) = table_map.get(&mask) { return i; } | |
| let i = tables.len(); | |
| tables.push(Table { mask, ..Default::default() }); | |
| table_map.insert(mask, i); | |
| on_new_table(query_cache, mask, i); | |
| i | |
| } | |
| // -- Migration ---------------------------------------------------------------- | |
| // | |
| // migrate moves entity e from its current table to the table for new_mask. | |
| // It is called by add_components and remove_components after they have | |
| // computed the target mask. | |
| // | |
| // Edge graph lookup: | |
| // delta = old_mask XOR new_mask -- the bits that changed. | |
| // adding = new_mask > old_mask -- true iff new_mask is a strict superset. | |
| // (A superset mask is always numerically larger because all bits in | |
| // old_mask are also in new_mask, plus at least one more. This holds for | |
| // any bit-count delta, not just single-bit ones.) | |
| // | |
| // If delta has exactly one bit set, probe add_single or remove_single with | |
| // the component index -- an array access, no hashing. On a miss, resolve via | |
| // table_map and write the edge for next time. | |
| // | |
| // If delta has multiple bits set, probe add_multi or remove_multi with delta | |
| // as the key. Same miss/fill logic. | |
| // | |
| // The split_at_mut dance gives simultaneous mutable references to two | |
| // different elements of world.tables. Rust does not allow two &mut borrows | |
| // into the same Vec, but split_at_mut splits it into two non-overlapping | |
| // slices. assert_ne!(src_ti, dst_ti) documents and enforces the precondition | |
| // (add_components and remove_components both short-circuit the no-op case | |
| // before calling migrate, guaranteeing the tables differ). | |
| // | |
| // After table_move_row, two location records need updating: | |
| // - e itself now lives in dst at dst_row. | |
| // - The entity back-filled into src[loc.row] by swap_remove (if any) needs | |
| // its row updated to loc.row. | |
| fn migrate(world: &mut World, e: Entity, loc: Location, new_mask: u64) { | |
| let src_ti = loc.table; | |
| let old_mask = world.tables[src_ti].mask; | |
| let delta = old_mask ^ new_mask; | |
| let adding = new_mask > old_mask; | |
| let dst_ti = if delta.count_ones() == 1 { | |
| let ci = component_index(delta).unwrap(); | |
| let cached = if adding { | |
| world.tables[src_ti].add_single[ci] | |
| } else { | |
| world.tables[src_ti].remove_single[ci] | |
| }; | |
| cached.unwrap_or_else(|| { | |
| let dst = get_or_create_table( | |
| &mut world.tables, &mut world.table_map, &mut world.query_cache, new_mask, | |
| ); | |
| if adding { world.tables[src_ti].add_single[ci] = Some(dst); } | |
| else { world.tables[src_ti].remove_single[ci] = Some(dst); } | |
| dst | |
| }) | |
| } else { | |
| let cached = if adding { | |
| world.tables[src_ti].add_multi.get(&delta).copied() | |
| } else { | |
| world.tables[src_ti].remove_multi.get(&delta).copied() | |
| }; | |
| cached.unwrap_or_else(|| { | |
| let dst = get_or_create_table( | |
| &mut world.tables, &mut world.table_map, &mut world.query_cache, new_mask, | |
| ); | |
| if adding { world.tables[src_ti].add_multi.insert(delta, dst); } | |
| else { world.tables[src_ti].remove_multi.insert(delta, dst); } | |
| dst | |
| }) | |
| }; | |
| assert_ne!(src_ti, dst_ti); | |
| let (src, dst) = if src_ti < dst_ti { | |
| let (left, right) = world.tables.split_at_mut(dst_ti); | |
| (&mut left[src_ti], &mut right[0]) | |
| } else { | |
| let (left, right) = world.tables.split_at_mut(src_ti); | |
| (&mut right[0], &mut left[dst_ti]) | |
| }; | |
| let (dst_row, back_filled) = table_move_row(src, loc.row, dst); | |
| set_location(&mut world.locations, e, Location { table: dst_ti, row: dst_row }); | |
| if let Some(moved) = back_filled { | |
| set_location(&mut world.locations, moved, Location { table: src_ti, row: loc.row }); | |
| } | |
| } | |
| // -- World operations --------------------------------------------------------- | |
| // spawn creates a new entity with the given component mask, places it in the | |
| // appropriate table, and returns its handle. The handle is valid until despawn | |
| // is called for that entity. | |
| fn spawn(world: &mut World, mask: u64) -> Entity { | |
| let e = alloc_entity(&mut world.generations, &mut world.free_ids); | |
| let ti = get_or_create_table( | |
| &mut world.tables, &mut world.table_map, &mut world.query_cache, mask, | |
| ); | |
| let row = table_push(&mut world.tables[ti], e); | |
| set_location(&mut world.locations, e, Location { table: ti, row }); | |
| e | |
| } | |
| // despawn removes the entity from its table (swap_remove, O(1)), patches the | |
| // back-filled entity's location, clears the entity's own location, and frees | |
| // the slot. After despawn, is_live returns false for the handle. | |
| fn despawn(world: &mut World, e: Entity) { | |
| if !is_live(&world.generations, e) { return; } | |
| let loc = get_location(&world.locations, e).unwrap(); | |
| if let Some(moved) = table_swap_remove(&mut world.tables[loc.table], loc.row) { | |
| set_location(&mut world.locations, moved, Location { table: loc.table, row: loc.row }); | |
| } | |
| clear_location(&mut world.locations, e); | |
| free_entity(&mut world.generations, &mut world.free_ids, e); | |
| } | |
| // add_components migrates e to a new archetype whose mask is old | added. | |
| // No-ops if e already has all components in `added`, or if e is not live. | |
| // Component data already held by e is preserved across the migration. | |
| // Newly gained components are default-initialised. | |
| fn add_components(world: &mut World, e: Entity, added: u64) { | |
| if !is_live(&world.generations, e) { return; } | |
| let loc = get_location(&world.locations, e).unwrap(); | |
| let old_mask = world.tables[loc.table].mask; | |
| if old_mask & added == added { return; } | |
| migrate(world, e, loc, old_mask | added); | |
| } | |
| // remove_components migrates e to a new archetype whose mask is old & !removed. | |
| // No-ops if e has none of the components in `removed`, or if e is not live. | |
| // Removed component data is dropped during migration. | |
| fn remove_components(world: &mut World, e: Entity, removed: u64) { | |
| if !is_live(&world.generations, e) { return; } | |
| let loc = get_location(&world.locations, e).unwrap(); | |
| let old_mask = world.tables[loc.table].mask; | |
| if old_mask & removed == 0 { return; } | |
| migrate(world, e, loc, old_mask & !removed); | |
| } | |
| // -- Component accessors ------------------------------------------------------ | |
| // | |
| // Each accessor checks is_live first, then resolves the location, then checks | |
| // that the table's mask includes the requested component. Returning None for a | |
| // stale handle or a missing component avoids panics and makes the API | |
| // composable with Option combinators. | |
| // | |
| // get_position returns a shared reference (reading position during rendering | |
| // does not need mutation). get_velocity_mut and get_vitality_mut return mutable | |
| // references for systems that write component data. | |
| fn get_position(world: &World, e: Entity) -> Option<&Position> { | |
| if !is_live(&world.generations, e) { return None; } | |
| let loc = get_location(&world.locations, e)?; | |
| let t = &world.tables[loc.table]; | |
| (t.mask & POSITION != 0).then(|| &t.positions[loc.row]) | |
| } | |
| fn get_velocity_mut(world: &mut World, e: Entity) -> Option<&mut Velocity> { | |
| if !is_live(&world.generations, e) { return None; } | |
| let loc = get_location(&world.locations, e)?; | |
| let t = &mut world.tables[loc.table]; | |
| (t.mask & VELOCITY != 0).then(|| &mut t.velocities[loc.row]) | |
| } | |
| fn get_vitality_mut(world: &mut World, e: Entity) -> Option<&mut Vitality> { | |
| if !is_live(&world.generations, e) { return None; } | |
| let loc = get_location(&world.locations, e)?; | |
| let t = &mut world.tables[loc.table]; | |
| (t.mask & VITALITY != 0).then(|| &mut t.vitalities[loc.row]) | |
| } | |
| // -- Systems ------------------------------------------------------------------ | |
| // | |
| // Systems follow the same pattern: call query_tables to get the list of | |
| // matching table indices, then iterate over each table's component Vecs | |
| // directly. No per-entity dispatch, no virtual calls -- just a tight inner | |
| // loop over contiguous arrays. | |
| // | |
| // query_tables returns a borrowed slice, so we call .to_vec() to take a | |
| // snapshot before borrowing world.tables mutably for the inner loop. This is | |
| // the same pattern used throughout: separate the index lookup from the | |
| // mutation so the borrow checker is satisfied without unsafe. | |
| fn wind_system(world: &mut World, dt: f32) { | |
| let table_indices = query_tables( | |
| &mut world.query_cache, &world.tables, POSITION | VELOCITY, | |
| ).to_vec(); | |
| for ti in table_indices { | |
| let t = &mut world.tables[ti]; | |
| for i in 0..t.entities.len() { | |
| t.positions[i].x += t.velocities[i].dx * dt; | |
| t.positions[i].y += t.velocities[i].dy * dt; | |
| } | |
| } | |
| } | |
| fn sunlight_system(world: &mut World, dt: f32) { | |
| let table_indices = query_tables( | |
| &mut world.query_cache, &world.tables, VITALITY, | |
| ).to_vec(); | |
| for ti in table_indices { | |
| let t = &mut world.tables[ti]; | |
| for v in t.vitalities.iter_mut() { | |
| v.hp = (v.hp + 5.0 * dt).min(100.0); | |
| } | |
| } | |
| } | |
| // -- Demo --------------------------------------------------------------------- | |
| fn main() { | |
| let mut world = World::default(); | |
| // -- Generational handle safety ------------------------------------------- | |
| println!("=== generational handle safety ==="); | |
| let drifting_seed = spawn(&mut world, POSITION | VELOCITY); | |
| get_velocity_mut(&mut world, drifting_seed).unwrap().dx = 10.0; | |
| despawn(&mut world, drifting_seed); | |
| let new_seedling = spawn(&mut world, POSITION); | |
| println!("drifting_seed live? {}", is_live(&world.generations, drifting_seed)); // false | |
| println!("new_seedling live? {}", is_live(&world.generations, new_seedling)); // true | |
| println!("stale lookup: {:?}", get_position(&world, drifting_seed)); // None | |
| // -- Sprouting: structural change via the edge graph ---------------------- | |
| println!("\n=== sprouting (single-bit edge cache) ==="); | |
| let shrub = spawn(&mut world, POSITION | VITALITY); | |
| get_vitality_mut(&mut world, shrub).unwrap().hp = 50.0; | |
| add_components(&mut world, shrub, VELOCITY); | |
| get_velocity_mut(&mut world, shrub).unwrap().dx = 3.0; | |
| println!("runner extended -- vel: {:?} vitality: {:?}", | |
| get_velocity_mut(&mut world, shrub).map(|v| (v.dx, v.dy)), | |
| get_vitality_mut(&mut world, shrub).map(|v| v.hp)); | |
| remove_components(&mut world, shrub, VELOCITY); | |
| println!("runner rooted -- vel: {:?} vitality: {:?}", | |
| get_velocity_mut(&mut world, shrub).map(|v| (v.dx, v.dy)), | |
| get_vitality_mut(&mut world, shrub).map(|v| v.hp)); | |
| add_components(&mut world, shrub, VELOCITY); | |
| println!("second runner (array hit) -- vel: {:?}", | |
| get_velocity_mut(&mut world, shrub).map(|v| (v.dx, v.dy))); | |
| // -- Multi-bit transition ------------------------------------------------- | |
| println!("\n=== multi-bit transition (delta-keyed cache) ==="); | |
| let seed = spawn(&mut world, POSITION); | |
| add_components(&mut world, seed, VELOCITY | VITALITY); | |
| get_velocity_mut(&mut world, seed).unwrap().dx = 1.0; | |
| get_vitality_mut(&mut world, seed).unwrap().hp = 30.0; | |
| println!("sprouted seed -- vel: {:?} vitality: {:?}", | |
| get_velocity_mut(&mut world, seed).map(|v| (v.dx, v.dy)), | |
| get_vitality_mut(&mut world, seed).map(|v| v.hp)); | |
| let seed2 = spawn(&mut world, POSITION); | |
| add_components(&mut world, seed2, VELOCITY | VITALITY); | |
| println!("second sprout (map hit) -- vel: {:?}", | |
| get_velocity_mut(&mut world, seed2).map(|v| (v.dx, v.dy))); | |
| // -- Query cache ---------------------------------------------------------- | |
| println!("\n=== query cache ==="); | |
| let seedling = spawn(&mut world, POSITION | VELOCITY | VITALITY); | |
| get_velocity_mut(&mut world, seedling).unwrap().dy = 2.0; | |
| get_vitality_mut(&mut world, seedling).unwrap().hp = 80.0; | |
| let first = query_tables(&mut world.query_cache, &world.tables, POSITION | VELOCITY).to_vec(); | |
| let second = query_tables(&mut world.query_cache, &world.tables, POSITION | VELOCITY).to_vec(); | |
| println!("tables matching POS|VEL (both calls): {:?} == {:?}", first, second); | |
| // -- Tick: wind and sunlight ---------------------------------------------- | |
| println!("\n=== tick (dt=1.0) ==="); | |
| wind_system(&mut world, 1.0); | |
| sunlight_system(&mut world, 1.0); | |
| println!("seedling pos: {:?}", get_position(&world, seedling)); | |
| println!("seedling vitality: {:?}", get_vitality_mut(&mut world, seedling).map(|v| v.hp)); | |
| println!("shrub vitality: {:?}", get_vitality_mut(&mut world, shrub).map(|v| v.hp)); | |
| println!("seed vitality: {:?}", get_vitality_mut(&mut world, seed).map(|v| v.hp)); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment