feat(ecs): add voltex_ecs crate with Entity, EntityAllocator, and SparseSet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ members = [
|
||||
"crates/voltex_math",
|
||||
"crates/voltex_platform",
|
||||
"crates/voltex_renderer",
|
||||
"crates/voltex_ecs",
|
||||
"examples/triangle",
|
||||
"examples/model_viewer",
|
||||
]
|
||||
@@ -12,6 +13,7 @@ members = [
|
||||
voltex_math = { path = "crates/voltex_math" }
|
||||
voltex_platform = { path = "crates/voltex_platform" }
|
||||
voltex_renderer = { path = "crates/voltex_renderer" }
|
||||
voltex_ecs = { path = "crates/voltex_ecs" }
|
||||
wgpu = "28.0"
|
||||
winit = "0.30"
|
||||
bytemuck = { version = "1", features = ["derive"] }
|
||||
|
||||
7
crates/voltex_ecs/Cargo.toml
Normal file
7
crates/voltex_ecs/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "voltex_ecs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
voltex_math.workspace = true
|
||||
136
crates/voltex_ecs/src/entity.rs
Normal file
136
crates/voltex_ecs/src/entity.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Entity {
|
||||
pub id: u32,
|
||||
pub generation: u32,
|
||||
}
|
||||
|
||||
struct EntityEntry {
|
||||
generation: u32,
|
||||
alive: bool,
|
||||
}
|
||||
|
||||
pub struct EntityAllocator {
|
||||
entries: Vec<EntityEntry>,
|
||||
free_list: Vec<u32>,
|
||||
alive_count: usize,
|
||||
}
|
||||
|
||||
impl EntityAllocator {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entries: Vec::new(),
|
||||
free_list: Vec::new(),
|
||||
alive_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allocate(&mut self) -> Entity {
|
||||
self.alive_count += 1;
|
||||
if let Some(id) = self.free_list.pop() {
|
||||
let entry = &mut self.entries[id as usize];
|
||||
// generation was already incremented on deallocate
|
||||
entry.alive = true;
|
||||
Entity {
|
||||
id,
|
||||
generation: entry.generation,
|
||||
}
|
||||
} else {
|
||||
let id = self.entries.len() as u32;
|
||||
self.entries.push(EntityEntry {
|
||||
generation: 0,
|
||||
alive: true,
|
||||
});
|
||||
Entity { id, generation: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deallocate(&mut self, entity: Entity) -> bool {
|
||||
let Some(entry) = self.entries.get_mut(entity.id as usize) else {
|
||||
return false;
|
||||
};
|
||||
if !entry.alive || entry.generation != entity.generation {
|
||||
return false;
|
||||
}
|
||||
entry.alive = false;
|
||||
entry.generation = entry.generation.wrapping_add(1);
|
||||
self.free_list.push(entity.id);
|
||||
self.alive_count -= 1;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn is_alive(&self, entity: Entity) -> bool {
|
||||
self.entries
|
||||
.get(entity.id as usize)
|
||||
.map_or(false, |e| e.alive && e.generation == entity.generation)
|
||||
}
|
||||
|
||||
pub fn alive_count(&self) -> usize {
|
||||
self.alive_count
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EntityAllocator {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_allocate() {
|
||||
let mut alloc = EntityAllocator::new();
|
||||
let e0 = alloc.allocate();
|
||||
let e1 = alloc.allocate();
|
||||
assert_eq!(e0.id, 0);
|
||||
assert_eq!(e1.id, 1);
|
||||
assert_eq!(e0.generation, 0);
|
||||
assert_eq!(e1.generation, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deallocate_and_reuse() {
|
||||
let mut alloc = EntityAllocator::new();
|
||||
let e0 = alloc.allocate();
|
||||
let _e1 = alloc.allocate();
|
||||
assert!(alloc.deallocate(e0));
|
||||
let e0_new = alloc.allocate();
|
||||
assert_eq!(e0_new.id, 0);
|
||||
assert_eq!(e0_new.generation, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_alive() {
|
||||
let mut alloc = EntityAllocator::new();
|
||||
let e = alloc.allocate();
|
||||
assert!(alloc.is_alive(e));
|
||||
alloc.deallocate(e);
|
||||
assert!(!alloc.is_alive(e));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stale_entity_rejected() {
|
||||
let mut alloc = EntityAllocator::new();
|
||||
let e = alloc.allocate();
|
||||
alloc.deallocate(e);
|
||||
// stale entity not alive
|
||||
assert!(!alloc.is_alive(e));
|
||||
// double-delete fails
|
||||
assert!(!alloc.deallocate(e));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_alive_count() {
|
||||
let mut alloc = EntityAllocator::new();
|
||||
assert_eq!(alloc.alive_count(), 0);
|
||||
let e0 = alloc.allocate();
|
||||
let e1 = alloc.allocate();
|
||||
assert_eq!(alloc.alive_count(), 2);
|
||||
alloc.deallocate(e0);
|
||||
assert_eq!(alloc.alive_count(), 1);
|
||||
alloc.deallocate(e1);
|
||||
assert_eq!(alloc.alive_count(), 0);
|
||||
}
|
||||
}
|
||||
5
crates/voltex_ecs/src/lib.rs
Normal file
5
crates/voltex_ecs/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod entity;
|
||||
pub mod sparse_set;
|
||||
|
||||
pub use entity::{Entity, EntityAllocator};
|
||||
pub use sparse_set::SparseSet;
|
||||
256
crates/voltex_ecs/src/sparse_set.rs
Normal file
256
crates/voltex_ecs/src/sparse_set.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
use std::any::Any;
|
||||
use crate::entity::Entity;
|
||||
|
||||
pub struct SparseSet<T> {
|
||||
sparse: Vec<Option<usize>>,
|
||||
dense_entities: Vec<Entity>,
|
||||
dense_data: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> SparseSet<T> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
sparse: Vec::new(),
|
||||
dense_entities: Vec::new(),
|
||||
dense_data: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, entity: Entity, value: T) {
|
||||
let id = entity.id as usize;
|
||||
// Grow sparse vec if needed
|
||||
if id >= self.sparse.len() {
|
||||
self.sparse.resize(id + 1, None);
|
||||
}
|
||||
|
||||
if let Some(dense_idx) = self.sparse[id] {
|
||||
// Overwrite existing
|
||||
self.dense_data[dense_idx] = value;
|
||||
self.dense_entities[dense_idx] = entity;
|
||||
} else {
|
||||
let dense_idx = self.dense_data.len();
|
||||
self.sparse[id] = Some(dense_idx);
|
||||
self.dense_entities.push(entity);
|
||||
self.dense_data.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, entity: Entity) -> Option<T> {
|
||||
let id = entity.id as usize;
|
||||
let dense_idx = *self.sparse.get(id)?.as_ref()?;
|
||||
|
||||
// Check entity matches (generation safety)
|
||||
if self.dense_entities[dense_idx] != entity {
|
||||
return None;
|
||||
}
|
||||
|
||||
let last_idx = self.dense_data.len() - 1;
|
||||
self.sparse[id] = None;
|
||||
|
||||
if dense_idx == last_idx {
|
||||
self.dense_entities.pop();
|
||||
Some(self.dense_data.pop().unwrap())
|
||||
} else {
|
||||
// Swap with last
|
||||
let swapped_entity = self.dense_entities[last_idx];
|
||||
self.sparse[swapped_entity.id as usize] = Some(dense_idx);
|
||||
self.dense_entities.swap_remove(dense_idx);
|
||||
Some(self.dense_data.swap_remove(dense_idx))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, entity: Entity) -> Option<&T> {
|
||||
let id = entity.id as usize;
|
||||
let dense_idx = self.sparse.get(id)?.as_ref().copied()?;
|
||||
if self.dense_entities[dense_idx] != entity {
|
||||
return None;
|
||||
}
|
||||
Some(&self.dense_data[dense_idx])
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, entity: Entity) -> Option<&mut T> {
|
||||
let id = entity.id as usize;
|
||||
let dense_idx = self.sparse.get(id)?.as_ref().copied()?;
|
||||
if self.dense_entities[dense_idx] != entity {
|
||||
return None;
|
||||
}
|
||||
Some(&mut self.dense_data[dense_idx])
|
||||
}
|
||||
|
||||
pub fn contains(&self, entity: Entity) -> bool {
|
||||
let id = entity.id as usize;
|
||||
self.sparse
|
||||
.get(id)
|
||||
.and_then(|opt| opt.as_ref())
|
||||
.map_or(false, |&dense_idx| {
|
||||
self.dense_entities[dense_idx] == entity
|
||||
})
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.dense_data.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.dense_data.is_empty()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (Entity, &T)> {
|
||||
self.dense_entities.iter().copied().zip(self.dense_data.iter())
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = (Entity, &mut T)> {
|
||||
self.dense_entities.iter().copied().zip(self.dense_data.iter_mut())
|
||||
}
|
||||
|
||||
pub fn entities(&self) -> &[Entity] {
|
||||
&self.dense_entities
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &[T] {
|
||||
&self.dense_data
|
||||
}
|
||||
|
||||
pub fn data_mut(&mut self) -> &mut [T] {
|
||||
&mut self.dense_data
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for SparseSet<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ComponentStorage: Any {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
fn remove_entity(&mut self, entity: Entity);
|
||||
fn storage_len(&self) -> usize;
|
||||
}
|
||||
|
||||
impl<T: 'static> ComponentStorage for SparseSet<T> {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
fn remove_entity(&mut self, entity: Entity) {
|
||||
self.remove(entity);
|
||||
}
|
||||
fn storage_len(&self) -> usize {
|
||||
self.dense_data.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
fn make_entity(id: u32, generation: u32) -> Entity {
|
||||
Entity { id, generation }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_and_get() {
|
||||
let mut set: SparseSet<i32> = SparseSet::new();
|
||||
let e = make_entity(0, 0);
|
||||
set.insert(e, 42);
|
||||
assert_eq!(set.get(e), Some(&42));
|
||||
assert_eq!(set.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_overwrite() {
|
||||
let mut set: SparseSet<i32> = SparseSet::new();
|
||||
let e = make_entity(0, 0);
|
||||
set.insert(e, 1);
|
||||
set.insert(e, 99);
|
||||
assert_eq!(set.get(e), Some(&99));
|
||||
assert_eq!(set.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove() {
|
||||
let mut set: SparseSet<i32> = SparseSet::new();
|
||||
let e0 = make_entity(0, 0);
|
||||
let e1 = make_entity(1, 0);
|
||||
let e2 = make_entity(2, 0);
|
||||
set.insert(e0, 10);
|
||||
set.insert(e1, 20);
|
||||
set.insert(e2, 30);
|
||||
// Remove middle
|
||||
let removed = set.remove(e1);
|
||||
assert_eq!(removed, Some(20));
|
||||
assert_eq!(set.len(), 2);
|
||||
assert!(set.get(e1).is_none());
|
||||
// Remaining still accessible
|
||||
assert_eq!(set.get(e0), Some(&10));
|
||||
assert_eq!(set.get(e2), Some(&30));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_nonexistent() {
|
||||
let mut set: SparseSet<i32> = SparseSet::new();
|
||||
let e = make_entity(5, 0);
|
||||
assert_eq!(set.remove(e), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter() {
|
||||
let mut set: SparseSet<i32> = SparseSet::new();
|
||||
let e0 = make_entity(0, 0);
|
||||
let e1 = make_entity(1, 0);
|
||||
set.insert(e0, 100);
|
||||
set.insert(e1, 200);
|
||||
let mut values: Vec<i32> = set.iter().map(|(_, v)| *v).collect();
|
||||
values.sort();
|
||||
assert_eq!(values, vec![100, 200]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter_mut() {
|
||||
let mut set: SparseSet<i32> = SparseSet::new();
|
||||
let e0 = make_entity(0, 0);
|
||||
let e1 = make_entity(1, 0);
|
||||
set.insert(e0, 1);
|
||||
set.insert(e1, 2);
|
||||
for (_, v) in set.iter_mut() {
|
||||
*v *= 10;
|
||||
}
|
||||
assert_eq!(set.get(e0), Some(&10));
|
||||
assert_eq!(set.get(e1), Some(&20));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contains() {
|
||||
let mut set: SparseSet<i32> = SparseSet::new();
|
||||
let e = make_entity(3, 0);
|
||||
assert!(!set.contains(e));
|
||||
set.insert(e, 7);
|
||||
assert!(set.contains(e));
|
||||
set.remove(e);
|
||||
assert!(!set.contains(e));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_swap_remove_correctness() {
|
||||
let mut set: SparseSet<i32> = SparseSet::new();
|
||||
let e0 = make_entity(0, 0);
|
||||
let e1 = make_entity(1, 0);
|
||||
let e2 = make_entity(2, 0);
|
||||
set.insert(e0, 10);
|
||||
set.insert(e1, 20);
|
||||
set.insert(e2, 30);
|
||||
// Remove first (triggers swap with last)
|
||||
let removed = set.remove(e0);
|
||||
assert_eq!(removed, Some(10));
|
||||
assert_eq!(set.len(), 2);
|
||||
assert!(set.get(e0).is_none());
|
||||
// Remaining still accessible
|
||||
assert_eq!(set.get(e1), Some(&20));
|
||||
assert_eq!(set.get(e2), Some(&30));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user