Open alice-i-cecile opened 21 hours ago
So, I'm fully in favor of this API. It's useful, simple, and doesn't have the same ecosystem compatibility hazards as a general "spawn entity at this ID" API. Thank you for explaining the exact pattern and application :)
I think that by exposing this as a first-party feature we can achieve much better performance, and expose options like "only clone cloneable components" and so on. Many of the same concerns as #1515 apply here, and we may want to tackle both at once.
@chengts95, are you able to migrate to Bevy 0.15 at all? If not, we should reintroduce some deprecated APIs in a point release, and then only remove them once this functionality is covered properly.
As far as I'm aware the API's are deprecated, but are still in the code. If you ignore the deprecation warnings it should still work until the point where we actually remove it.
Cloning a world with identical data and entities is useful for many applications. For instance, if bevy is used as an industrial simulation platform, multiple case studies may run at the same time with identical data. However, it relies on entities and should spawn identical ids of entities, otherwise it is not a clone.
To highlight the performance benefits of ECS, it should be possible to do bulk insertion and even construct a whole or sub archetype in one shot. The current design relies on reflection, which does much more than serialization/deserialization and data copy. Thus, although it is difficult to clone an archetype for many pratical reasons, a Clone
based solution is still fesible.
That is the component registry proposed for clone, working for components with/without Clone trait:
use bevy_ecs::{component::Component, world::World};
use bevy_utils::hashbrown::HashMap;
use std::any::TypeId;
/// A dynamic cloning function type that operates on two Worlds.
/// It facilitates copying components from a source World to a destination World.
type CloneFn = Box<dyn Fn(&World, &mut World) + Send + Sync>;
/// A registry for managing component cloning functions.
/// This enables the dynamic registration and handling of components during world cloning.
pub struct ComponentRegistry {
clone_fns: HashMap<TypeId, CloneFn>, // A mapping from TypeId to the associated clone function.
}
/// A generic wrapper type for components.
/// This can be used to encapsulate components that need additional handling.
#[derive(Clone, Component)]
pub struct CWrapper<T>(pub T);
/// A macro to register multiple component types at once with the registry.
#[macro_export]
macro_rules! register_components {
($registry:expr, $($component:ty),*) => {
$(
$registry.register::<$component>();
)*
};
}
/// Clones entities from a source World to a destination World.
/// This function preserves entity relationships but does not copy components.
pub fn clone_entities(src_world: &World, dst_world: &mut World) {
let ents = src_world.iter_entities();
let mut cmd = dst_world.commands();
let v: Vec<_> = ents.map(|x| (x.id(), ())).collect();
cmd.insert_or_spawn_batch(v.into_iter());
dst_world.flush();
}
impl ComponentRegistry {
/// Creates a new ComponentRegistry.
pub fn new() -> Self {
Self {
clone_fns: HashMap::new(),
}
}
/// Clones all registered components from the source World to the destination World.
pub fn clone_world(&self, src_world: &World, dst_world: &mut World) {
for (_type_id, clone_fn) in &self.clone_fns {
clone_fn(src_world, dst_world);
}
}
/// Registers a component type `T` that implements `Clone`.
/// This allows the component to be dynamically cloned between worlds.
pub fn register<T: Component + Clone + 'static>(&mut self) {
let type_id = TypeId::of::<T>();
// Save the cloning function for this component type.
self.clone_fns.insert(
type_id,
Box::new(move |src_world: &World, dst_world: &mut World| {
let comps = src_world.components();
if let Some(comp_id) = comps.get_id(type_id) {
let ents_to_copy = src_world
.archetypes()
.iter()
.filter(|a| a.contains(comp_id)) // Filter Archetypes containing the component.
.flat_map(|a| {
a.entities().iter().filter_map(|e| {
let eid = e.id();
src_world
.entity(eid)
.get::<T>()
.map(|comp| (eid, comp.clone()))
})
});
dst_world
.insert_or_spawn_batch(ents_to_copy.into_iter())
.unwrap();
}
}),
);
}
/// Registers a component type `T` with a custom cloning handler.
/// The handler defines how the component should be cloned dynamically.
pub fn register_with_handler<T, F>(&mut self, handler: F)
where
T: Component + 'static,
F: Fn(&T) -> T + 'static + Sync + Send,
{
let type_id = TypeId::of::<T>();
self.clone_fns.insert(
type_id,
Box::new(move |src_world: &World, dst_world: &mut World| {
let comps = src_world.components();
if let Some(comp_id) = comps.get_id(type_id) {
let ents_to_copy = src_world
.archetypes()
.iter()
.filter(|a| a.contains(comp_id)) // Filter Archetypes containing the component.
.flat_map(|a| {
a.entities().iter().filter_map(|e| {
let eid = e.id();
src_world
.entity(eid)
.get::<T>()
.map(|comp| (eid, handler(comp)))
})
});
dst_world
.insert_or_spawn_batch(ents_to_copy.into_iter())
.unwrap();
}
}),
);
}
/// Retrieves the clone function for a specific component type by its `TypeId`.
pub fn get_clone_fn(&self, type_id: TypeId) -> Option<&CloneFn> {
self.clone_fns.get(&type_id)
}
}
#[cfg(test)]
mod tests {
use super::*;
use bevy_ecs::prelude::*;
use bevy_hierarchy::{BuildChildren, ChildBuild, Children, Parent};
use bevy_reflect::FromReflect;
#[derive(Component, Clone)]
struct Position {
x: f32,
y: f32,
}
#[derive(Component, Clone)]
struct Velocity {
dx: f32,
dy: f32,
}
#[test]
fn test_component_registry_he() {
let mut registry = ComponentRegistry::new();
registry.register_with_handler::<Children, _>(|comp| {
//println!("Handled Children: {:?}", comp);
Children::from_reflect(comp).unwrap()
});
registry.register_with_handler::<Parent, _>(|comp| {
//println!("Handled Parent: {:?}", comp);
Parent::from_reflect(comp).unwrap()
});
let mut src_world = World::new();
let parent_entity = src_world.spawn_empty();
let pid = parent_entity.id();
src_world.entity_mut(pid).with_children(|builder| {
for _ in 0..7 {
builder.spawn_empty().with_children(|builder| {
builder.spawn_empty();
});
}
});
println!("Source World:");
for entity in src_world.iter_entities() {
println!("{:?}", entity.id());
if let Some(children) = entity.get::<Children>() {
println!("-Children: {:?}", children);
}
if let Some(parent) = entity.get::<Parent>() {
println!("-Parent: {:?}", parent);
}
}
let mut dst_world = World::new();
registry.clone_world(&src_world, &mut dst_world);
println!("\nCloned World:");
for entity in dst_world.iter_entities() {
println!("{:?}", entity.id());
if let Some(children) = entity.get::<Children>() {
println!(
"-Children: {:?}",
children
);
}
if let Some(parent) = entity.get::<Parent>() {
println!("-Parent: {:?}", parent);
}
}
let src_child = src_world.entity(pid).get::<Children>().unwrap();
let dst_child = dst_world.entity(pid).get::<Children>().unwrap();
src_child
.iter()
.zip(dst_child.iter())
.for_each(|(src, dst)| {
assert_eq!(src, dst);
});
}
#[test]
fn test_component_registry() {
let mut registry = ComponentRegistry::new();
registry.register::<Position>();
registry.register::<Velocity>();
let mut src_world = World::new();
let entity = src_world.spawn((Position { x: 1.0, y: 2.0 }, Velocity { dx: 0.5, dy: 1.0 }));
let r_id = entity.id();
let mut dst_world = World::new();
registry.clone_world(&src_world, &mut dst_world);
let cloned_entity = dst_world.entity(r_id);
let position = cloned_entity.get::<Position>().unwrap();
assert_eq!(position.x, 1.0);
assert_eq!(position.y, 2.0);
let velocity = cloned_entity.get::<CWrapper<Velocity>>().unwrap();
assert_eq!(velocity.0.dx, 0.5);
assert_eq!(velocity.0.dy, 1.0);
}
}
Then register the types similar to reflection. Because the clone trait or handle functions must be implemented for the T in the registry, it won't fail and can be controlled by users. Since the world is cloned, all existing ids are duplicated and there is no risk for entity id collision. Such registry can be outside of the default App so that this feature is fully optional and managed by users.
It can be modified for mutiple purposes. For example, if an entity remap object is presented, we can have an extra API in registry so that the clone functions can correctly copy data from source world to target world. Or when we only want to duplicate the changed data from source world to target world, it can directly obtain all entities with desired components and send all the changes within one command. An API to do bulk insertion/deletion on multiple entities is desired for this purpose. For example, if a multi-layer tilemap is built with each tile as an entity, and there is no way to copy and move tilemap data efficiently, the remap hashmap will consume insane memory without any performance benefits compared to traditional dense/sparse matrix.
As many applications can ensure consistent entity ids (in common sense, entity remapping is an advanced optional feature instead of banning sharing entity ids between worlds) and a huge remapping hashmap is not fesible for the scenarios that can benefit from ECS, I believe this feature and related concerns should be noticed. I believe when I want to use ECS, I am not using it for 100-1000 plain objects that is not at all a problem for OOP game engine, but simulating 10k+ entities.
I like that approach. I think that the reflection-backed approach is a good, uncontroversial approach to support cloning both entities and worlds.
And yeah, since we're cloning the entire world at once we can safely assume the entity ids match without remapping. Entity cloning / hierarchy cloning needs more care though: you generally won't want to clone an entity tree and have them match up to the old parents :(
So, I'm fully in favor of this API. It's useful, simple, and doesn't have the same ecosystem compatibility hazards as a general "spawn entity at this ID" API. Thank you for explaining the exact pattern and application :)
I think that by exposing this as a first-party feature we can achieve much better performance, and expose options like "only clone cloneable components" and so on. Many of the same concerns as #1515 apply here, and we may want to tackle both at once.
@chengts95, are you able to migrate to Bevy 0.15 at all? If not, we should reintroduce some deprecated APIs in a point release, and then only remove them once this functionality is covered properly.
I can use 0.15 because the API is still there. The new external reflection is also very nice. But reflection is overkill for clone, serialization, and deserialization, it is more about runtime object information.
Glad to here that the deprecation strategy served its purpose: prompting users to complain loudly is exactly why we do it. Let's get this in, and then run it by you and other users before removing them fully.
I like that approach. I think that the reflection-backed approach is a good, uncontroversial approach to support cloning both entities and worlds.
And yeah, since we're cloning the entire world at once we can safely assume the entity ids match without remapping. Entity cloning / hierarchy cloning needs more care though: you generally won't want to clone an entity tree and have them match up to the old parents :(
That is also why I am confused, if the entity id is not going to be consistent, how can we access the children and parents without complex remapping.
In my implementation, because the IDs are identical between worlds, the children and parents are copied without problems I think.
Originally posted by @chengts95 in #15459