Skip to content

Instantly share code, notes, and snippets.

@matthewjberger
Last active November 6, 2024 19:48
Show Gist options
  • Save matthewjberger/642e74d6b997c34e87c637247815597b to your computer and use it in GitHub Desktop.
Save matthewjberger/642e74d6b997c34e87c637247815597b to your computer and use it in GitHub Desktop.
Vec<Option<T>> comparison to precomputing entity archetypes
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
};
}
}
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
};
}
}
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