diff --git a/CHANGELOG.md b/CHANGELOG.md index 73adf037..4487589b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# Unreleased + +### Added + +- `examples/cloning` is a new example showing how to clone `World` with some or all components +- Added `ColumnBatchType::add_dynamic()` to allow construction of batches for bulk insertion of + component data into archetypes. This is useful for inserting data into archetypes where type + information for each component is only available at runtime - e.g. the cloning World example. + +### Changed + +- `TypeIdMap` and `TypeInfo` are now public to facilitate easy cloning of `World` + # 0.10.5 ### Added @@ -100,7 +113,7 @@ themselves. # 0.7.7 - + ### Added - `Entity::DANGLING` convenience constant diff --git a/examples/cloning.rs b/examples/cloning.rs new file mode 100644 index 00000000..e1278061 --- /dev/null +++ b/examples/cloning.rs @@ -0,0 +1,141 @@ +//! This example demonstrates using the [ColumnBatch][hecs::ColumnBatch] API to efficiently clone +//! the entities in a [World] along with some or all components. +//! +//! Note that the cloned world may have different iteration order and/or newly created entity ids +//! may diverge between the original and newly created worlds. If that is a dealbreaker for you, +//! see https://github.com/Ralith/hecs/issues/332 for some pointers on preserving entity allocator +//! state; as of time of writing, you'll need to patch `hecs`. + +use std::any::TypeId; + +use hecs::{Archetype, ColumnBatchBuilder, ColumnBatchType, Component, TypeIdMap, TypeInfo, World}; + +struct ComponentCloneMetadata { + type_info: TypeInfo, + insert_into_batch_func: &'static dyn Fn(&Archetype, &mut ColumnBatchBuilder), +} + +/// Clones world entities along with registered components when [Self::clone_world()] is called. +/// +/// 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. +#[derive(Default)] +struct WorldCloner { + registry: TypeIdMap, +} + +impl WorldCloner { + pub fn register(&mut self) { + self.registry.insert( + TypeId::of::(), + ComponentCloneMetadata { + type_info: TypeInfo::of::(), + insert_into_batch_func: &|src, dest| { + let mut column = dest.writer::().unwrap(); + for component in &*src.get::<&T>().unwrap() { + _ = column.push(component.clone()); + } + }, + }, + ); + } + + fn clone_world(&self, world: &World) -> World { + let mut cloned = World::new(); + + for archetype in world.archetypes() { + let mut batch_type = ColumnBatchType::new(); + for (&type_id, clone_metadata) in self.registry.iter() { + if archetype.has_dynamic(type_id) { + batch_type.add_dynamic(clone_metadata.type_info); + } + } + + let mut batch_builder = batch_type.into_batch(archetype.ids().len() as u32); + for (&type_id, clone_metadata) in self.registry.iter() { + if archetype.has_dynamic(type_id) { + (clone_metadata.insert_into_batch_func)(archetype, &mut batch_builder) + } + } + + let batch = batch_builder.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 = cloner.clone_world(&world0); + + assert_eq!( + world0.len(), + world1.len(), + "cloned world should have same entity count as original world" + ); + + // 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_eq!( + w0_e.query::().get().unwrap(), + w1_e.query::().get().unwrap() + ); + } + + type SomeRegisteredComponentsQuery = (&'static String,); + let w0_e = world0.entity(entity2).expect("w0 entity2 should exist"); + let w1_e = world1.entity(entity2).expect("w1 entity2 should exist"); + assert!(w0_e.satisfies::()); + assert!(w1_e.satisfies::()); + + assert_eq!( + 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;