Created
July 19, 2025 15:01
-
-
Save matthewjberger/35296e570bb1813abe51e8798910f90e to your computer and use it in GitHub Desktop.
freecs expansion, showing how the underlying ecs works
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 ecs::*; | |
| mod ecs { | |
| use super::components::*; | |
| #[derive( | |
| Default, Clone, Copy, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize, | |
| )] | |
| pub struct Entity { | |
| pub id: u32, | |
| pub generation: u32, | |
| } | |
| impl std::fmt::Display for Entity { | |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| let Self { id, generation } = self; | |
| write!(f, "Id: {id} - Generation: {generation}") | |
| } | |
| } | |
| #[allow(unused)] | |
| #[derive(Default, Debug, Clone)] | |
| pub struct EntityBuilder { | |
| position: Option<Position>, | |
| velocity: Option<Velocity>, | |
| health: Option<Health>, | |
| } | |
| #[allow(unused)] | |
| impl EntityBuilder { | |
| pub fn new() -> Self { | |
| Self::default() | |
| } | |
| pub fn with_position(&mut self, value: Position) -> &mut Self { | |
| self.position = Some(value); | |
| self | |
| } | |
| pub fn with_velocity(&mut self, value: Velocity) -> &mut Self { | |
| self.velocity = Some(value); | |
| self | |
| } | |
| pub fn with_health(&mut self, value: Health) -> &mut Self { | |
| self.health = Some(value); | |
| self | |
| } | |
| pub fn spawn(&self, world: &mut World, instances: usize) -> Vec<Entity> { | |
| let mut mask = 0; | |
| if self.position.is_some() { | |
| mask |= POSITION; | |
| } | |
| if self.velocity.is_some() { | |
| mask |= VELOCITY; | |
| } | |
| if self.health.is_some() { | |
| mask |= HEALTH; | |
| } | |
| let entities = world.spawn_entities(mask, instances); | |
| for entity in entities.iter() { | |
| if let Some(component) = self.position.clone() { | |
| world.set_position(*entity, component); | |
| } | |
| if let Some(component) = self.velocity.clone() { | |
| world.set_velocity(*entity, component); | |
| } | |
| if let Some(component) = self.health.clone() { | |
| world.set_health(*entity, component); | |
| } | |
| } | |
| entities | |
| } | |
| } | |
| #[repr(u64)] | |
| #[allow(clippy::upper_case_acronyms)] | |
| #[allow(non_camel_case_types)] | |
| pub enum Component { | |
| POSITION, | |
| VELOCITY, | |
| HEALTH, | |
| } | |
| pub const POSITION: u64 = 1 << (Component::POSITION as u64); | |
| pub const VELOCITY: u64 = 1 << (Component::VELOCITY as u64); | |
| pub const HEALTH: u64 = 1 << (Component::HEALTH as u64); | |
| pub const COMPONENT_COUNT: usize = { | |
| let mut count = 0; | |
| count += 1; | |
| let _ = Component::POSITION; | |
| count += 1; | |
| let _ = Component::VELOCITY; | |
| count += 1; | |
| let _ = Component::HEALTH; | |
| count | |
| }; | |
| #[derive(Default)] | |
| pub struct EntityAllocator { | |
| next_id: u32, | |
| free_ids: Vec<(u32, u32)>, | |
| } | |
| #[derive(Copy, Clone, Default)] | |
| struct EntityLocation { | |
| generation: u32, | |
| table_index: u32, | |
| array_index: u32, | |
| allocated: bool, | |
| } | |
| #[derive(Default)] | |
| pub struct EntityLocations { | |
| locations: Vec<EntityLocation>, | |
| } | |
| #[derive(Default)] | |
| #[allow(unused)] | |
| pub struct World { | |
| entity_locations: EntityLocations, | |
| tables: Vec<ComponentArrays>, | |
| allocator: EntityAllocator, | |
| pub resources: Resources, | |
| table_edges: Vec<TableEdges>, | |
| table_lookup: std::collections::HashMap<u64, usize>, | |
| } | |
| #[allow(unused)] | |
| impl World { | |
| #[inline] | |
| pub fn get_position(&self, entity: Entity) -> Option<&Position> { | |
| let (table_index, array_index) = get_location(&self.entity_locations, entity)?; | |
| if !self.entity_locations.locations[entity.id as usize].allocated { | |
| return None; | |
| } | |
| let table = &self.tables[table_index]; | |
| if table.mask & POSITION == 0 { | |
| return None; | |
| } | |
| Some(&table.position[array_index]) | |
| } | |
| #[inline] | |
| pub fn get_position_mut(&mut self, entity: Entity) -> Option<&mut Position> { | |
| let (table_index, array_index) = get_location(&self.entity_locations, entity)?; | |
| let table = &mut self.tables[table_index]; | |
| if table.mask & POSITION == 0 { | |
| return None; | |
| } | |
| Some(&mut table.position[array_index]) | |
| } | |
| #[inline] | |
| pub fn entity_has_position(&self, entity: Entity) -> bool { | |
| self.entity_has_components(entity, POSITION) | |
| } | |
| #[inline] | |
| pub fn set_position(&mut self, entity: Entity, value: Position) { | |
| if let Some((table_index, array_index)) = get_location(&self.entity_locations, entity) { | |
| if self.entity_locations.locations[entity.id as usize].allocated { | |
| let table = &mut self.tables[table_index]; | |
| if table.mask & POSITION != 0 { | |
| table.position[array_index] = value; | |
| return; | |
| } | |
| } | |
| } | |
| self.add_components(entity, POSITION); | |
| if let Some((table_index, array_index)) = get_location(&self.entity_locations, entity) { | |
| self.tables[table_index].position[array_index] = value; | |
| } | |
| } | |
| #[inline] | |
| pub fn add_position(&mut self, entity: Entity) { | |
| self.add_components(entity, POSITION); | |
| } | |
| #[inline] | |
| pub fn remove_position(&mut self, entity: Entity) -> bool { | |
| self.remove_components(entity, POSITION) | |
| } | |
| #[inline] | |
| pub fn get_velocity(&self, entity: Entity) -> Option<&Velocity> { | |
| let (table_index, array_index) = get_location(&self.entity_locations, entity)?; | |
| if !self.entity_locations.locations[entity.id as usize].allocated { | |
| return None; | |
| } | |
| let table = &self.tables[table_index]; | |
| if table.mask & VELOCITY == 0 { | |
| return None; | |
| } | |
| Some(&table.velocity[array_index]) | |
| } | |
| #[inline] | |
| pub fn get_velocity_mut(&mut self, entity: Entity) -> Option<&mut Velocity> { | |
| let (table_index, array_index) = get_location(&self.entity_locations, entity)?; | |
| let table = &mut self.tables[table_index]; | |
| if table.mask & VELOCITY == 0 { | |
| return None; | |
| } | |
| Some(&mut table.velocity[array_index]) | |
| } | |
| #[inline] | |
| pub fn entity_has_velocity(&self, entity: Entity) -> bool { | |
| self.entity_has_components(entity, VELOCITY) | |
| } | |
| #[inline] | |
| pub fn set_velocity(&mut self, entity: Entity, value: Velocity) { | |
| if let Some((table_index, array_index)) = get_location(&self.entity_locations, entity) { | |
| if self.entity_locations.locations[entity.id as usize].allocated { | |
| let table = &mut self.tables[table_index]; | |
| if table.mask & VELOCITY != 0 { | |
| table.velocity[array_index] = value; | |
| return; | |
| } | |
| } | |
| } | |
| self.add_components(entity, VELOCITY); | |
| if let Some((table_index, array_index)) = get_location(&self.entity_locations, entity) { | |
| self.tables[table_index].velocity[array_index] = value; | |
| } | |
| } | |
| #[inline] | |
| pub fn add_velocity(&mut self, entity: Entity) { | |
| self.add_components(entity, VELOCITY); | |
| } | |
| #[inline] | |
| pub fn remove_velocity(&mut self, entity: Entity) -> bool { | |
| self.remove_components(entity, VELOCITY) | |
| } | |
| #[inline] | |
| pub fn get_health(&self, entity: Entity) -> Option<&Health> { | |
| let (table_index, array_index) = get_location(&self.entity_locations, entity)?; | |
| if !self.entity_locations.locations[entity.id as usize].allocated { | |
| return None; | |
| } | |
| let table = &self.tables[table_index]; | |
| if table.mask & HEALTH == 0 { | |
| return None; | |
| } | |
| Some(&table.health[array_index]) | |
| } | |
| #[inline] | |
| pub fn get_health_mut(&mut self, entity: Entity) -> Option<&mut Health> { | |
| let (table_index, array_index) = get_location(&self.entity_locations, entity)?; | |
| let table = &mut self.tables[table_index]; | |
| if table.mask & HEALTH == 0 { | |
| return None; | |
| } | |
| Some(&mut table.health[array_index]) | |
| } | |
| #[inline] | |
| pub fn entity_has_health(&self, entity: Entity) -> bool { | |
| self.entity_has_components(entity, HEALTH) | |
| } | |
| #[inline] | |
| pub fn set_health(&mut self, entity: Entity, value: Health) { | |
| if let Some((table_index, array_index)) = get_location(&self.entity_locations, entity) { | |
| if self.entity_locations.locations[entity.id as usize].allocated { | |
| let table = &mut self.tables[table_index]; | |
| if table.mask & HEALTH != 0 { | |
| table.health[array_index] = value; | |
| return; | |
| } | |
| } | |
| } | |
| self.add_components(entity, HEALTH); | |
| if let Some((table_index, array_index)) = get_location(&self.entity_locations, entity) { | |
| self.tables[table_index].health[array_index] = value; | |
| } | |
| } | |
| #[inline] | |
| pub fn add_health(&mut self, entity: Entity) { | |
| self.add_components(entity, HEALTH); | |
| } | |
| #[inline] | |
| pub fn remove_health(&mut self, entity: Entity) -> bool { | |
| self.remove_components(entity, HEALTH) | |
| } | |
| pub fn spawn_entities(&mut self, mask: u64, count: usize) -> Vec<Entity> { | |
| let mut entities = Vec::with_capacity(count); | |
| let table_index = get_or_create_table(self, mask); | |
| self.tables[table_index].entity_indices.reserve(count); | |
| if mask & POSITION != 0 { | |
| self.tables[table_index].position.reserve(count); | |
| } | |
| if mask & VELOCITY != 0 { | |
| self.tables[table_index].velocity.reserve(count); | |
| } | |
| if mask & HEALTH != 0 { | |
| self.tables[table_index].health.reserve(count); | |
| } | |
| for _ in 0..count { | |
| let entity = create_entity(self); | |
| add_to_table( | |
| &mut self.tables[table_index], | |
| entity, | |
| ( | |
| if mask & POSITION != 0 { | |
| Some(<Position>::default()) | |
| } else { | |
| None | |
| }, | |
| if mask & VELOCITY != 0 { | |
| Some(<Velocity>::default()) | |
| } else { | |
| None | |
| }, | |
| if mask & HEALTH != 0 { | |
| Some(<Health>::default()) | |
| } else { | |
| None | |
| }, | |
| ), | |
| ); | |
| entities.push(entity); | |
| insert_location( | |
| &mut self.entity_locations, | |
| entity, | |
| ( | |
| table_index, | |
| self.tables[table_index].entity_indices.len() - 1, | |
| ), | |
| ); | |
| } | |
| entities | |
| } | |
| pub fn query_entities(&self, mask: u64) -> Vec<Entity> { | |
| let total_capacity = self | |
| .tables | |
| .iter() | |
| .filter(|table| table.mask & mask == mask) | |
| .map(|table| table.entity_indices.len()) | |
| .sum(); | |
| let mut result = Vec::with_capacity(total_capacity); | |
| for table in &self.tables { | |
| if table.mask & mask == mask { | |
| result.extend( | |
| table | |
| .entity_indices | |
| .iter() | |
| .copied() | |
| .filter(|&e| self.entity_locations.locations[e.id as usize].allocated), | |
| ); | |
| } | |
| } | |
| result | |
| } | |
| pub fn query_first_entity(&self, mask: u64) -> Option<Entity> { | |
| for table in &self.tables { | |
| if !table.mask & mask == mask { | |
| continue; | |
| } | |
| let indices = table | |
| .entity_indices | |
| .iter() | |
| .copied() | |
| .filter(|&e| self.entity_locations.locations[e.id as usize].allocated) | |
| .collect::<Vec<_>>(); | |
| if let Some(entity) = indices.first() { | |
| return Some(*entity); | |
| } | |
| } | |
| None | |
| } | |
| pub fn despawn_entities(&mut self, entities: &[Entity]) -> Vec<Entity> { | |
| let mut despawned = Vec::with_capacity(entities.len()); | |
| let mut tables_to_update = Vec::new(); | |
| for &entity in entities { | |
| let id = entity.id as usize; | |
| if id < self.entity_locations.locations.len() { | |
| let loc = &mut self.entity_locations.locations[id]; | |
| if loc.allocated && loc.generation == entity.generation { | |
| let table_idx = loc.table_index as usize; | |
| let array_idx = loc.array_index as usize; | |
| loc.allocated = false; | |
| loc.generation = loc.generation.wrapping_add(1); | |
| self.allocator.free_ids.push((entity.id, loc.generation)); | |
| tables_to_update.push((table_idx, array_idx)); | |
| despawned.push(entity); | |
| } | |
| } | |
| } | |
| for (table_idx, array_idx) in tables_to_update.into_iter().rev() { | |
| if table_idx >= self.tables.len() { | |
| continue; | |
| } | |
| let table = &mut self.tables[table_idx]; | |
| let last_idx = table.entity_indices.len() - 1; | |
| if array_idx < last_idx { | |
| let moved_entity = table.entity_indices[last_idx]; | |
| if let Some(loc) = self | |
| .entity_locations | |
| .locations | |
| .get_mut(moved_entity.id as usize) | |
| { | |
| if loc.allocated { | |
| loc.array_index = array_idx as u32; | |
| } | |
| } | |
| } | |
| if table.mask & POSITION != 0 { | |
| table.position.swap_remove(array_idx); | |
| } | |
| if table.mask & VELOCITY != 0 { | |
| table.velocity.swap_remove(array_idx); | |
| } | |
| if table.mask & HEALTH != 0 { | |
| table.health.swap_remove(array_idx); | |
| } | |
| table.entity_indices.swap_remove(array_idx); | |
| } | |
| despawned | |
| } | |
| pub fn add_components(&mut self, entity: Entity, mask: u64) -> bool { | |
| if let Some((table_index, array_index)) = get_location(&self.entity_locations, entity) { | |
| let current_mask = self.tables[table_index].mask; | |
| if current_mask & mask == mask { | |
| return true; | |
| } | |
| let target_table = if mask.count_ones() == 1 { | |
| get_component_index(mask) | |
| .and_then(|idx| self.table_edges[table_index].add_edges[idx]) | |
| } else { | |
| None | |
| }; | |
| let new_table_index = | |
| target_table.unwrap_or_else(|| get_or_create_table(self, current_mask | mask)); | |
| move_entity(self, entity, table_index, array_index, new_table_index); | |
| true | |
| } else { | |
| false | |
| } | |
| } | |
| pub fn remove_components(&mut self, entity: Entity, mask: u64) -> bool { | |
| if let Some((table_index, array_index)) = get_location(&self.entity_locations, entity) { | |
| let current_mask = self.tables[table_index].mask; | |
| if current_mask & mask == 0 { | |
| return true; | |
| } | |
| let target_table = if mask.count_ones() == 1 { | |
| get_component_index(mask) | |
| .and_then(|idx| self.table_edges[table_index].remove_edges[idx]) | |
| } else { | |
| None | |
| }; | |
| let new_table_index = | |
| target_table.unwrap_or_else(|| get_or_create_table(self, current_mask & !mask)); | |
| move_entity(self, entity, table_index, array_index, new_table_index); | |
| true | |
| } else { | |
| false | |
| } | |
| } | |
| pub fn component_mask(&self, entity: Entity) -> Option<u64> { | |
| get_location(&self.entity_locations, entity) | |
| .map(|(table_index, _)| self.tables[table_index].mask) | |
| } | |
| pub fn get_all_entities(&self) -> Vec<Entity> { | |
| let mut result = Vec::new(); | |
| for table in &self.tables { | |
| result.extend( | |
| table | |
| .entity_indices | |
| .iter() | |
| .copied() | |
| .filter(|&e| self.entity_locations.locations[e.id as usize].allocated), | |
| ); | |
| } | |
| result | |
| } | |
| pub fn entity_has_components(&self, entity: Entity, components: u64) -> bool { | |
| self.component_mask(entity).unwrap_or(0) & components != 0 | |
| } | |
| } | |
| #[derive(Default)] | |
| pub struct Resources { | |
| pub delta_time: f32, | |
| } | |
| #[derive(Default)] | |
| pub struct ComponentArrays { | |
| pub position: Vec<Position>, | |
| pub velocity: Vec<Velocity>, | |
| pub health: Vec<Health>, | |
| pub entity_indices: Vec<Entity>, | |
| pub mask: u64, | |
| } | |
| #[derive(Copy, Clone)] | |
| struct TableEdges { | |
| add_edges: [Option<usize>; COMPONENT_COUNT], | |
| remove_edges: [Option<usize>; COMPONENT_COUNT], | |
| } | |
| impl Default for TableEdges { | |
| fn default() -> Self { | |
| Self { | |
| add_edges: [None; COMPONENT_COUNT], | |
| remove_edges: [None; COMPONENT_COUNT], | |
| } | |
| } | |
| } | |
| fn get_component_index(mask: u64) -> Option<usize> { | |
| match mask { | |
| POSITION => Some(Component::POSITION as _), | |
| VELOCITY => Some(Component::VELOCITY as _), | |
| HEALTH => Some(Component::HEALTH as _), | |
| _ => None, | |
| } | |
| } | |
| fn remove_from_table(arrays: &mut ComponentArrays, index: usize) -> Option<Entity> { | |
| let last_index = arrays.entity_indices.len() - 1; | |
| let mut swapped_entity = None; | |
| if index < last_index { | |
| swapped_entity = Some(arrays.entity_indices[last_index]); | |
| } | |
| if arrays.mask & POSITION != 0 { | |
| arrays.position.swap_remove(index); | |
| } | |
| if arrays.mask & VELOCITY != 0 { | |
| arrays.velocity.swap_remove(index); | |
| } | |
| if arrays.mask & HEALTH != 0 { | |
| arrays.health.swap_remove(index); | |
| } | |
| arrays.entity_indices.swap_remove(index); | |
| swapped_entity | |
| } | |
| fn move_entity( | |
| world: &mut World, | |
| entity: Entity, | |
| from_table: usize, | |
| from_index: usize, | |
| to_table: usize, | |
| ) { | |
| let components = { | |
| let from_table_ref = &mut world.tables[from_table]; | |
| ( | |
| if from_table_ref.mask & POSITION != 0 { | |
| Some(std::mem::take(&mut from_table_ref.position[from_index])) | |
| } else { | |
| None | |
| }, | |
| if from_table_ref.mask & VELOCITY != 0 { | |
| Some(std::mem::take(&mut from_table_ref.velocity[from_index])) | |
| } else { | |
| None | |
| }, | |
| if from_table_ref.mask & HEALTH != 0 { | |
| Some(std::mem::take(&mut from_table_ref.health[from_index])) | |
| } else { | |
| None | |
| }, | |
| ) | |
| }; | |
| add_to_table(&mut world.tables[to_table], entity, components); | |
| let new_index = world.tables[to_table].entity_indices.len() - 1; | |
| insert_location(&mut world.entity_locations, entity, (to_table, new_index)); | |
| if let Some(swapped) = remove_from_table(&mut world.tables[from_table], from_index) { | |
| insert_location( | |
| &mut world.entity_locations, | |
| swapped, | |
| (from_table, from_index), | |
| ); | |
| } | |
| } | |
| fn get_location(locations: &EntityLocations, entity: Entity) -> Option<(usize, usize)> { | |
| let id = entity.id as usize; | |
| if id >= locations.locations.len() { | |
| return None; | |
| } | |
| let location = &locations.locations[id]; | |
| if !location.allocated || location.generation != entity.generation { | |
| return None; | |
| } | |
| Some((location.table_index as usize, location.array_index as usize)) | |
| } | |
| fn insert_location(locations: &mut EntityLocations, entity: Entity, location: (usize, usize)) { | |
| let id = entity.id as usize; | |
| if id >= locations.locations.len() { | |
| locations | |
| .locations | |
| .resize(id + 1, EntityLocation::default()); | |
| } | |
| locations.locations[id] = EntityLocation { | |
| generation: entity.generation, | |
| table_index: location.0 as u32, | |
| array_index: location.1 as u32, | |
| allocated: true, | |
| }; | |
| } | |
| fn create_entity(world: &mut World) -> Entity { | |
| if let Some((id, next_gen)) = world.allocator.free_ids.pop() { | |
| let id_usize = id as usize; | |
| if id_usize >= world.entity_locations.locations.len() { | |
| world.entity_locations.locations.resize( | |
| (world.entity_locations.locations.len() * 2).max(64), | |
| EntityLocation::default(), | |
| ); | |
| } | |
| world.entity_locations.locations[id_usize].generation = next_gen; | |
| Entity { | |
| id, | |
| generation: next_gen, | |
| } | |
| } else { | |
| let id = world.allocator.next_id; | |
| world.allocator.next_id += 1; | |
| let id_usize = id as usize; | |
| if id_usize >= world.entity_locations.locations.len() { | |
| world.entity_locations.locations.resize( | |
| (world.entity_locations.locations.len() * 2).max(64), | |
| EntityLocation::default(), | |
| ); | |
| } | |
| Entity { id, generation: 0 } | |
| } | |
| } | |
| fn add_to_table( | |
| arrays: &mut ComponentArrays, | |
| entity: Entity, | |
| components: (Option<Position>, Option<Velocity>, Option<Health>), | |
| ) { | |
| let (position, velocity, health) = components; | |
| if arrays.mask & POSITION != 0 { | |
| if let Some(component) = position { | |
| arrays.position.push(component); | |
| } else { | |
| arrays.position.push(<Position>::default()); | |
| } | |
| } | |
| if arrays.mask & VELOCITY != 0 { | |
| if let Some(component) = velocity { | |
| arrays.velocity.push(component); | |
| } else { | |
| arrays.velocity.push(<Velocity>::default()); | |
| } | |
| } | |
| if arrays.mask & HEALTH != 0 { | |
| if let Some(component) = health { | |
| arrays.health.push(component); | |
| } else { | |
| arrays.health.push(<Health>::default()); | |
| } | |
| } | |
| arrays.entity_indices.push(entity); | |
| } | |
| fn get_or_create_table(world: &mut World, mask: u64) -> usize { | |
| if let Some(&index) = world.table_lookup.get(&mask) { | |
| return index; | |
| } | |
| let table_index = world.tables.len(); | |
| world.tables.push(ComponentArrays { | |
| mask, | |
| ..Default::default() | |
| }); | |
| world.table_edges.push(TableEdges::default()); | |
| world.table_lookup.insert(mask, table_index); | |
| for comp_mask in [POSITION, VELOCITY, HEALTH] { | |
| if let Some(comp_idx) = get_component_index(comp_mask) { | |
| for (idx, table) in world.tables.iter().enumerate() { | |
| if table.mask | comp_mask == mask { | |
| world.table_edges[idx].add_edges[comp_idx] = Some(table_index); | |
| } | |
| if table.mask & !comp_mask == mask { | |
| world.table_edges[idx].remove_edges[comp_idx] = Some(table_index); | |
| } | |
| } | |
| } | |
| } | |
| table_index | |
| } | |
| } | |
| pub fn main() { | |
| let mut world = World::default(); | |
| // Spawn entities with components | |
| let _entity = world.spawn_entities(POSITION | VELOCITY, 1)[0]; | |
| // Or use the entity builder | |
| let entity = EntityBuilder::new() | |
| .with_position(Position { x: 1.0, y: 2.0 }) | |
| .spawn(&mut world, 1)[0]; | |
| // Read components using the generated methods | |
| let position = world.get_position(entity); | |
| println!("Position: {:?}", position); | |
| // Set components (adds if not present) | |
| world.set_position(entity, Position { x: 1.0, y: 2.0 }); | |
| // Mutate a component | |
| if let Some(position) = world.get_position_mut(entity) { | |
| position.x += 1.0; | |
| } | |
| // Get an entity's component mask | |
| let _component_mask = world.component_mask(entity).unwrap(); | |
| // Add a new component to an entity | |
| world.add_components(entity, HEALTH); | |
| // Or use the generated add method | |
| world.add_health(entity); | |
| // Query all entities | |
| let _entities = world.get_all_entities(); | |
| // Query all entities with a specific component | |
| let _players = world.query_entities(POSITION | VELOCITY | HEALTH); | |
| // Query the first entity with a specific component, | |
| // returning early instead of checking remaining entities | |
| let _first_player_entity = world.query_first_entity(POSITION | VELOCITY | HEALTH); | |
| // Remove a component from an entity | |
| world.remove_components(entity, HEALTH); | |
| // Or use the generated remove method | |
| world.remove_health(entity); | |
| // Check if entity has components | |
| if world.entity_has_position(entity) { | |
| println!("Entity has position component"); | |
| } | |
| // Systems are functions that transform component data | |
| systems::run_systems(&mut world); | |
| // Despawn entities, freeing their table slots for reuse | |
| world.despawn_entities(&[entity]); | |
| } | |
| use components::*; | |
| mod components { | |
| #[derive(Default, Debug, Clone, Copy)] | |
| pub struct Position { | |
| pub x: f32, | |
| pub y: f32, | |
| } | |
| #[derive(Default, Debug, Clone, Copy)] | |
| pub struct Velocity { | |
| pub x: f32, | |
| pub y: f32, | |
| } | |
| #[derive(Default, Debug, Clone, Copy)] | |
| pub struct Health { | |
| pub value: f32, | |
| } | |
| } | |
| mod systems { | |
| use super::*; | |
| pub fn run_systems(world: &mut World) { | |
| // Systems use queries and component accessors | |
| example_system(world); | |
| update_positions_system(world); | |
| health_system(world); | |
| } | |
| fn example_system(world: &mut World) { | |
| for entity in world.query_entities(POSITION | VELOCITY) { | |
| if let Some(position) = world.get_position_mut(entity) { | |
| position.x += 1.0; | |
| } | |
| } | |
| } | |
| fn update_positions_system(world: &mut World) { | |
| let dt = world.resources.delta_time; | |
| // Collect entities with their velocities first to avoid borrow conflicts | |
| let updates: Vec<(Entity, Velocity)> = world | |
| .query_entities(POSITION | VELOCITY) | |
| .into_iter() | |
| .filter_map(|entity| world.get_velocity(entity).map(|vel| (entity, *vel))) | |
| .collect(); | |
| // Now update positions | |
| for (entity, vel) in updates { | |
| if let Some(pos) = world.get_position_mut(entity) { | |
| pos.x += vel.x * dt; | |
| pos.y += vel.y * dt; | |
| } | |
| } | |
| } | |
| fn health_system(world: &mut World) { | |
| for entity in world.query_entities(HEALTH) { | |
| if let Some(health) = world.get_health_mut(entity) { | |
| health.value *= 0.98; | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment