Last active
November 6, 2024 19:48
-
-
Save matthewjberger/642e74d6b997c34e87c637247815597b to your computer and use it in GitHub Desktop.
Vec<Option<T>> comparison to precomputing entity archetypes
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
impl_world! { | |
positions: Position => POSITION = 0, | |
velocities: Velocity => VELOCITY = 1, | |
gravities: Gravity => GRAVITY = 2, | |
healths: Health => HEALTH = 3, | |
damages: Damage => DAMAGE = 4, | |
} | |
pub fn main() { | |
let mut world = World::default(); | |
let entities = spawn_entities(&mut world, POSITION | VELOCITY | GRAVITY, 4); | |
for (index, &entity) in entities.iter().enumerate() { | |
let index = index as f32; | |
set_components( | |
&mut world, | |
entity, | |
( | |
Some(Position { x: index, y: index }), | |
Some(Velocity { | |
x: index * 0.1, | |
y: index * 0.1, | |
}), | |
Some(Gravity(9.81)), | |
None, | |
Some(Damage(0.0)), | |
), | |
); | |
} | |
let dt = 1.0 / 60.0; | |
systems::run_systems(&mut world, dt); | |
} | |
use components::*; | |
mod components { | |
#[derive(Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize)] | |
pub struct Position { | |
pub x: f32, | |
pub y: f32, | |
} | |
#[derive(Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize)] | |
pub struct Velocity { | |
pub x: f32, | |
pub y: f32, | |
} | |
#[derive(Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize)] | |
pub struct Gravity(pub f32); | |
#[derive(Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize)] | |
pub struct Health(pub f32); | |
#[derive(Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize)] | |
pub struct Damage(pub f32); | |
} | |
mod systems { | |
use crate::*; | |
use rayon::prelude::*; | |
pub fn run_systems(world: &mut World, dt: f32) { | |
// TODO: call this every 60 frames or so | |
// merge_tables(world); | |
world.tables.par_iter_mut().for_each(|table| { | |
if has_components!(table, POSITION | VELOCITY) { | |
movement_system_parallel(&mut table.positions, &table.velocities, dt); | |
} | |
if has_components!(table, VELOCITY | GRAVITY) { | |
gravity_system_parallel(&mut table.velocities, &table.gravities, dt); | |
} | |
if has_components!(table, HEALTH | DAMAGE) { | |
damage_system_parallel(&mut table.healths, &table.damages, dt); | |
} | |
}); | |
} | |
pub fn movement_system_parallel(positions: &mut [Position], velocities: &[Velocity], dt: f32) { | |
positions | |
.par_iter_mut() | |
.zip(velocities.par_iter()) | |
.for_each(|(pos, vel)| { | |
pos.x += vel.x * dt; | |
pos.y += vel.y * dt; | |
}); | |
} | |
pub fn gravity_system_parallel(velocities: &mut [Velocity], gravities: &[Gravity], dt: f32) { | |
velocities | |
.par_iter_mut() | |
.zip(gravities.par_iter()) | |
.for_each(|(vel, gravity)| { | |
vel.y -= gravity.0 * dt; | |
}); | |
} | |
pub fn damage_system_parallel(healths: &mut [Health], damages: &[Damage], dt: f32) { | |
healths | |
.par_iter_mut() | |
.zip(damages.par_iter()) | |
.for_each(|(health, damage)| { | |
health.0 -= damage.0 * dt; | |
}); | |
} | |
} | |
mod world { | |
#[macro_export] | |
macro_rules! impl_world { | |
( | |
$($name:ident: $type:ty => $mask:ident = $value:expr),* $(,)? | |
) => { | |
$(pub const $mask: u32 = 1 << $value;)* | |
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] | |
pub struct EntityId { | |
pub id: u32, | |
pub generation: u32, | |
} | |
#[derive(Default, serde::Serialize, serde::Deserialize)] | |
pub struct EntityLocations { | |
pub generations: Vec<u32>, | |
pub locations: Vec<Option<(usize, usize)>>, | |
} | |
#[derive(Default, serde::Serialize, serde::Deserialize)] | |
pub struct World { | |
pub entity_locations: EntityLocations, | |
pub tables: Vec<ComponentArrays>, | |
pub next_entity_id: u32, | |
pub table_registry: Vec<(u32, usize)>, | |
} | |
#[derive(Default, serde::Serialize, serde::Deserialize)] | |
pub struct ComponentArrays { | |
$(pub $name: Vec<$type>,)* | |
pub entity_indices: Vec<EntityId>, | |
pub mask: u32, | |
} | |
/// Spawn a batch of new entities with the same component mask | |
pub fn spawn_entities(world: &mut World, mask: u32, count: usize) -> Vec<EntityId> { | |
let mut entities = Vec::with_capacity(count); | |
let table_index = get_or_create_table(world, mask); | |
$( | |
if mask & $mask != 0 { | |
world.tables[table_index].$name.reserve(count); | |
} | |
)* | |
world.tables[table_index].entity_indices.reserve(count); | |
for _ in 0..count { | |
let entity = create_entity(world); | |
add_to_table( | |
&mut world.tables[table_index], | |
entity, | |
( | |
$( | |
if mask & $mask != 0 { | |
Some(<$type>::default()) | |
} else { | |
None | |
}, | |
)* | |
), | |
); | |
entities.push(entity); | |
location_insert( | |
&mut world.entity_locations, | |
entity, | |
( | |
table_index, | |
world.tables[table_index].entity_indices.len() - 1, | |
), | |
); | |
} | |
entities | |
} | |
/// Assign an entity's components | |
pub fn set_components( | |
world: &mut World, | |
entity: EntityId, | |
components: ( $(Option<$type>,)* ), | |
) { | |
let ($($name,)*) = components; | |
if let Some((table_idx, array_idx)) = location_get(&world.entity_locations, entity) { | |
let table = &mut world.tables[table_idx]; | |
$( | |
if let Some($name) = $name { | |
if table.mask & $mask != 0 { | |
table.$name[array_idx] = $name; | |
} | |
} | |
)* | |
} | |
} | |
/// Add components to an entity | |
pub fn add_components(world: &mut World, entity: EntityId, mask: u32) -> bool { | |
if let Some((table_idx, array_idx)) = location_get(&world.entity_locations, entity) { | |
let current_mask = world.tables[table_idx].mask; | |
// If entity already has all these components, no need to move | |
if current_mask & mask == mask { | |
return true; | |
} | |
let new_mask = current_mask | mask; | |
let new_table_idx = get_or_create_table(world, new_mask); | |
move_entity(world, entity, table_idx, array_idx, new_table_idx); | |
true | |
} else { | |
false | |
} | |
} | |
/// Remove components from an entity | |
pub fn remove_components(world: &mut World, entity: EntityId, mask: u32) -> bool { | |
if let Some((table_idx, array_idx)) = location_get(&world.entity_locations, entity) { | |
let current_mask = world.tables[table_idx].mask; | |
// If entity doesn't have any of these components, no need to move | |
if current_mask & mask == 0 { | |
return true; | |
} | |
let new_mask = current_mask & !mask; | |
let new_table_idx = get_or_create_table(world, new_mask); | |
move_entity(world, entity, table_idx, array_idx, new_table_idx); | |
true | |
} else { | |
false | |
} | |
} | |
/// Get the current component mask for an entity | |
pub fn get_component_mask(world: &World, entity: EntityId) -> Option<u32> { | |
location_get(&world.entity_locations, entity) | |
.map(|(table_idx, _)| world.tables[table_idx].mask) | |
} | |
/// Merge tables that have the same mask. Call this periodically, such as every 60 frames. | |
pub fn merge_tables(world: &mut World) { | |
let mut moves = Vec::new(); | |
// Collect all moves first to avoid holding references while mutating | |
{ | |
let mut mask_to_tables = std::collections::HashMap::new(); | |
for (i, table) in world.tables.iter().enumerate() { | |
mask_to_tables | |
.entry(table.mask) | |
.or_insert_with(Vec::new) | |
.push(i); | |
} | |
for tables in mask_to_tables.values() { | |
if tables.len() <= 1 { | |
continue; | |
} | |
let target_idx = tables[0]; | |
for &source_idx in &tables[1..] { | |
let source = &world.tables[source_idx]; | |
for (i, &entity) in source.entity_indices.iter().enumerate() { | |
if let Some((table_idx, _array_idx)) = | |
location_get(&world.entity_locations, entity) | |
{ | |
if table_idx == source_idx { | |
moves.push((entity, source_idx, i, target_idx)); | |
} | |
} | |
} | |
} | |
} | |
} | |
// Now perform all moves | |
for (entity, source_idx, array_idx, target_idx) in moves { | |
move_entity(world, entity, source_idx, array_idx, target_idx); | |
} | |
// Clean up empty tables | |
let mut i = 0; | |
while i < world.tables.len() { | |
if world.tables[i].entity_indices.is_empty() { | |
world.tables.swap_remove(i); | |
world | |
.table_registry | |
.retain(|(_, table_idx)| *table_idx != i); | |
for (_, table_idx) in world.table_registry.iter_mut() { | |
if *table_idx > i { | |
*table_idx -= 1; | |
} | |
} | |
} else { | |
i += 1; | |
} | |
} | |
} | |
// Implementation details | |
fn remove_from_table(arrays: &mut ComponentArrays, index: usize) { | |
$( | |
if arrays.mask & $mask != 0 { | |
arrays.$name.swap_remove(index); | |
} | |
)* | |
arrays.entity_indices.swap_remove(index); | |
} | |
fn move_entity( | |
world: &mut World, | |
entity: EntityId, | |
from_table: usize, | |
from_index: usize, | |
to_table: usize, | |
) { | |
let ($($name,)*) = | |
get_components(&world.tables[from_table], from_index); | |
remove_from_table(&mut world.tables[from_table], from_index); | |
let dst = &mut world.tables[to_table]; | |
add_to_table(dst, entity, ($($name,)*)); | |
location_insert( | |
&mut world.entity_locations, | |
entity, | |
(to_table, dst.entity_indices.len() - 1), | |
); | |
} | |
fn get_components( | |
arrays: &ComponentArrays, | |
index: usize, | |
) -> ( $(Option<$type>,)* ) { | |
( | |
$( | |
if arrays.mask & $mask != 0 { | |
Some(arrays.$name[index]) | |
} else { | |
None | |
}, | |
)* | |
) | |
} | |
fn location_get(locations: &EntityLocations, entity: EntityId) -> Option<(usize, usize)> { | |
if entity.id as usize >= locations.generations.len() { | |
return None; | |
} | |
if locations.generations[entity.id as usize] != entity.generation { | |
return None; | |
} | |
if entity.id as usize >= locations.locations.len() { | |
None | |
} else { | |
locations.locations[entity.id as usize] | |
} | |
} | |
fn location_insert( | |
locations: &mut EntityLocations, | |
entity: EntityId, | |
location: (usize, usize), | |
) { | |
let id = entity.id as usize; | |
if id >= locations.generations.len() { | |
locations.generations.resize(id + 1, 0); | |
} | |
if id >= locations.locations.len() { | |
locations.locations.resize(id + 1, None); | |
} | |
locations.generations[id] = entity.generation; | |
locations.locations[id] = Some(location); | |
} | |
fn create_entity(world: &mut World) -> EntityId { | |
let id = world.next_entity_id; | |
world.next_entity_id += 1; | |
let generation = if id as usize >= world.entity_locations.generations.len() { | |
0 | |
} else { | |
world.entity_locations.generations[id as usize] | |
}; | |
EntityId { id, generation } | |
} | |
fn add_to_table( | |
arrays: &mut ComponentArrays, | |
entity: EntityId, | |
components: ( $(Option<$type>,)* ), | |
) { | |
let ($($name,)*) = components; | |
$( | |
if arrays.mask & $mask != 0 { | |
arrays | |
.$name | |
.push($name.unwrap_or_default()); | |
} | |
)* | |
arrays.entity_indices.push(entity); | |
} | |
fn get_or_create_table(world: &mut World, mask: u32) -> usize { | |
if let Some(idx) = world.table_registry.iter().position(|(m, _)| *m == mask) { | |
return world.table_registry[idx].1; | |
} | |
world.tables.push(ComponentArrays { | |
mask, | |
..Default::default() | |
}); | |
let table_idx = world.tables.len() - 1; | |
world.table_registry.push((mask, table_idx)); | |
table_idx | |
} | |
}; | |
} | |
#[macro_export] | |
macro_rules! has_components { | |
($table:expr, $mask:expr) => { | |
$table.mask & $mask == $mask | |
}; | |
} | |
} |
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
impl_world! { | |
positions: Position => POSITION = 0, | |
velocities: Velocity => VELOCITY = 1, | |
gravities: Gravity => GRAVITY = 2, | |
healths: Health => HEALTH = 3, | |
damages: Damage => DAMAGE = 4, | |
} | |
pub fn main() { | |
let mut world = World::default(); | |
let entities = spawn_entities(&mut world, POSITION | VELOCITY | GRAVITY, 4); | |
for (index, &entity) in entities.iter().enumerate() { | |
let index = index as f32; | |
set_components( | |
&mut world, | |
entity, | |
( | |
Some(Position { x: index, y: index }), | |
Some(Velocity { | |
x: index * 0.1, | |
y: index * 0.1, | |
}), | |
Some(Gravity(9.81)), | |
None, | |
Some(Damage(0.0)), | |
), | |
); | |
} | |
let dt = 1.0 / 60.0; | |
systems::run_systems(&mut world, dt); | |
} | |
use components::*; | |
mod components { | |
#[derive(Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize)] | |
pub struct Position { | |
pub x: f32, | |
pub y: f32, | |
} | |
#[derive(Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize)] | |
pub struct Velocity { | |
pub x: f32, | |
pub y: f32, | |
} | |
#[derive(Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize)] | |
pub struct Gravity(pub f32); | |
#[derive(Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize)] | |
pub struct Health(pub f32); | |
#[derive(Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize)] | |
pub struct Damage(pub f32); | |
} | |
mod systems { | |
use crate::*; | |
use rayon::prelude::*; | |
pub fn run_systems(world: &mut World, dt: f32) { | |
// TODO: call this every 60 frames or so | |
// merge_tables(world); | |
world.tables.par_iter_mut().for_each(|table| { | |
if has_components!(table, POSITION | VELOCITY) { | |
movement_system_parallel(&mut table.positions, &table.velocities, dt); | |
} | |
if has_components!(table, VELOCITY | GRAVITY) { | |
gravity_system_parallel(&mut table.velocities, &table.gravities, dt); | |
} | |
if has_components!(table, HEALTH | DAMAGE) { | |
damage_system_parallel(&mut table.healths, &table.damages, dt); | |
} | |
}); | |
} | |
pub fn movement_system_parallel(positions: &mut [Position], velocities: &[Velocity], dt: f32) { | |
positions | |
.par_iter_mut() | |
.zip(velocities.par_iter()) | |
.for_each(|(pos, vel)| { | |
pos.x += vel.x * dt; | |
pos.y += vel.y * dt; | |
}); | |
} | |
pub fn gravity_system_parallel(velocities: &mut [Velocity], gravities: &[Gravity], dt: f32) { | |
velocities | |
.par_iter_mut() | |
.zip(gravities.par_iter()) | |
.for_each(|(vel, gravity)| { | |
vel.y -= gravity.0 * dt; | |
}); | |
} | |
pub fn damage_system_parallel(healths: &mut [Health], damages: &[Damage], dt: f32) { | |
healths | |
.par_iter_mut() | |
.zip(damages.par_iter()) | |
.for_each(|(health, damage)| { | |
health.0 -= damage.0 * dt; | |
}); | |
} | |
} | |
mod world { | |
#[macro_export] | |
macro_rules! impl_world { | |
( | |
$($name:ident: $type:ty => $mask:ident = $value:expr),* $(,)? | |
) => { | |
$(pub const $mask: u32 = 1 << $value;)* | |
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] | |
pub struct EntityId { | |
pub id: u32, | |
pub generation: u32, | |
} | |
#[derive(Default, serde::Serialize, serde::Deserialize)] | |
pub struct EntityLocations { | |
pub generations: Vec<u32>, | |
pub locations: Vec<Option<(usize, usize)>>, | |
} | |
#[derive(Default, serde::Serialize, serde::Deserialize)] | |
pub struct World { | |
pub entity_locations: EntityLocations, | |
pub tables: Vec<ComponentArrays>, | |
pub next_entity_id: u32, | |
pub table_registry: Vec<(u32, usize)>, | |
} | |
#[derive(Default, serde::Serialize, serde::Deserialize)] | |
pub struct ComponentArrays { | |
$(pub $name: Vec<$type>,)* | |
pub entity_indices: Vec<EntityId>, | |
pub mask: u32, | |
} | |
/// Spawn a batch of new entities with the same component mask | |
pub fn spawn_entities(world: &mut World, mask: u32, count: usize) -> Vec<EntityId> { | |
let mut entities = Vec::with_capacity(count); | |
let table_index = get_or_create_table(world, mask); | |
$( | |
if mask & $mask != 0 { | |
world.tables[table_index].$name.reserve(count); | |
} | |
)* | |
world.tables[table_index].entity_indices.reserve(count); | |
for _ in 0..count { | |
let entity = create_entity(world); | |
add_to_table( | |
&mut world.tables[table_index], | |
entity, | |
( | |
$( | |
if mask & $mask != 0 { | |
Some(<$type>::default()) | |
} else { | |
None | |
}, | |
)* | |
), | |
); | |
entities.push(entity); | |
location_insert( | |
&mut world.entity_locations, | |
entity, | |
( | |
table_index, | |
world.tables[table_index].entity_indices.len() - 1, | |
), | |
); | |
} | |
entities | |
} | |
/// Assign an entity's components | |
pub fn set_components( | |
world: &mut World, | |
entity: EntityId, | |
components: ( $(Option<$type>,)* ), | |
) { | |
let ($($name,)*) = components; | |
if let Some((table_idx, array_idx)) = location_get(&world.entity_locations, entity) { | |
let table = &mut world.tables[table_idx]; | |
$( | |
if let Some($name) = $name { | |
if table.mask & $mask != 0 { | |
table.$name[array_idx] = $name; | |
} | |
} | |
)* | |
} | |
} | |
/// Add components to an entity | |
pub fn add_components(world: &mut World, entity: EntityId, mask: u32) -> bool { | |
if let Some((table_idx, array_idx)) = location_get(&world.entity_locations, entity) { | |
let current_mask = world.tables[table_idx].mask; | |
// If entity already has all these components, no need to move | |
if current_mask & mask == mask { | |
return true; | |
} | |
let new_mask = current_mask | mask; | |
let new_table_idx = get_or_create_table(world, new_mask); | |
move_entity(world, entity, table_idx, array_idx, new_table_idx); | |
true | |
} else { | |
false | |
} | |
} | |
/// Remove components from an entity | |
pub fn remove_components(world: &mut World, entity: EntityId, mask: u32) -> bool { | |
if let Some((table_idx, array_idx)) = location_get(&world.entity_locations, entity) { | |
let current_mask = world.tables[table_idx].mask; | |
// If entity doesn't have any of these components, no need to move | |
if current_mask & mask == 0 { | |
return true; | |
} | |
let new_mask = current_mask & !mask; | |
let new_table_idx = get_or_create_table(world, new_mask); | |
move_entity(world, entity, table_idx, array_idx, new_table_idx); | |
true | |
} else { | |
false | |
} | |
} | |
/// Get the current component mask for an entity | |
pub fn get_component_mask(world: &World, entity: EntityId) -> Option<u32> { | |
location_get(&world.entity_locations, entity) | |
.map(|(table_idx, _)| world.tables[table_idx].mask) | |
} | |
/// Merge tables that have the same mask. Call this periodically, such as every 60 frames. | |
pub fn merge_tables(world: &mut World) { | |
let mut moves = Vec::new(); | |
// Collect all moves first to avoid holding references while mutating | |
{ | |
let mut mask_to_tables = std::collections::HashMap::new(); | |
for (i, table) in world.tables.iter().enumerate() { | |
mask_to_tables | |
.entry(table.mask) | |
.or_insert_with(Vec::new) | |
.push(i); | |
} | |
for tables in mask_to_tables.values() { | |
if tables.len() <= 1 { | |
continue; | |
} | |
let target_idx = tables[0]; | |
for &source_idx in &tables[1..] { | |
let source = &world.tables[source_idx]; | |
for (i, &entity) in source.entity_indices.iter().enumerate() { | |
if let Some((table_idx, _array_idx)) = | |
location_get(&world.entity_locations, entity) | |
{ | |
if table_idx == source_idx { | |
moves.push((entity, source_idx, i, target_idx)); | |
} | |
} | |
} | |
} | |
} | |
} | |
// Now perform all moves | |
for (entity, source_idx, array_idx, target_idx) in moves { | |
move_entity(world, entity, source_idx, array_idx, target_idx); | |
} | |
// Clean up empty tables | |
let mut i = 0; | |
while i < world.tables.len() { | |
if world.tables[i].entity_indices.is_empty() { | |
world.tables.swap_remove(i); | |
world | |
.table_registry | |
.retain(|(_, table_idx)| *table_idx != i); | |
for (_, table_idx) in world.table_registry.iter_mut() { | |
if *table_idx > i { | |
*table_idx -= 1; | |
} | |
} | |
} else { | |
i += 1; | |
} | |
} | |
} | |
// Implementation details | |
fn remove_from_table(arrays: &mut ComponentArrays, index: usize) { | |
$( | |
if arrays.mask & $mask != 0 { | |
arrays.$name.swap_remove(index); | |
} | |
)* | |
arrays.entity_indices.swap_remove(index); | |
} | |
fn move_entity( | |
world: &mut World, | |
entity: EntityId, | |
from_table: usize, | |
from_index: usize, | |
to_table: usize, | |
) { | |
let ($($name,)*) = | |
get_components(&world.tables[from_table], from_index); | |
remove_from_table(&mut world.tables[from_table], from_index); | |
let dst = &mut world.tables[to_table]; | |
add_to_table(dst, entity, ($($name,)*)); | |
location_insert( | |
&mut world.entity_locations, | |
entity, | |
(to_table, dst.entity_indices.len() - 1), | |
); | |
} | |
fn get_components( | |
arrays: &ComponentArrays, | |
index: usize, | |
) -> ( $(Option<$type>,)* ) { | |
( | |
$( | |
if arrays.mask & $mask != 0 { | |
Some(arrays.$name[index]) | |
} else { | |
None | |
}, | |
)* | |
) | |
} | |
fn location_get(locations: &EntityLocations, entity: EntityId) -> Option<(usize, usize)> { | |
if entity.id as usize >= locations.generations.len() { | |
return None; | |
} | |
if locations.generations[entity.id as usize] != entity.generation { | |
return None; | |
} | |
if entity.id as usize >= locations.locations.len() { | |
None | |
} else { | |
locations.locations[entity.id as usize] | |
} | |
} | |
fn location_insert( | |
locations: &mut EntityLocations, | |
entity: EntityId, | |
location: (usize, usize), | |
) { | |
let id = entity.id as usize; | |
if id >= locations.generations.len() { | |
locations.generations.resize(id + 1, 0); | |
} | |
if id >= locations.locations.len() { | |
locations.locations.resize(id + 1, None); | |
} | |
locations.generations[id] = entity.generation; | |
locations.locations[id] = Some(location); | |
} | |
fn create_entity(world: &mut World) -> EntityId { | |
let id = world.next_entity_id; | |
world.next_entity_id += 1; | |
let generation = if id as usize >= world.entity_locations.generations.len() { | |
0 | |
} else { | |
world.entity_locations.generations[id as usize] | |
}; | |
EntityId { id, generation } | |
} | |
fn add_to_table( | |
arrays: &mut ComponentArrays, | |
entity: EntityId, | |
components: ( $(Option<$type>,)* ), | |
) { | |
let ($($name,)*) = components; | |
$( | |
if arrays.mask & $mask != 0 { | |
arrays | |
.$name | |
.push($name.unwrap_or_default()); | |
} | |
)* | |
arrays.entity_indices.push(entity); | |
} | |
fn get_or_create_table(world: &mut World, mask: u32) -> usize { | |
if let Some(idx) = world.table_registry.iter().position(|(m, _)| *m == mask) { | |
return world.table_registry[idx].1; | |
} | |
world.tables.push(ComponentArrays { | |
mask, | |
..Default::default() | |
}); | |
let table_idx = world.tables.len() - 1; | |
world.table_registry.push((mask, table_idx)); | |
table_idx | |
} | |
}; | |
} | |
#[macro_export] | |
macro_rules! has_components { | |
($table:expr, $mask:expr) => { | |
$table.mask & $mask == $mask | |
}; | |
} | |
} |
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::time::Instant; | |
// -- Components | |
#[derive(Clone, Debug, Default)] | |
pub struct Position { | |
pub x: f32, | |
pub y: f32, | |
} | |
#[derive(Clone, Debug, Default)] | |
pub struct Velocity { | |
pub x: f32, | |
pub y: f32, | |
} | |
#[derive(Clone, Debug, Default)] | |
pub struct Gravity(pub f32); | |
#[derive(Clone, Debug, Default)] | |
pub struct Health(pub f32); | |
#[derive(Clone, Debug, Default)] | |
pub struct Damage(pub f32); | |
// -- World storage using Vec<Option<T>> | |
#[derive(Default)] | |
pub struct World { | |
positions: Vec<Option<Position>>, | |
velocities: Vec<Option<Velocity>>, | |
gravities: Vec<Option<Gravity>>, | |
healths: Vec<Option<Health>>, | |
damages: Vec<Option<Damage>>, | |
next_entity: usize, | |
active_entities: Vec<usize>, | |
} | |
impl World { | |
pub fn new() -> Self { | |
World::default() | |
} | |
pub fn spawn_entities(&mut self, has_pos: bool, has_vel: bool, has_grav: bool, | |
has_health: bool, has_damage: bool, count: usize) -> Vec<usize> { | |
let mut entities = Vec::with_capacity(count); | |
for _ in 0..count { | |
let entity = self.next_entity; | |
self.next_entity += 1; | |
// Extend vectors if needed | |
while entity >= self.positions.len() { | |
self.positions.push(None); | |
self.velocities.push(None); | |
self.gravities.push(None); | |
self.healths.push(None); | |
self.damages.push(None); | |
} | |
if has_pos { self.positions[entity] = Some(Position::default()); } | |
if has_vel { self.velocities[entity] = Some(Velocity::default()); } | |
if has_grav { self.gravities[entity] = Some(Gravity::default()); } | |
if has_health { self.healths[entity] = Some(Health::default()); } | |
if has_damage { self.damages[entity] = Some(Damage::default()); } | |
self.active_entities.push(entity); | |
entities.push(entity); | |
} | |
entities | |
} | |
} | |
// -- Systems | |
pub fn movement_system(world: &mut World, dt: f32) { | |
for &entity in &world.active_entities { | |
if let (Some(pos), Some(vel)) = ( | |
world.positions.get_mut(entity).and_then(|p| p.as_mut()), | |
world.velocities.get(entity).and_then(|v| v.as_ref()) | |
) { | |
pos.x += vel.x * dt; | |
pos.y += vel.y * dt; | |
} | |
} | |
} | |
pub fn gravity_system(world: &mut World, dt: f32) { | |
for &entity in &world.active_entities { | |
if let (Some(vel), Some(grav)) = ( | |
world.velocities.get_mut(entity).and_then(|v| v.as_mut()), | |
world.gravities.get(entity).and_then(|g| g.as_ref()) | |
) { | |
vel.y -= grav.0 * dt; | |
} | |
} | |
} | |
pub fn damage_system(world: &mut World, dt: f32) { | |
for &entity in &world.active_entities { | |
if let (Some(health), Some(damage)) = ( | |
world.healths.get_mut(entity).and_then(|h| h.as_mut()), | |
world.damages.get(entity).and_then(|d| d.as_ref()) | |
) { | |
health.0 -= damage.0 * dt; | |
} | |
} | |
} | |
fn main() { | |
let mut world = World::new(); | |
println!("Starting Vec<Option<T>> ECS stress test...\n"); | |
// Test 1: Mass entity creation | |
let start = Instant::now(); | |
// Physics entities | |
let physics = world.spawn_entities(true, true, true, false, false, 50_000); | |
// Combat entities | |
let combat = world.spawn_entities(false, false, false, true, true, 25_000); | |
// Full entities | |
let full = world.spawn_entities(true, true, true, true, true, 25_000); | |
// Minimal entities | |
let minimal = world.spawn_entities(true, false, false, false, false, 50_000); | |
let spawn_time = start.elapsed(); | |
let total_entities = physics.len() + combat.len() + full.len() + minimal.len(); | |
// Test 2: System performance | |
println!("Running systems..."); | |
let start = Instant::now(); | |
let dt = 1.0 / 60.0; | |
// Run 180 frames (3 seconds at 60fps) | |
let mut frame_times = Vec::with_capacity(180); | |
for _frame in 0..180 { | |
let frame_start = Instant::now(); | |
gravity_system(&mut world, dt); | |
movement_system(&mut world, dt); | |
damage_system(&mut world, dt); | |
frame_times.push(frame_start.elapsed()); | |
} | |
let sim_time = start.elapsed(); | |
// Final Report | |
println!("\n=== Vec<Option<T>> ECS Stress Test Report ===\n"); | |
println!("Entity Creation:"); | |
println!("----------------"); | |
println!("Total Entities: {}", total_entities); | |
println!("Spawn Time: {:.2?}", spawn_time); | |
println!("Entities/second: {:.0}", total_entities as f64 / spawn_time.as_secs_f64()); | |
println!("\nStorage Statistics:"); | |
println!("------------------"); | |
println!("Vector sizes:"); | |
println!(" Positions: {}", world.positions.len()); | |
println!(" Velocities: {}", world.velocities.len()); | |
println!(" Gravities: {}", world.gravities.len()); | |
println!(" Healths: {}", world.healths.len()); | |
println!(" Damages: {}", world.damages.len()); | |
let memory_used = (world.positions.len() + world.velocities.len() + | |
world.gravities.len() + world.healths.len() + | |
world.damages.len()) * std::mem::size_of::<Option<f32>>(); | |
println!("Total memory used: {} bytes", memory_used); | |
println!("\nPerformance Metrics:"); | |
println!("-------------------"); | |
println!("Total Simulation Time: {:.2?}", sim_time); | |
println!("Average Frame Time: {:.2?}", sim_time / 180); | |
let mut sorted_times = frame_times.clone(); | |
sorted_times.sort_by(|a, b| a.partial_cmp(b).unwrap()); | |
println!("Frame Time Statistics:"); | |
println!(" Min: {:.2?}", sorted_times.first().unwrap()); | |
println!(" Max: {:.2?}", sorted_times.last().unwrap()); | |
println!(" Median: {:.2?}", sorted_times[sorted_times.len() / 2]); | |
println!(" 99th percentile: {:.2?}", sorted_times[(sorted_times.len() as f32 * 0.99) as usize]); | |
println!("\nEntity Sampling:"); | |
println!("---------------"); | |
for &entity in physics.iter().take(2) { | |
if let Some(pos) = &world.positions[entity] { | |
println!("Physics entity {}: pos=({:.1}, {:.1})", | |
entity, pos.x, pos.y); | |
} | |
} | |
for &entity in combat.iter().take(2) { | |
if let Some(health) = &world.healths[entity] { | |
println!("Combat entity {}: health={:.1}", | |
entity, health.0); | |
} | |
} | |
for &entity in full.iter().take(2) { | |
if let (Some(pos), Some(health)) = (&world.positions[entity], &world.healths[entity]) { | |
println!("Full entity {}: pos=({:.1}, {:.1}), health={:.1}", | |
entity, pos.x, pos.y, health.0); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment