From 96d44163fce21e8371c4d07c465c9e29995fef6c Mon Sep 17 00:00:00 2001 From: BitSyndicate Date: Fri, 2 May 2025 01:10:17 +0200 Subject: [PATCH 1/2] feat(ecs): add basic component storage for ECS --- src/collections/sparse_set.rs | 47 ++++++++++-- src/ecs/mod.rs | 137 ++++++++++++++++++++++++++++++++++ src/main.rs | 1 + 3 files changed, 179 insertions(+), 6 deletions(-) create mode 100644 src/ecs/mod.rs diff --git a/src/collections/sparse_set.rs b/src/collections/sparse_set.rs index 5e0ec84..75b04ea 100644 --- a/src/collections/sparse_set.rs +++ b/src/collections/sparse_set.rs @@ -20,6 +20,19 @@ where dense_to_id: Vec, } +impl core::fmt::Debug for SparseSet +where + T: core::fmt::Debug, + PackedAlloc: Allocator, + SparseAlloc: Allocator, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_map() + .entries(self.dense_to_id.iter().zip(self.dense.iter())) + .finish() + } +} + impl SparseSet { pub const fn new() -> Self { Self { @@ -236,7 +249,10 @@ mod tests { assert_eq!(sparse_set.remove(SPARSE_PAGESIZE + 2).unwrap(), 3); assert_eq!(sparse_set.sparse[1].as_ref().unwrap().1, 2); - assert_eq!(sparse_set.keys(), [10, 11, 12, SPARSE_PAGESIZE, SPARSE_PAGESIZE + 1]); + assert_eq!( + sparse_set.keys(), + [10, 11, 12, SPARSE_PAGESIZE, SPARSE_PAGESIZE + 1] + ); assert_eq!(sparse_set.values(), [1, 2, 2, 1, 2]); assert_eq!(sparse_set.remove(SPARSE_PAGESIZE + 1).unwrap(), 2); @@ -249,25 +265,44 @@ mod tests { assert_eq!(sparse_set.keys(), [10, 11, 12]); assert_eq!(sparse_set.values(), [1, 2, 2]); - sparse_set.insert(SPARSE_PAGESIZE, 1); sparse_set.insert(SPARSE_PAGESIZE + 1, 2); sparse_set.insert(SPARSE_PAGESIZE + 2, 3); assert_eq!(sparse_set.remove(10).unwrap(), 1); assert_eq!(sparse_set.sparse[0].as_ref().unwrap().1, 2); - // swap-remove - assert_eq!(sparse_set.keys(), [SPARSE_PAGESIZE + 2, 11, 12, SPARSE_PAGESIZE, SPARSE_PAGESIZE + 1]); + // swap-remove + assert_eq!( + sparse_set.keys(), + [ + SPARSE_PAGESIZE + 2, + 11, + 12, + SPARSE_PAGESIZE, + SPARSE_PAGESIZE + 1 + ] + ); assert_eq!(sparse_set.values(), [3, 2, 2, 1, 2]); assert_eq!(sparse_set.remove(11).unwrap(), 2); assert_eq!(sparse_set.sparse[0].as_ref().unwrap().1, 1); - assert_eq!(sparse_set.keys(), [SPARSE_PAGESIZE + 2, SPARSE_PAGESIZE + 1, 12, SPARSE_PAGESIZE]); + assert_eq!( + sparse_set.keys(), + [ + SPARSE_PAGESIZE + 2, + SPARSE_PAGESIZE + 1, + 12, + SPARSE_PAGESIZE + ] + ); assert_eq!(sparse_set.values(), [3, 2, 2, 1]); assert_eq!(sparse_set.remove(12).unwrap(), 2); assert!(sparse_set.sparse[0].is_none()); - assert_eq!(sparse_set.keys(), [SPARSE_PAGESIZE + 2, SPARSE_PAGESIZE + 1, SPARSE_PAGESIZE]); + assert_eq!( + sparse_set.keys(), + [SPARSE_PAGESIZE + 2, SPARSE_PAGESIZE + 1, SPARSE_PAGESIZE] + ); assert_eq!(sparse_set.values(), [3, 2, 1]); } } diff --git a/src/ecs/mod.rs b/src/ecs/mod.rs new file mode 100644 index 0000000..bb2500a --- /dev/null +++ b/src/ecs/mod.rs @@ -0,0 +1,137 @@ +use core::{any::TypeId, num::NonZeroU64, sync::atomic::AtomicU64}; +use std::{collections::BTreeMap, sync::RwLock}; + +use allocator_api2::alloc::Allocator; +use bytemuck::Contiguous; + +use crate::collections::SparseSet; + +pub type ECS = EntityComponentSystem; + +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Entity(u64); + +static COMPONENT_ID_CREATOR: AtomicU64 = AtomicU64::new(1); +static COMPONENT_IDS: RwLock> = RwLock::new(BTreeMap::new()); + +fn create_component_id() -> ComponentId +where + T: 'static + Sized, +{ + let type_id = core::any::TypeId::of::(); + { + // unwrap-justification: this only errors if RwLock is poisoned + let component_read = COMPONENT_IDS.read().unwrap(); + if let Some(id) = component_read.get(&type_id) { + return *id; + } + } + let new_id = ComponentId( + NonZeroU64::new(COMPONENT_ID_CREATOR.fetch_add(1, core::sync::atomic::Ordering::Relaxed)) + .unwrap(), + ); + { + // unwrap-justification: see above + let mut component_write = COMPONENT_IDS.write().unwrap(); + component_write.insert(type_id, new_id); + } + new_id +} + +pub trait ComponentAllocator: Allocator { + fn new() -> Self; +} + +pub trait Component: core::fmt::Debug + Send + Sized + 'static { + type Allocator: ComponentAllocator; + + fn id() -> ComponentId { + static COMPONENT_ID: AtomicU64 = AtomicU64::new(0); + // TODO: reevaluate ordering later + let mut current_id = COMPONENT_ID.load(core::sync::atomic::Ordering::SeqCst); + if current_id == 0 { + current_id = create_component_id::().0.into_integer(); + COMPONENT_ID.store(current_id, core::sync::atomic::Ordering::SeqCst); + } + ComponentId(NonZeroU64::new(current_id).unwrap()) + } +} + +pub trait ComponentStorage: core::fmt::Debug + 'static {} + +impl ComponentStorage for SparseSet +where + T: Component, + SparseAlloc: Allocator + Clone + 'static, +{ +} + +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ComponentId(NonZeroU64); + +#[derive(Debug)] +pub struct ComponentSet { + sets: SparseSet>, + cold_alloc: A, +} + +pub struct EntityComponentSystem { + components: ComponentSet, +} + +impl ComponentSet { + fn new() -> Self { + Self { + sets: SparseSet::new(), + cold_alloc: allocator_api2::alloc::Global, + } + } +} + +impl ComponentSet +where + A: Allocator + Clone + 'static, +{ + fn new_in(alloc: A) -> Self { + Self { + sets: SparseSet::new(), + cold_alloc: alloc, + } + } + + fn get_component_set(&self) -> Option<&SparseSet> { + let set = self.sets.get(T::id().0.into_integer() as usize)?; + (set as &dyn core::any::Any).downcast_ref() + } + + fn get_component_set_mut( + &mut self, + ) -> Option<&mut SparseSet> { + let set = self.sets.get_mut(T::id().0.into_integer() as usize)?; + (set as &mut dyn core::any::Any).downcast_mut() + } + + fn insert_component_set(&mut self) -> &mut SparseSet { + if self.sets.contains(T::id().0.into_integer() as usize) { + self.get_component_set_mut::().unwrap() + } else { + let set = SparseSet::::new_in(T::Allocator::new(), self.cold_alloc.clone()); + self.sets + .insert(T::id().0.into_integer() as usize, Box::new(set) as Box<_>); + self.get_component_set_mut::().unwrap() + } + } + + fn add_to_entity(&mut self, entity: Entity, data: T) -> Option { + let set = self.insert_component_set::(); + set.insert(entity.0 as usize, data) + } +} + +impl Default for ComponentSet { + fn default() -> Self { + Self::new() + } +} diff --git a/src/main.rs b/src/main.rs index 0105e37..6786044 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,7 @@ use zlog::config::LoggerConfig; pub mod camera; pub mod collections; +pub mod ecs; pub mod model; pub mod texture; -- 2.47.2 From 1b89120b735be202f608ebf4fec3a158f93ca88e Mon Sep 17 00:00:00 2001 From: Chance Date: Thu, 1 May 2025 20:00:06 -0400 Subject: [PATCH 2/2] feat(ecs): entity spawning --- src/ecs/mod.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/ecs/mod.rs b/src/ecs/mod.rs index bb2500a..4069d90 100644 --- a/src/ecs/mod.rs +++ b/src/ecs/mod.rs @@ -27,6 +27,7 @@ where return *id; } } + let new_id = ComponentId( NonZeroU64::new(COMPONENT_ID_CREATOR.fetch_add(1, core::sync::atomic::Ordering::Relaxed)) .unwrap(), @@ -48,11 +49,10 @@ pub trait Component: core::fmt::Debug + Send + Sized + 'static { fn id() -> ComponentId { static COMPONENT_ID: AtomicU64 = AtomicU64::new(0); - // TODO: reevaluate ordering later - let mut current_id = COMPONENT_ID.load(core::sync::atomic::Ordering::SeqCst); + let mut current_id = COMPONENT_ID.load(core::sync::atomic::Ordering::Relaxed); if current_id == 0 { current_id = create_component_id::().0.into_integer(); - COMPONENT_ID.store(current_id, core::sync::atomic::Ordering::SeqCst); + COMPONENT_ID.store(current_id, core::sync::atomic::Ordering::Relaxed); } ComponentId(NonZeroU64::new(current_id).unwrap()) } @@ -79,6 +79,16 @@ pub struct ComponentSet { pub struct EntityComponentSystem { components: ComponentSet, + next_id: AtomicU64, +} + +impl EntityComponentSystem { + fn spawn(&mut self) -> Entity { + let entity_id = self + .next_id + .fetch_add(1, core::sync::atomic::Ordering::Relaxed); + Entity(entity_id) + } } impl ComponentSet { -- 2.47.2