From 6417edb00ef7bc9b3c663ce317037b99f9a3e060 Mon Sep 17 00:00:00 2001 From: Caspar Krieger Date: Sat, 6 Jul 2024 16:02:01 +0800 Subject: [PATCH] Use type erasure to DRY component registration --- examples/cloning.rs | 138 ++++++++++++++++++++++++++++---------------- src/archetype.rs | 4 +- src/batch.rs | 6 ++ src/lib.rs | 4 +- 4 files changed, 96 insertions(+), 56 deletions(-) diff --git a/examples/cloning.rs b/examples/cloning.rs index 8e8b577b..7c798f3b 100644 --- a/examples/cloning.rs +++ b/examples/cloning.rs @@ -1,71 +1,88 @@ -use hecs::{ColumnBatchBuilder, ColumnBatchType, Component, World}; - -fn maybe_add_type_to_batch( - archetype: &hecs::Archetype, - batch_type: &mut ColumnBatchType, -) { - if archetype.has::() { - batch_type.add::(); - } -} +use std::any::TypeId; -fn maybe_clone_column( - archetype: &hecs::Archetype, - batch: &mut ColumnBatchBuilder, -) { - if let Some((column, mut writer)) = archetype.get::<&T>().zip(batch.writer::()) { - for item in column.iter() { - if let Err(_) = writer.push(item.clone()) { - unreachable!("push should always succeed since batch was sized to match archetype"); - } - } - } +use hecs::{Archetype, ColumnBatchBuilder, ColumnBatchType, Component, TypeIdMap, TypeInfo, World}; + +struct ComponentCloneMetadata { + type_info: TypeInfo, + func: &'static dyn Fn(&Archetype, &mut ColumnBatchBuilder), } -/// Clones world entities along with a hardcoded list of components. +/// Clones world entities along with registered components when [Self::clone_world()] is called. /// -/// Components not included are omitted from the cloned world. +/// Unregistered components are omitted from the cloned world. Entities containing unregistered +/// components will still be cloned. /// /// Note that entity allocator state may differ in the cloned world - so for example a new entity /// spawned in each world may end up with different entity ids, entity iteration order may be /// different, etc. -fn clone_world(world: &World) -> World { - let mut cloned = World::new(); - - for archetype in world.archetypes() { - let mut batch = ColumnBatchType::new(); - // types have to be listed one by one here - maybe_add_type_to_batch::(archetype, &mut batch); - maybe_add_type_to_batch::(archetype, &mut batch); - - let mut batch = batch.into_batch(archetype.ids().len() as u32); - // and types need to be listed again here - maybe_clone_column::(archetype, &mut batch); - maybe_clone_column::(archetype, &mut batch); - - let batch = batch.build().expect("batch should be complete"); - - let handles = &cloned - .reserve_entities(archetype.ids().len() as u32) - .collect::>(); - cloned.flush(); - cloned.spawn_column_batch_at(handles, batch); +#[derive(Default)] +struct WorldCloner { + registry: TypeIdMap, +} + +impl WorldCloner { + pub fn register(&mut self) { + self.registry.insert(TypeId::of::(), ComponentCloneMetadata { + type_info: TypeInfo::of::(), + func : &|src, dest| { + let mut column = dest.writer::().unwrap(); + for component in &*src.get::<&T>().unwrap() { + _ = column.push(component.clone()); + } + }}); } - cloned + fn clone_world(&self, world: &World) -> World { + let mut cloned = World::new(); + + for archetype in world.archetypes() { + let mut batch = ColumnBatchType::new(); + + for (&k, v) in self.registry.iter() { + if archetype.has_dynamic(k) { + batch.add_dynamic(v.type_info); + } + } + let mut batch = batch.into_batch(archetype.ids().len() as u32); + + for (&k, v) in self.registry.iter() { + if archetype.has_dynamic(k) { + (v.func)(archetype, &mut batch) + } + } + + let batch = batch.build().expect("batch should be complete"); + + let handles = &cloned + .reserve_entities(archetype.ids().len() as u32) + .collect::>(); + cloned.flush(); + cloned.spawn_column_batch_at(handles, batch); + } + + cloned + } } + pub fn main() { let int0 = 0; let int1 = 1; let str0 = "Ada".to_owned(); let str1 = "Bob".to_owned(); + let str2 = "Cal".to_owned(); let mut world0 = World::new(); let entity0 = world0.spawn((int0, str0)); let entity1 = world0.spawn((int1, str1)); + let entity2 = world0.spawn((str2,)); + let entity3 = world0.spawn((0u8,)); // unregistered component + + let mut cloner = WorldCloner::default(); + cloner.register::(); + cloner.register::(); - let world1 = clone_world(&world0); + let world1 = cloner.clone_world(&world0); assert_eq!( world0.len(), @@ -73,17 +90,36 @@ pub fn main() { "cloned world should have same entity count as original world" ); - type AllComponentsQuery = (&'static i32, &'static String); + // NB: unregistered components don't get cloned + assert!(world0.entity(entity3).expect("w0 entity3 should exist").has::(), + "original world entity has u8 component"); + assert!(!world1.entity(entity3).expect("w1 entity3 should exist").has::(), + "cloned world entity does not have u8 component because it was not registered"); + type AllRegisteredComponentsQuery = (&'static i32, &'static String); for entity in [entity0, entity1] { let w0_e = world0.entity(entity).expect("w0 entity should exist"); let w1_e = world1.entity(entity).expect("w1 entity should exist"); - assert!(w0_e.satisfies::()); - assert!(w1_e.satisfies::()); + assert!(w0_e.satisfies::()); + assert!(w1_e.satisfies::()); + + assert_eq!( + w0_e.query::().get().unwrap(), + w1_e.query::().get().unwrap() + ); + } + + type SomeRegisteredComponentsQuery = (&'static String,); + for entity in [entity2] { + let w0_e = world0.entity(entity).expect("w0 entity should exist"); + let w1_e = world1.entity(entity).expect("w1 entity should exist"); + assert!(w0_e.satisfies::()); + assert!(w1_e.satisfies::()); assert_eq!( - w0_e.query::().get().unwrap(), - w1_e.query::().get().unwrap() + w0_e.query::().get().unwrap(), + w1_e.query::().get().unwrap() ); } + } diff --git a/src/archetype.rs b/src/archetype.rs index 09ec6b78..d7b844cf 100644 --- a/src/archetype.rs +++ b/src/archetype.rs @@ -454,7 +454,7 @@ struct Data { /// TypeId is already thoroughly hashed, so there's no reason to hash it again. /// Just leave the bits unchanged. #[derive(Default)] -pub(crate) struct TypeIdHasher { +pub struct TypeIdHasher { hash: u64, } @@ -491,7 +491,7 @@ impl Hasher for TypeIdHasher { /// Because TypeId is already a fully-hashed u64 (including data in the high seven bits, /// which hashbrown needs), there is no need to hash it again. Instead, this uses the much /// faster no-op hash. -pub(crate) type TypeIdMap = HashMap>; +pub type TypeIdMap = HashMap>; struct OrderedTypeIdMap(Box<[(TypeId, V)]>); diff --git a/src/batch.rs b/src/batch.rs index fec87f50..ecb40d67 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -24,6 +24,12 @@ impl ColumnBatchType { self } + /// [Self::add()] but using type information determined at runtime via [TypeInfo::of()] + pub fn add_dynamic(&mut self, id: TypeInfo) -> &mut Self { + self.types.push(id); + self + } + /// Construct a [`ColumnBatchBuilder`] for *exactly* `size` entities with these components pub fn into_batch(self, size: u32) -> ColumnBatchBuilder { let mut types = self.types.into_sorted_vec(); diff --git a/src/lib.rs b/src/lib.rs index dd077861..68a6d8de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,7 +84,7 @@ pub mod serialize; mod take; mod world; -pub use archetype::{Archetype, ArchetypeColumn, ArchetypeColumnMut}; +pub use archetype::{Archetype, ArchetypeColumn, ArchetypeColumnMut, TypeIdMap, TypeInfo}; pub use batch::{BatchIncomplete, BatchWriter, ColumnBatch, ColumnBatchBuilder, ColumnBatchType}; pub use bundle::{ bundle_satisfies_query, dynamic_bundle_satisfies_query, Bundle, DynamicBundle, @@ -109,8 +109,6 @@ pub use world::{ // Unstable implementation details needed by the macros #[doc(hidden)] -pub use archetype::TypeInfo; -#[doc(hidden)] pub use bundle::DynamicClone; #[doc(hidden)] pub use query::Fetch;