Skip to content

Commit

Permalink
Add World cloning example and convenience support APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
caspark authored Jul 7, 2024
1 parent ed23ded commit 1ffdca9
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 6 deletions.
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -100,7 +113,7 @@
themselves.

# 0.7.7

### Added
- `Entity::DANGLING` convenience constant

Expand Down
141 changes: 141 additions & 0 deletions examples/cloning.rs
Original file line number Diff line number Diff line change
@@ -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<ComponentCloneMetadata>,
}

impl WorldCloner {
pub fn register<T: Component + Clone>(&mut self) {
self.registry.insert(
TypeId::of::<T>(),
ComponentCloneMetadata {
type_info: TypeInfo::of::<T>(),
insert_into_batch_func: &|src, dest| {
let mut column = dest.writer::<T>().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::<Vec<_>>();
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::<i32>();
cloner.register::<String>();

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::<u8>(),
"original world entity has u8 component"
);
assert!(
!world1
.entity(entity3)
.expect("w1 entity3 should exist")
.has::<u8>(),
"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::<AllRegisteredComponentsQuery>());
assert!(w1_e.satisfies::<AllRegisteredComponentsQuery>());

assert_eq!(
w0_e.query::<AllRegisteredComponentsQuery>().get().unwrap(),
w1_e.query::<AllRegisteredComponentsQuery>().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::<SomeRegisteredComponentsQuery>());
assert!(w1_e.satisfies::<SomeRegisteredComponentsQuery>());

assert_eq!(
w0_e.query::<SomeRegisteredComponentsQuery>().get().unwrap(),
w1_e.query::<SomeRegisteredComponentsQuery>().get().unwrap()
);
}
4 changes: 2 additions & 2 deletions src/archetype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down Expand Up @@ -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<V> = HashMap<TypeId, V, BuildHasherDefault<TypeIdHasher>>;
pub type TypeIdMap<V> = HashMap<TypeId, V, BuildHasherDefault<TypeIdHasher>>;

struct OrderedTypeIdMap<V>(Box<[(TypeId, V)]>);

Expand Down
6 changes: 6 additions & 0 deletions src/batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 1 addition & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down

0 comments on commit 1ffdca9

Please sign in to comment.