From 4c0b5171277a06fd3c919b9e866d196e0406b600 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 26 Nov 2023 21:05:35 -0500 Subject: [PATCH] Implement despawning entities --- lyra-ecs/Cargo.lock | 68 ++++++++ lyra-ecs/Cargo.toml | 4 + lyra-ecs/README.md | 12 +- lyra-ecs/src/archetype.rs | 303 ++++++++++++++++++++++++++++++++- lyra-ecs/src/query/borrow.rs | 18 ++ lyra-ecs/src/query/entities.rs | 12 +- lyra-ecs/src/tests.rs | 12 ++ lyra-ecs/src/world.rs | 29 +++- 8 files changed, 437 insertions(+), 21 deletions(-) diff --git a/lyra-ecs/Cargo.lock b/lyra-ecs/Cargo.lock index f9ca240..e3c6de7 100644 --- a/lyra-ecs/Cargo.lock +++ b/lyra-ecs/Cargo.lock @@ -2,6 +2,74 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + [[package]] name = "lyra-ecs" version = "0.1.0" +dependencies = [ + "rand", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" diff --git a/lyra-ecs/Cargo.toml b/lyra-ecs/Cargo.toml index edc4d79..41aae70 100644 --- a/lyra-ecs/Cargo.toml +++ b/lyra-ecs/Cargo.toml @@ -6,3 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] + + +[dev-dependencies] +rand = "0.8.5" # used for tests \ No newline at end of file diff --git a/lyra-ecs/README.md b/lyra-ecs/README.md index f042cce..32c0314 100644 --- a/lyra-ecs/README.md +++ b/lyra-ecs/README.md @@ -7,10 +7,13 @@ I couldn't find anything that fulfilled my needs, specifically an ECS that can s ## Features - [ ] Archetypes - [x] Spawning entities - - [ ] Despawning entities - - [ ] Delete entities from archetypes - - [ ] Find some way to fill in the gaps in component columns after entities are deleted - - [ ] Grow archetype as it fills up + - [x] Despawning entities + - [x] Delete entities from archetypes + - [x] Find some way to fill in the gaps in component columns after entities are deleted + * This was done by moving the last entity in the column to the gap that was just removed. + - [x] Grow archetype as it fills up + - [ ] Borrow safety of components inside Archetypes + * Its wonk right now; a component can be borrowed mutably from a non-mutable reference. - [ ] Querying components from archetypes - [x] Views - [ ] Mutable views @@ -21,5 +24,6 @@ I couldn't find anything that fulfilled my needs, specifically an ECS that can s - [ ] Systems - [ ] Dispatchers/Executors +
To be honest, the name may change at some point \ No newline at end of file diff --git a/lyra-ecs/src/archetype.rs b/lyra-ecs/src/archetype.rs index df97f2b..1a1e160 100644 --- a/lyra-ecs/src/archetype.rs +++ b/lyra-ecs/src/archetype.rs @@ -1,12 +1,13 @@ -use std::{any::TypeId, ptr::{NonNull, self}, alloc::{self, Layout, alloc}}; +use std::{any::TypeId, ptr::{NonNull, self}, alloc::{self, Layout, alloc, dealloc}, mem, collections::HashMap}; -use crate::{world::{Entity, ArchetypeEntityId}, bundle::Bundle, component_info::ComponentInfo}; +use crate::{world::{Entity, ArchetypeEntityId, Record}, bundle::Bundle, component_info::ComponentInfo}; pub struct ComponentColumn { pub data: NonNull, pub capacity: usize, pub info: ComponentInfo, pub entry_size: usize, + pub len: usize, } #[allow(dead_code)] @@ -18,6 +19,7 @@ impl ComponentColumn { capacity: 0, info: ComponentInfo::new::<()>(), entry_size: 0, + len: 0, } } @@ -43,6 +45,7 @@ impl ComponentColumn { capacity, info, entry_size: size, + len: 0, } } @@ -55,6 +58,7 @@ impl ComponentColumn { assert!(entity_index < self.capacity); let dest = NonNull::new_unchecked(self.data.as_ptr().add(entity_index * self.entry_size)); ptr::copy_nonoverlapping(comp_src.as_ptr(), dest.as_ptr(), self.entry_size); + self.len += 1; } /// Get a component at an entities index. @@ -63,10 +67,10 @@ impl ComponentColumn { /// /// This column MUST have the entity. If it does not, it WILL NOT panic and will cause UB. pub unsafe fn get(&self, entity_index: usize) -> &T { - let p = self.data.as_ptr() - .cast::() - .add(entity_index * self.entry_size); - &*p + let ptr = NonNull::new_unchecked(self.data.as_ptr() + .add(entity_index * self.entry_size)) + .cast(); + &*ptr.as_ptr() } /// Get a component at an entities index. @@ -80,6 +84,51 @@ impl ComponentColumn { .add(entity_index * self.entry_size); &mut *p } + + /// Grow the column to fit `new_capacity` amount of components. + /// + /// Parameters: + /// * `new_capacity` - The new capacity of components that can fit in this column. + /// + /// # Safety + /// + /// Will panic if `new_capacity` is less than the current capacity of the column. + pub unsafe fn grow(&mut self, new_capacity: usize) { + assert!(new_capacity > self.capacity); + let mut new_ptr = Self::alloc(self.info.layout, new_capacity); + ptr::copy_nonoverlapping(self.data.as_ptr(), new_ptr.as_ptr(), self.capacity * self.entry_size); + + let old_layout = Layout::from_size_align_unchecked( + self.info.layout.size().checked_mul(self.capacity).unwrap(), + self.info.layout.align() + ); + + mem::swap(&mut self.data, &mut new_ptr); // 'new_ptr' is now the old pointer + dealloc(new_ptr.as_ptr(), old_layout); + + self.capacity = new_capacity; + } + + /// Removes a component from the column, freeing it, and returning the old index of the entity that took its place in the column. + pub unsafe fn remove_component(&mut self, entity_index: usize) -> Option { + let mut old_comp_ptr = NonNull::new_unchecked(self.data.as_ptr() + .add(entity_index * self.entry_size)); + + let moved_index = if entity_index != self.len - 1 { + let moved_index = self.len - 1; + let mut new_comp_ptr = NonNull::new_unchecked(self.data.as_ptr() + .add(moved_index * self.entry_size)); + + ptr::copy_nonoverlapping(new_comp_ptr.as_ptr(), old_comp_ptr.as_ptr(), self.entry_size); + + mem::swap(&mut old_comp_ptr, &mut new_comp_ptr); // new_comp_ptr is now the old ptr + Some(moved_index) + } else { None }; + + self.len -= 1; + + moved_index + } } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] @@ -97,8 +146,9 @@ impl ArchetypeId { pub struct Archetype { pub(crate) id: ArchetypeId, - pub(crate) entities: Vec, + pub(crate) entities: HashMap, pub(crate) columns: Vec, + capacity: usize, } /// The default capacity of the columns @@ -112,8 +162,9 @@ impl Archetype { Archetype { id: new_id, - entities: Vec::new(), + entities: HashMap::new(), columns, + capacity: DEFAULT_CAPACITY, } } @@ -126,8 +177,14 @@ impl Archetype { where B: Bundle { + if self.capacity == self.entities.len() { + let new_cap = self.capacity * 2; + self.grow_columns(new_cap); + self.capacity = new_cap; + } + let entity_index = self.entities.len(); - self.entities.push(entity); + self.entities.insert(entity, ArchetypeEntityId(entity_index as u64)); bundle.take(|data, type_id, _size| { let col = self.columns.iter_mut().find(|c| c.info.type_id == type_id).unwrap(); @@ -137,8 +194,236 @@ impl Archetype { ArchetypeEntityId(entity_index as u64) } + /// Removes an entity from the Archetype and frees its components. Returns the entity record that took its place in the component column. + pub(crate) fn remove_entity(&mut self, entity: Entity) -> Option<(Entity, ArchetypeEntityId)> { + let entity_index = *self.entities.get(&entity) + .expect("The entity is not in this Archetype!"); + let mut removed_entity: Option<(Entity, ArchetypeEntityId)> = None; + + for c in self.columns.iter_mut() { + let moved_entity = unsafe { c.remove_component(entity_index.0 as usize) }; + + // Make sure that the moved entity is the same as what was moved in other columns. + // If this is the first move, find the EntityId that points to the column index. + // If there wasn't a moved entity, make sure no other columns moved something. + if let Some(res) = moved_entity { + if let Some((_, aid)) = removed_entity { + assert!(res as u64 == aid.0); // make sure we removed the same entity + } else { + let replaced_entity = self.entities.iter().find(|(e, a)| a.0 == res as u64) + .map(|(e, _a)| *e).expect("Failure to find entity for moved component!"); + removed_entity = Some((replaced_entity, ArchetypeEntityId(res as u64))); + } + } else { + assert!(removed_entity.is_none()); + } + } + + // now change the ArchetypeEntityId to be the index that the moved entity was moved into. + removed_entity.map(|(e, a)| (e, entity_index)) + } + /// Returns a boolean indicating whether this archetype can store the TypeIds given pub(crate) fn is_archetype_for(&self, types: Vec) -> bool { self.columns.iter().all(|c| types.contains(&c.info.type_id)) } + + /// Returns a boolean indicating whether this archetype is empty or not. + pub fn is_empty(&self) -> bool { + self.entities.is_empty() + } + + /// Returns the amount of entities that are stored in the archetype. + pub fn len(&self) -> usize { + self.entities.len() + } + + /// Grows columns in the archetype + /// + /// Parameters: + /// * `new_capacity` - The new capacity of components that can fit in this column. + /// + /// # Safety + /// + /// Will panic if new_capacity is less than the current capacity + fn grow_columns(&mut self, new_capacity: usize) { + assert!(new_capacity > self.capacity); + + for c in self.columns.iter_mut() { + unsafe { c.grow(new_capacity); } + } + } +} + +#[cfg(test)] +mod tests { + use rand::Rng; + + use crate::{tests::{Vec2, Vec3}, world::{Entity, EntityId}, bundle::{Bundle, self}}; + + use super::Archetype; + + #[test] + fn one_entity_one_component() { + let bundle = (Vec2::new(10.0, 20.0),); + let entity = Entity { + id: EntityId(0), + generation: 0 + }; + + let mut a = Archetype::from_bundle_info(super::ArchetypeId(0), bundle.info()); + let entity_arch_id = a.add_entity(entity, bundle); + + let col = a.columns.get(0).unwrap(); + let vec2: &Vec2 = unsafe { col.get(entity_arch_id.0 as usize) }; + assert_eq!(vec2.clone(), bundle.0); + } + + #[test] + fn one_entity_two_component() { + let bundle = (Vec2::new(10.0, 20.0),Vec3::new(15.0, 54.0, 84.0)); + let entity = Entity { + id: EntityId(0), + generation: 0 + }; + + let mut a = Archetype::from_bundle_info(super::ArchetypeId(0), bundle.info()); + let entity_arch_id = a.add_entity(entity, bundle); + + let col = a.columns.get(0).unwrap(); + let vec2: &Vec2 = unsafe { col.get(entity_arch_id.0 as usize) }; + assert_eq!(vec2.clone(), bundle.0); + + let col = a.columns.get(1).unwrap(); + let vec3: &Vec3 = unsafe { col.get(entity_arch_id.0 as usize) }; + assert_eq!(vec3.clone(), bundle.1); + } + + #[test] + fn two_entity_one_component() { + let b1 = (Vec2::new(10.0, 20.0),); + let e1 = Entity { + id: EntityId(0), + generation: 0 + }; + let b2 = (Vec2::new(19.0, 43.0),); + let e2 = Entity { + id: EntityId(1), + generation: 0 + }; + + let mut a = Archetype::from_bundle_info(super::ArchetypeId(0), b1.info()); + let earch1 = a.add_entity(e1, b1); + let earch2 = a.add_entity(e2, b2); + + let col = a.columns.get(0).unwrap(); + let vec2: &Vec2 = unsafe { col.get(earch1.0 as usize) }; + assert_eq!(vec2.clone(), b1.0); + let vec2: &Vec2 = unsafe { col.get(earch2.0 as usize) }; + assert_eq!(vec2.clone(), b2.0); + } + + #[test] + fn two_entity_two_component() { + let b1 = (Vec2::new(10.0, 20.0), Vec3::new(84.0, 283.0, 28.0)); + let e1 = Entity { + id: EntityId(0), + generation: 0 + }; + let b2 = (Vec2::new(19.0, 43.0), Vec3::new(74.0, 28.0, 93.0)); + let e2 = Entity { + id: EntityId(1), + generation: 0 + }; + + let mut a = Archetype::from_bundle_info(super::ArchetypeId(0), b1.info()); + let earch1 = a.add_entity(e1, b1); + let earch2 = a.add_entity(e2, b2); + + let col = a.columns.get(0).unwrap(); + let vec2: &Vec2 = unsafe { col.get(earch1.0 as usize) }; + assert_eq!(vec2.clone(), b1.0); + let vec2: &Vec2 = unsafe { col.get(earch2.0 as usize) }; + assert_eq!(vec2.clone(), b2.0); + + let col = a.columns.get(1).unwrap(); + let vec3: &Vec3 = unsafe { col.get(earch1.0 as usize) }; + assert_eq!(vec3.clone(), b1.1); + let vec3: &Vec3 = unsafe { col.get(earch2.0 as usize) }; + assert_eq!(vec3.clone(), b2.1); + } + + #[test] + fn column_growth() { + let mut rng = rand::thread_rng(); + let bundle_count = rng.gen_range(50..150); + + let mut bundles = vec![]; + bundles.reserve(bundle_count); + + let info = (Vec2::new(0.0, 0.0),).info(); + let mut a = Archetype::from_bundle_info(super::ArchetypeId(0), info); + + for i in 0..bundle_count { + let c = (Vec2::new(rng.gen_range(10.0..3000.0), rng.gen_range(10.0..3000.0)),); + bundles.push(c); + + a.add_entity( + Entity { + id: EntityId(i as u64), + generation: 0 + }, + c + ); + } + + let col = a.columns.get(0).unwrap(); + for i in 0..bundle_count { + let vec2: &Vec2 = unsafe { col.get(i) }; + assert_eq!(vec2.clone(), bundles[i].0); + } + } + + #[test] + fn remove_entity() { + let mut rng = rand::thread_rng(); + let range = 30.0..1853.0; + + let bundles = vec![ + ( Vec2::new(rng.gen_range(range.clone()), rng.gen_range(range.clone())), ), + ( Vec2::new(rng.gen_range(range.clone()), rng.gen_range(range.clone())), ), + ( Vec2::new(rng.gen_range(range.clone()), rng.gen_range(range.clone())), ) + ]; + + let info = (Vec2::new(0.0, 0.0),).info(); + let mut a = Archetype::from_bundle_info(super::ArchetypeId(0), info); + + // add the entities to the archetype + for i in 0..bundles.len() { + a.add_entity( + Entity { + id: EntityId(i as u64), + generation: 0 + }, + bundles[i], + ); + } + + // Remove the 'middle' entity in the column + let moved_entity = a.remove_entity( + Entity { + id: EntityId(1u64), + generation: 0 + } + ).expect("No entity was moved"); + + // The last entity in the column should have been moved + assert!(moved_entity.0.id.0 == 2); + assert!(moved_entity.1.0 == 1); + + // make sure that the entities' component was actually moved in the column + let col = &a.columns[0]; + let comp = unsafe { col.get::(1) }; + assert_eq!(comp.clone(), bundles[2].0); + } } \ No newline at end of file diff --git a/lyra-ecs/src/query/borrow.rs b/lyra-ecs/src/query/borrow.rs index 99ec9dc..7bdbd30 100644 --- a/lyra-ecs/src/query/borrow.rs +++ b/lyra-ecs/src/query/borrow.rs @@ -2,6 +2,7 @@ use std::{marker::PhantomData, any::TypeId, ptr::NonNull}; use super::{Fetch, Query, AsQuery, DefaultQuery}; +/// Fetcher for borrowing components from archetypes. pub struct FetchBorrow<'a, T> { ptr: NonNull, size: usize, @@ -30,6 +31,14 @@ where } } +/// A Query for borrowing components from archetypes. +/// +/// Since [`AsQuery`] is implemented for `&T`, you can use this query like this: +/// ```rust +/// for ts in world.view::<&T>() { +/// println!("Got an &T!"); +/// } +/// ``` pub struct QueryBorrow { type_id: TypeId, _phantom: PhantomData @@ -89,6 +98,7 @@ impl DefaultQuery for &T { } } +/// A fetcher for mutably borrowing components from archetypes. pub struct FetchBorrowMut<'a, T> { ptr: NonNull, size: usize, @@ -117,6 +127,14 @@ where } } +/// A Query for mutably borrowing components from archetypes. +/// +/// Since [`AsQuery`] is implemented for `&mut T`, you can use this query like this: +/// ```rust +/// for ts in world.view::<&mut T>() { +/// println!("Got an &T!"); +/// } +/// ``` pub struct QueryBorrowMut { type_id: TypeId, _phantom: PhantomData diff --git a/lyra-ecs/src/query/entities.rs b/lyra-ecs/src/query/entities.rs index 6156a1e..c85e773 100644 --- a/lyra-ecs/src/query/entities.rs +++ b/lyra-ecs/src/query/entities.rs @@ -2,11 +2,11 @@ use crate::{world::Entity, archetype::{Archetype, ArchetypeId}}; use super::{Fetch, Query}; -pub struct EntitiesFetch<'a> { - entities: &'a [Entity], +pub struct EntitiesFetch { + entities: Vec, } -impl<'a> Fetch<'a> for EntitiesFetch<'a> { +impl<'a> Fetch<'a> for EntitiesFetch { type Item = Entity; unsafe fn get_item(&mut self, entity: crate::world::ArchetypeEntityId) -> Self::Item { @@ -16,7 +16,7 @@ impl<'a> Fetch<'a> for EntitiesFetch<'a> { fn dangling() -> Self { Self { - entities: &[], + entities: vec![], } } } @@ -26,7 +26,7 @@ pub struct Entities; impl Query for Entities { type Item<'a> = Entity; - type Fetch<'a> = EntitiesFetch<'a>; + type Fetch<'a> = EntitiesFetch; fn can_visit_archetype(&self, archetype: &Archetype) -> bool { let _ = archetype; // ignore unused warnings @@ -36,7 +36,7 @@ impl Query for Entities { unsafe fn fetch<'a>(&self, arch_id: ArchetypeId, archetype: &'a Archetype) -> Self::Fetch<'a> { let _ = arch_id; // ignore unused warnings EntitiesFetch { - entities: &archetype.entities, + entities: archetype.entities.keys().cloned().collect::>(), } } } \ No newline at end of file diff --git a/lyra-ecs/src/tests.rs b/lyra-ecs/src/tests.rs index 589db5b..b03fc6c 100644 --- a/lyra-ecs/src/tests.rs +++ b/lyra-ecs/src/tests.rs @@ -1,3 +1,5 @@ +use rand::Rng; + /// This source file includes some common things that tests are using. #[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)] @@ -13,6 +15,16 @@ impl Vec2 { y, } } + + pub fn rand() -> Self { + let mut rng = rand::thread_rng(); + let range = 30.0..1853.0; + + Vec2 { + x: rng.gen_range(range.clone()), + y: rng.gen_range(range) + } + } } #[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)] diff --git a/lyra-ecs/src/world.rs b/lyra-ecs/src/world.rs index 25278e9..1c0dfb1 100644 --- a/lyra-ecs/src/world.rs +++ b/lyra-ecs/src/world.rs @@ -12,8 +12,8 @@ pub struct ArchetypeEntityId(pub u64); #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct Entity { - id: EntityId, - generation: u64, + pub(crate) id: EntityId, + pub(crate) generation: u64, } pub struct Record { @@ -101,6 +101,18 @@ impl World { new_entity } + /// Despawn an entity from the World + pub fn despawn(&mut self, entity: Entity) { + if let Some(record) = self.entity_index.get_mut(&entity.id) { + let arch = self.archetypes.get_mut(&record.id).unwrap(); + + if let Some((moved, new_index)) = arch.remove_entity(entity) { + // replace the archetype index of the moved index with its new index. + self.entity_index.get_mut(&moved.id).unwrap().index = new_index; + } + } + } + pub fn view<'a, T: 'static + Component + DefaultQuery>(&'a self) -> ViewIter<'a, T::Query> { let archetypes = self.archetypes.values().collect(); let v = View::new(T::default_query(), archetypes); @@ -146,4 +158,17 @@ mod tests { } assert!(count == 3); } + + #[test] + fn despawn_entity() { + let mut world = World::new(); + world.spawn((Vec2::rand(),)); + let middle_en = world.spawn((Vec2::rand(),)); + let last_en = world.spawn((Vec2::rand(),)); + + world.despawn(middle_en); + + let record = world.entity_index.get(&last_en.id).unwrap(); + assert_eq!(record.index.0, 1); + } } \ No newline at end of file