Created
May 16, 2026 03:04
-
-
Save matthewjberger/4578b6d03514523f5f345951c7395b40 to your computer and use it in GitHub Desktop.
Build your own ECS in Rust, part 3: change detection, events, tags, commands
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, HashSet}; | |
| #[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; | |
| pub const COMPONENT_COUNT: usize = 2; | |
| pub fn component_index(mask: u64) -> Option<usize> { | |
| match mask { | |
| POSITION => Some(0), | |
| VELOCITY => Some(1), | |
| _ => None, | |
| } | |
| } | |
| #[derive(Default)] | |
| pub struct ComponentArrays { | |
| pub mask: u64, | |
| pub entities: Vec<Entity>, | |
| pub positions: Vec<Position>, | |
| pub positions_changed: Vec<u32>, | |
| pub velocities: Vec<Velocity>, | |
| pub velocities_changed: Vec<u32>, | |
| } | |
| #[derive(Default, Clone)] | |
| pub struct TableEdges { | |
| pub add_edges: [Option<usize>; COMPONENT_COUNT], | |
| pub remove_edges: [Option<usize>; COMPONENT_COUNT], | |
| } | |
| #[derive(Clone)] | |
| pub struct EventQueue<T> { | |
| pub current: Vec<T>, | |
| pub previous: Vec<T>, | |
| } | |
| impl<T> Default for EventQueue<T> { | |
| fn default() -> Self { | |
| Self { | |
| current: Vec::new(), | |
| previous: Vec::new(), | |
| } | |
| } | |
| } | |
| impl<T> EventQueue<T> { | |
| pub fn send(&mut self, event: T) { | |
| self.current.push(event); | |
| } | |
| pub fn read(&self) -> impl Iterator<Item = &T> { | |
| self.previous.iter().chain(self.current.iter()) | |
| } | |
| pub fn drain(&mut self) -> impl Iterator<Item = T> + '_ { | |
| self.previous.drain(..).chain(self.current.drain(..)) | |
| } | |
| pub fn update(&mut self) { | |
| self.previous.clear(); | |
| std::mem::swap(&mut self.current, &mut self.previous); | |
| } | |
| pub fn len(&self) -> usize { | |
| self.current.len() + self.previous.len() | |
| } | |
| pub fn is_empty(&self) -> bool { | |
| self.current.is_empty() && self.previous.is_empty() | |
| } | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct CollisionEvent { | |
| pub entity_a: Entity, | |
| pub entity_b: Entity, | |
| } | |
| pub enum Command { | |
| Spawn { mask: u64 }, | |
| Despawn { entity: Entity }, | |
| AddComponents { entity: Entity, mask: u64 }, | |
| RemoveComponents { entity: Entity, mask: u64 }, | |
| SetPosition { entity: Entity, value: Position }, | |
| SetVelocity { entity: Entity, value: Velocity }, | |
| AddPlayer { entity: Entity }, | |
| RemovePlayer { entity: Entity }, | |
| AddEnemy { entity: Entity }, | |
| RemoveEnemy { entity: Entity }, | |
| } | |
| #[derive(Default)] | |
| pub struct World { | |
| pub allocator: EntityAllocator, | |
| pub entity_locations: EntityLocations, | |
| pub tables: Vec<ComponentArrays>, | |
| pub table_lookup: HashMap<u64, usize>, | |
| pub table_edges: Vec<TableEdges>, | |
| pub query_cache: HashMap<u64, Vec<usize>>, | |
| pub current_tick: u32, | |
| pub last_tick: u32, | |
| pub collisions: EventQueue<CollisionEvent>, | |
| pub players: HashSet<Entity>, | |
| pub enemies: HashSet<Entity>, | |
| pub command_buffer: Vec<Command>, | |
| } | |
| impl World { | |
| pub fn invalidate_query_cache_for_new_table( | |
| &mut self, | |
| new_mask: u64, | |
| new_table_index: usize, | |
| ) { | |
| for (query_mask, cached_tables) in self.query_cache.iter_mut() { | |
| if new_mask & query_mask == *query_mask { | |
| cached_tables.push(new_table_index); | |
| } | |
| } | |
| } | |
| pub fn get_or_create_table(&mut self, mask: u64) -> usize { | |
| if let Some(&index) = self.table_lookup.get(&mask) { | |
| return index; | |
| } | |
| let new_table_index = self.tables.len(); | |
| self.tables.push(ComponentArrays { | |
| mask, | |
| ..Default::default() | |
| }); | |
| self.table_edges.push(TableEdges::default()); | |
| self.table_lookup.insert(mask, new_table_index); | |
| self.invalidate_query_cache_for_new_table(mask, new_table_index); | |
| for component_bit in [POSITION, VELOCITY] { | |
| let Some(component_index_value) = component_index(component_bit) else { | |
| continue; | |
| }; | |
| for (existing_index, existing) in self.tables.iter().enumerate() { | |
| if existing.mask | component_bit == mask { | |
| self.table_edges[existing_index].add_edges[component_index_value] = | |
| Some(new_table_index); | |
| } | |
| if existing.mask & !component_bit == mask { | |
| self.table_edges[existing_index].remove_edges[component_index_value] = | |
| Some(new_table_index); | |
| } | |
| } | |
| } | |
| new_table_index | |
| } | |
| pub fn cached_tables(&mut self, mask: u64) -> &[usize] { | |
| if !self.query_cache.contains_key(&mask) { | |
| let matching: Vec<usize> = self | |
| .tables | |
| .iter() | |
| .enumerate() | |
| .filter(|(_, table)| table.mask & mask == mask) | |
| .map(|(index, _)| index) | |
| .collect(); | |
| self.query_cache.insert(mask, matching); | |
| } | |
| &self.query_cache[&mask] | |
| } | |
| pub fn current_tick(&self) -> u32 { | |
| self.current_tick | |
| } | |
| pub fn last_tick(&self) -> u32 { | |
| self.last_tick | |
| } | |
| pub fn step(&mut self) { | |
| self.collisions.update(); | |
| self.last_tick = self.current_tick; | |
| self.current_tick = self.current_tick.wrapping_add(1); | |
| } | |
| pub fn spawn(&mut self, mask: u64) -> Entity { | |
| let entity = self.allocator.allocate(); | |
| let table_index = self.get_or_create_table(mask); | |
| let current_tick = self.current_tick; | |
| 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()); | |
| table.positions_changed.push(current_tick); | |
| } | |
| if mask & VELOCITY != 0 { | |
| table.velocities.push(Velocity::default()); | |
| table.velocities_changed.push(current_tick); | |
| } | |
| 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 current_tick = self.current_tick; | |
| let table = &mut self.tables[table_index]; | |
| if table.mask & POSITION == 0 { | |
| return None; | |
| } | |
| table.positions_changed[array_index] = current_tick; | |
| 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 current_tick = self.current_tick; | |
| let table = &mut self.tables[table_index]; | |
| if table.mask & VELOCITY == 0 { | |
| return None; | |
| } | |
| table.velocities_changed[array_index] = current_tick; | |
| Some(&mut table.velocities[array_index]) | |
| } | |
| pub fn set_position(&mut self, entity: Entity, value: Position) { | |
| let Some((table_index, array_index)) = self.entity_locations.get(entity) else { | |
| return; | |
| }; | |
| let current_tick = self.current_tick; | |
| if self.tables[table_index].mask & POSITION != 0 { | |
| self.tables[table_index].positions[array_index] = value; | |
| self.tables[table_index].positions_changed[array_index] = current_tick; | |
| return; | |
| } | |
| self.add_components(entity, POSITION); | |
| if let Some((table_index, array_index)) = self.entity_locations.get(entity) { | |
| self.tables[table_index].positions[array_index] = value; | |
| self.tables[table_index].positions_changed[array_index] = current_tick; | |
| } | |
| } | |
| pub fn set_velocity(&mut self, entity: Entity, value: Velocity) { | |
| let Some((table_index, array_index)) = self.entity_locations.get(entity) else { | |
| return; | |
| }; | |
| let current_tick = self.current_tick; | |
| if self.tables[table_index].mask & VELOCITY != 0 { | |
| self.tables[table_index].velocities[array_index] = value; | |
| self.tables[table_index].velocities_changed[array_index] = current_tick; | |
| return; | |
| } | |
| self.add_components(entity, VELOCITY); | |
| if let Some((table_index, array_index)) = self.entity_locations.get(entity) { | |
| self.tables[table_index].velocities[array_index] = value; | |
| self.tables[table_index].velocities_changed[array_index] = current_tick; | |
| } | |
| } | |
| pub fn move_entity( | |
| &mut self, | |
| entity: Entity, | |
| from_table: usize, | |
| from_index: usize, | |
| to_table: usize, | |
| ) { | |
| let from_mask = self.tables[from_table].mask; | |
| let current_tick = self.current_tick; | |
| let position = if from_mask & POSITION != 0 { | |
| Some(std::mem::take( | |
| &mut self.tables[from_table].positions[from_index], | |
| )) | |
| } else { | |
| None | |
| }; | |
| let velocity = if from_mask & VELOCITY != 0 { | |
| Some(std::mem::take( | |
| &mut self.tables[from_table].velocities[from_index], | |
| )) | |
| } else { | |
| None | |
| }; | |
| let to_array_index = { | |
| let to = &mut self.tables[to_table]; | |
| let array_index = to.entities.len(); | |
| to.entities.push(entity); | |
| if to.mask & POSITION != 0 { | |
| to.positions.push(position.unwrap_or_default()); | |
| to.positions_changed.push(current_tick); | |
| } | |
| if to.mask & VELOCITY != 0 { | |
| to.velocities.push(velocity.unwrap_or_default()); | |
| to.velocities_changed.push(current_tick); | |
| } | |
| array_index | |
| }; | |
| self.entity_locations.set(entity, to_table, to_array_index); | |
| let from = &mut self.tables[from_table]; | |
| let last_index = from.entities.len() - 1; | |
| let swapped = if from_index < last_index { | |
| Some(from.entities[last_index]) | |
| } else { | |
| None | |
| }; | |
| from.entities.swap_remove(from_index); | |
| if from.mask & POSITION != 0 { | |
| from.positions.swap_remove(from_index); | |
| from.positions_changed.swap_remove(from_index); | |
| } | |
| if from.mask & VELOCITY != 0 { | |
| from.velocities.swap_remove(from_index); | |
| from.velocities_changed.swap_remove(from_index); | |
| } | |
| if let Some(moved) = swapped { | |
| self.entity_locations.set(moved, from_table, from_index); | |
| } | |
| } | |
| pub fn add_components(&mut self, entity: Entity, mask: u64) -> bool { | |
| let Some((table_index, array_index)) = self.entity_locations.get(entity) else { | |
| return false; | |
| }; | |
| let current_mask = self.tables[table_index].mask; | |
| if current_mask & mask == mask { | |
| return true; | |
| } | |
| let cached = if mask.count_ones() == 1 { | |
| component_index(mask) | |
| .and_then(|index| self.table_edges[table_index].add_edges[index]) | |
| } else { | |
| None | |
| }; | |
| let new_table_index = match cached { | |
| Some(index) => index, | |
| None => self.get_or_create_table(current_mask | mask), | |
| }; | |
| self.move_entity(entity, table_index, array_index, new_table_index); | |
| true | |
| } | |
| pub fn remove_components(&mut self, entity: Entity, mask: u64) -> bool { | |
| let Some((table_index, array_index)) = self.entity_locations.get(entity) else { | |
| return false; | |
| }; | |
| let current_mask = self.tables[table_index].mask; | |
| if current_mask & mask == 0 { | |
| return true; | |
| } | |
| let cached = if mask.count_ones() == 1 { | |
| component_index(mask) | |
| .and_then(|index| self.table_edges[table_index].remove_edges[index]) | |
| } else { | |
| None | |
| }; | |
| let new_table_index = match cached { | |
| Some(index) => index, | |
| None => self.get_or_create_table(current_mask & !mask), | |
| }; | |
| self.move_entity(entity, table_index, array_index, new_table_index); | |
| true | |
| } | |
| 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); | |
| self.players.remove(&entity); | |
| self.enemies.remove(&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); | |
| table.positions_changed.swap_remove(array_index); | |
| } | |
| if table.mask & VELOCITY != 0 { | |
| table.velocities.swap_remove(array_index); | |
| table.velocities_changed.swap_remove(array_index); | |
| } | |
| if let Some(moved) = swapped { | |
| self.entity_locations.set(moved, table_index, array_index); | |
| } | |
| true | |
| } | |
| pub fn query_entities(&self, mask: u64) -> impl Iterator<Item = Entity> + '_ { | |
| self.tables | |
| .iter() | |
| .filter(move |table| table.mask & mask == mask) | |
| .flat_map(|table| table.entities.iter().copied()) | |
| } | |
| pub fn for_each<F>(&self, include: u64, exclude: u64, mut f: F) | |
| where | |
| F: FnMut(Entity, &ComponentArrays, usize), | |
| { | |
| for table in &self.tables { | |
| if table.mask & include != include || table.mask & exclude != 0 { | |
| continue; | |
| } | |
| for array_index in 0..table.entities.len() { | |
| let entity = table.entities[array_index]; | |
| f(entity, table, array_index); | |
| } | |
| } | |
| } | |
| pub fn for_each_mut<F>(&mut self, include: u64, exclude: u64, mut f: F) | |
| where | |
| F: FnMut(Entity, &mut ComponentArrays, usize), | |
| { | |
| let table_indices: Vec<usize> = self.cached_tables(include).to_vec(); | |
| for table_index in table_indices { | |
| let table = &mut self.tables[table_index]; | |
| if table.mask & exclude != 0 { | |
| continue; | |
| } | |
| for array_index in 0..table.entities.len() { | |
| let entity = table.entities[array_index]; | |
| f(entity, table, array_index); | |
| } | |
| } | |
| } | |
| pub fn for_each_mut_changed<F>(&mut self, include: u64, exclude: u64, mut f: F) | |
| where | |
| F: FnMut(Entity, &mut ComponentArrays, usize), | |
| { | |
| let since_tick = self.last_tick; | |
| let table_indices: Vec<usize> = self.cached_tables(include).to_vec(); | |
| for table_index in table_indices { | |
| let table = &mut self.tables[table_index]; | |
| if table.mask & exclude != 0 { | |
| continue; | |
| } | |
| for array_index in 0..table.entities.len() { | |
| let mut changed = false; | |
| if include & POSITION != 0 | |
| && table.mask & POSITION != 0 | |
| && table.positions_changed[array_index] > since_tick | |
| { | |
| changed = true; | |
| } | |
| if include & VELOCITY != 0 | |
| && table.mask & VELOCITY != 0 | |
| && table.velocities_changed[array_index] > since_tick | |
| { | |
| changed = true; | |
| } | |
| if changed { | |
| let entity = table.entities[array_index]; | |
| f(entity, table, array_index); | |
| } | |
| } | |
| } | |
| } | |
| pub fn send_collision(&mut self, event: CollisionEvent) { | |
| self.collisions.send(event); | |
| } | |
| pub fn read_collisions(&self) -> impl Iterator<Item = &CollisionEvent> { | |
| self.collisions.read() | |
| } | |
| pub fn drain_collisions(&mut self) -> impl Iterator<Item = CollisionEvent> + '_ { | |
| self.collisions.drain() | |
| } | |
| pub fn add_player(&mut self, entity: Entity) { | |
| if self.entity_locations.get(entity).is_some() { | |
| self.players.insert(entity); | |
| } | |
| } | |
| pub fn remove_player(&mut self, entity: Entity) -> bool { | |
| self.players.remove(&entity) | |
| } | |
| pub fn has_player(&self, entity: Entity) -> bool { | |
| self.players.contains(&entity) | |
| } | |
| pub fn query_players(&self) -> impl Iterator<Item = Entity> + '_ { | |
| self.players.iter().copied() | |
| } | |
| pub fn add_enemy(&mut self, entity: Entity) { | |
| if self.entity_locations.get(entity).is_some() { | |
| self.enemies.insert(entity); | |
| } | |
| } | |
| pub fn remove_enemy(&mut self, entity: Entity) -> bool { | |
| self.enemies.remove(&entity) | |
| } | |
| pub fn has_enemy(&self, entity: Entity) -> bool { | |
| self.enemies.contains(&entity) | |
| } | |
| pub fn query_enemies(&self) -> impl Iterator<Item = Entity> + '_ { | |
| self.enemies.iter().copied() | |
| } | |
| pub fn queue_spawn(&mut self, mask: u64) { | |
| self.command_buffer.push(Command::Spawn { mask }); | |
| } | |
| pub fn queue_despawn(&mut self, entity: Entity) { | |
| self.command_buffer.push(Command::Despawn { entity }); | |
| } | |
| pub fn queue_add_components(&mut self, entity: Entity, mask: u64) { | |
| self.command_buffer | |
| .push(Command::AddComponents { entity, mask }); | |
| } | |
| pub fn queue_remove_components(&mut self, entity: Entity, mask: u64) { | |
| self.command_buffer | |
| .push(Command::RemoveComponents { entity, mask }); | |
| } | |
| pub fn queue_set_position(&mut self, entity: Entity, value: Position) { | |
| self.command_buffer | |
| .push(Command::SetPosition { entity, value }); | |
| } | |
| pub fn queue_set_velocity(&mut self, entity: Entity, value: Velocity) { | |
| self.command_buffer | |
| .push(Command::SetVelocity { entity, value }); | |
| } | |
| pub fn queue_add_player(&mut self, entity: Entity) { | |
| self.command_buffer.push(Command::AddPlayer { entity }); | |
| } | |
| pub fn queue_remove_player(&mut self, entity: Entity) { | |
| self.command_buffer.push(Command::RemovePlayer { entity }); | |
| } | |
| pub fn queue_add_enemy(&mut self, entity: Entity) { | |
| self.command_buffer.push(Command::AddEnemy { entity }); | |
| } | |
| pub fn queue_remove_enemy(&mut self, entity: Entity) { | |
| self.command_buffer.push(Command::RemoveEnemy { entity }); | |
| } | |
| pub fn apply_commands(&mut self) { | |
| let commands = std::mem::take(&mut self.command_buffer); | |
| for command in commands { | |
| match command { | |
| Command::Spawn { mask } => { | |
| self.spawn(mask); | |
| } | |
| Command::Despawn { entity } => { | |
| self.despawn(entity); | |
| } | |
| Command::AddComponents { entity, mask } => { | |
| self.add_components(entity, mask); | |
| } | |
| Command::RemoveComponents { entity, mask } => { | |
| self.remove_components(entity, mask); | |
| } | |
| Command::SetPosition { entity, value } => { | |
| self.set_position(entity, value); | |
| } | |
| Command::SetVelocity { entity, value } => { | |
| self.set_velocity(entity, value); | |
| } | |
| Command::AddPlayer { entity } => { | |
| self.add_player(entity); | |
| } | |
| Command::RemovePlayer { entity } => { | |
| self.remove_player(entity); | |
| } | |
| Command::AddEnemy { entity } => { | |
| self.add_enemy(entity); | |
| } | |
| Command::RemoveEnemy { entity } => { | |
| self.remove_enemy(entity); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| pub type SystemFn = Box<dyn FnMut(&mut World)>; | |
| #[derive(Default)] | |
| pub struct Schedule { | |
| pub systems: Vec<(&'static str, SystemFn)>, | |
| } | |
| impl Schedule { | |
| pub fn add<F>(&mut self, name: &'static str, system: F) -> &mut Self | |
| where | |
| F: FnMut(&mut World) + 'static, | |
| { | |
| self.systems.push((name, Box::new(system))); | |
| self | |
| } | |
| pub fn run(&mut self, world: &mut World) { | |
| for (_, system) in &mut self.systems { | |
| system(world); | |
| } | |
| } | |
| } | |
| fn physics_system(world: &mut World) { | |
| let current_tick = world.current_tick(); | |
| world.for_each_mut(POSITION | VELOCITY, 0, |_entity, table, index| { | |
| table.positions[index].x += table.velocities[index].x; | |
| table.positions[index].y += table.velocities[index].y; | |
| table.positions_changed[index] = current_tick; | |
| }); | |
| } | |
| fn collision_system(world: &mut World) { | |
| let positions: Vec<(Entity, Position)> = world | |
| .query_entities(POSITION) | |
| .filter_map(|entity| world.get_position(entity).map(|position| (entity, position.clone()))) | |
| .collect(); | |
| for index_a in 0..positions.len() { | |
| for index_b in (index_a + 1)..positions.len() { | |
| let (entity_a, position_a) = &positions[index_a]; | |
| let (entity_b, position_b) = &positions[index_b]; | |
| let delta_x = position_a.x - position_b.x; | |
| let delta_y = position_a.y - position_b.y; | |
| if delta_x * delta_x + delta_y * delta_y < 1.0 { | |
| world.send_collision(CollisionEvent { | |
| entity_a: *entity_a, | |
| entity_b: *entity_b, | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| fn collision_reporter(world: &mut World) { | |
| for event in world.drain_collisions() { | |
| println!("collision: {:?} and {:?}", event.entity_a, event.entity_b); | |
| } | |
| } | |
| fn render_changed_system(world: &mut World) { | |
| world.for_each_mut_changed(POSITION, 0, |entity, table, index| { | |
| println!( | |
| "redraw {:?}: ({}, {})", | |
| entity, table.positions[index].x, table.positions[index].y | |
| ); | |
| }); | |
| } | |
| fn main() { | |
| let mut world = World::default(); | |
| let player = world.spawn(POSITION | VELOCITY); | |
| world.set_position(player, Position { x: 0.0, y: 0.0 }); | |
| world.set_velocity(player, Velocity { x: 0.5, y: 0.0 }); | |
| world.add_player(player); | |
| let enemy = world.spawn(POSITION | VELOCITY); | |
| world.set_position(enemy, Position { x: 3.0, y: 0.0 }); | |
| world.set_velocity(enemy, Velocity { x: -0.5, y: 0.0 }); | |
| world.add_enemy(enemy); | |
| let landmark = world.spawn(POSITION); | |
| world.set_position(landmark, Position { x: 10.0, y: 10.0 }); | |
| let mut schedule = Schedule::default(); | |
| schedule | |
| .add("physics", physics_system) | |
| .add("collision", collision_system) | |
| .add("collision_reporter", collision_reporter) | |
| .add("render_changed", render_changed_system); | |
| world.step(); | |
| for frame in 0..4 { | |
| println!("--- frame {frame} ---"); | |
| schedule.run(&mut world); | |
| if frame == 2 { | |
| world.queue_despawn(enemy); | |
| } | |
| world.apply_commands(); | |
| world.step(); | |
| } | |
| println!("--- final ---"); | |
| println!("players: {}", world.query_players().count()); | |
| println!("enemies: {}", world.query_enemies().count()); | |
| println!("entities with position: {}", world.query_entities(POSITION).count()); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment