feat(ecs): add basic component storage for ECS
Some checks failed
Build Zenyx ⚡ / 🏗️ Build aarch64-pc-windows-msvc (push) Has been cancelled
Build Zenyx ⚡ / 🏗️ Build aarch64-unknown-linux-gnu (push) Has been cancelled
Build Zenyx ⚡ / 🏗️ Build x86_64-pc-windows-msvc (push) Has been cancelled
Build Zenyx ⚡ / 🏗️ Build x86_64-unknown-linux-gnu (push) Has been cancelled
Build Zenyx ⚡ / 🧪 Run Cargo Tests (push) Has been cancelled
Build Zenyx ⚡ / 🧪 Run Cargo Tests (pull_request) Successful in 2m48s
Build Zenyx ⚡ / 🏗️ Build aarch64-pc-windows-msvc (pull_request) Successful in 8m37s
Build Zenyx ⚡ / 🏗️ Build x86_64-pc-windows-msvc (pull_request) Successful in 8m41s
Build Zenyx ⚡ / 🏗️ Build aarch64-unknown-linux-gnu (pull_request) Successful in 8m55s
Build Zenyx ⚡ / 🏗️ Build x86_64-unknown-linux-gnu (pull_request) Successful in 5m5s

This commit is contained in:
BitSyndicate 2025-05-02 01:10:17 +02:00
parent ca715d1d67
commit 96d44163fc
Signed by: bitsyndicate
GPG key ID: 443E4198D6BBA6DE
3 changed files with 179 additions and 6 deletions

View file

@ -20,6 +20,19 @@ where
dense_to_id: Vec<usize, SparseAlloc>,
}
impl<T, PackedAlloc, SparseAlloc> core::fmt::Debug for SparseSet<T, PackedAlloc, SparseAlloc>
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<T> SparseSet<T> {
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]);
}
}

137
src/ecs/mod.rs Normal file
View file

@ -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<BTreeMap<TypeId, ComponentId>> = RwLock::new(BTreeMap::new());
fn create_component_id<T>() -> ComponentId
where
T: 'static + Sized,
{
let type_id = core::any::TypeId::of::<T>();
{
// 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::<Self>().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<T, SparseAlloc> ComponentStorage for SparseSet<T, T::Allocator, SparseAlloc>
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<A = allocator_api2::alloc::Global> {
sets: SparseSet<Box<dyn ComponentStorage>>,
cold_alloc: A,
}
pub struct EntityComponentSystem {
components: ComponentSet,
}
impl ComponentSet {
fn new() -> Self {
Self {
sets: SparseSet::new(),
cold_alloc: allocator_api2::alloc::Global,
}
}
}
impl<A> ComponentSet<A>
where
A: Allocator + Clone + 'static,
{
fn new_in(alloc: A) -> Self {
Self {
sets: SparseSet::new(),
cold_alloc: alloc,
}
}
fn get_component_set<T: Component>(&self) -> Option<&SparseSet<T, T::Allocator, A>> {
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<T: Component>(
&mut self,
) -> Option<&mut SparseSet<T, T::Allocator, A>> {
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<T: Component>(&mut self) -> &mut SparseSet<T, T::Allocator, A> {
if self.sets.contains(T::id().0.into_integer() as usize) {
self.get_component_set_mut::<T>().unwrap()
} else {
let set = SparseSet::<T, _, _>::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::<T>().unwrap()
}
}
fn add_to_entity<T: Component>(&mut self, entity: Entity, data: T) -> Option<T> {
let set = self.insert_component_set::<T>();
set.insert(entity.0 as usize, data)
}
}
impl Default for ComponentSet {
fn default() -> Self {
Self::new()
}
}

View file

@ -19,6 +19,7 @@ use zlog::config::LoggerConfig;
pub mod camera;
pub mod collections;
pub mod ecs;
pub mod model;
pub mod texture;