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:
2026-03-24 20:05:15 +09:00
parent 96cebecc6d
commit 2d64d226a2
5 changed files with 406 additions and 0 deletions

View File

@@ -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"] }

View File

@@ -0,0 +1,7 @@
[package]
name = "voltex_ecs"
version = "0.1.0"
edition = "2021"
[dependencies]
voltex_math.workspace = true

View 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);
}
}

View File

@@ -0,0 +1,5 @@
pub mod entity;
pub mod sparse_set;
pub use entity::{Entity, EntityAllocator};
pub use sparse_set::SparseSet;

View 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));
}
}