-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Add an API to clone worlds #16559
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
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 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);
}
} 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 :( |
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. |
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. |
I experimented with directly cloning components from archetype tables instead of querying individual entities from the Benchmark Setup:
Results:
This optimization is achieved by bypassing the overhead of accessing entity metadata and directly cloning components from Code Snippet:The updated implementation dynamically registers component types and handles cloning via archetype tables: pub fn register<T: Component + Clone + 'static>(&mut self) {
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();
let storage = src_world.storages();
if let Some(comp_id) = comps.get_id(type_id) {
let info = unsafe { comps.get_info_unchecked(comp_id) };
src_world
.archetypes()
.iter()
.filter(|a| a.contains(comp_id))
.for_each(|a| {
let mut cmd = dst_world.commands();
clone_archetype::<T>(storage, info, &mut cmd, a, comp_id);
});
dst_world.flush();
}
}),
);
}
fn clone_archetype<T: Component + Clone + 'static>(
storage: &bevy_ecs::storage::Storages,
info: &bevy_ecs::component::ComponentInfo,
cmd: &mut bevy_ecs::system::Commands<'_, '_>,
a: &bevy_ecs::archetype::Archetype,
comp_id: bevy_ecs::component::ComponentId,
) {
match info.storage_type() {
bevy_ecs::component::StorageType::Table => {
table_based_clone::<T>(storage, cmd, a, comp_id);
}
bevy_ecs::component::StorageType::SparseSet => {
sparse_set_clone::<T>(storage, cmd, a, comp_id);
}
}
}
fn sparse_set_clone<T: Component + Clone + 'static>(
storage: &bevy_ecs::storage::Storages,
cmd: &mut bevy_ecs::system::Commands<'_, '_>,
a: &bevy_ecs::archetype::Archetype,
comp_id: bevy_ecs::component::ComponentId,
) {
storage.sparse_sets.get(comp_id).map(|x| {
let data: Vec<_> = a
.entities()
.iter()
.filter_map(|e| {
x.get(e.id())
.map(|data| (e.id(), unsafe { data.deref::<T>().clone() }))
})
.collect();
cmd.insert_or_spawn_batch(data);
});
}
fn table_based_clone<T: Component + Clone + 'static>(
storage: &bevy_ecs::storage::Storages,
cmd: &mut bevy_ecs::system::Commands<'_, '_>,
a: &bevy_ecs::archetype::Archetype,
comp_id: bevy_ecs::component::ComponentId,
) {
let table = &storage.tables[a.table_id()];
let data: Vec<_> = table
.entities()
.iter()
.zip(unsafe { table.get_data_slice_for::<T>(comp_id).unwrap_or_default() })
.map(|(e, x)| {
(e.clone(), unsafe {
x.get().as_ref().unwrap_unchecked().clone()
})
})
.collect();
cmd.insert_or_spawn_batch(data);
} Conclusion:The table-based cloning method significantly reduces the time for cloning large numbers of entities with components. This approach avoids the overhead of querying the This only gets an approximate bandwidth of 480MB/s, which is far from optimal. I expect faster data copy with lower-level APIs so the data synchronization between apps can be more efficient |
I would benefit from "bit for bit identical" world cloning too: I do rollback networking, with my entire rollback-able game state in a separate ECS world1; having world-clones be identical in both components and entity ids (including the entity freelist, for future spawns) lets me rely on unstable-yet-still-cross-platform-deterministic iteration order through entities without having to do expensive things like sort the result of each and every query. With Bevy 0.16 specifically gaining first-class support for (non-reflection-based) entity cloning, I was hoping I would be able to use that to copy entities between worlds - but alas it seems not. Still, the archetype-based approach above from Bevy 0.15 does still seem to work in 0.16 based on a very quick test - and it seems like now that Bevy has gained a first party way to specify how components should be cloned, it might be possible to reuse that for actually cloning each component in an archetype, without having to register their clone functions in a separate component registry.. (although, hmm, from a quick glance at #16132's diff, it looks like @alice-i-cecile are you still on board with enabling this world clone use case? (including un-deprecating If so, I could possibly see whether any of the new entity cloning plumbing can be reused, and/or try adding full clone support to World directly. Footnotes
|
I still can use the clone function i manually wrote in bevy 0.16, could you tell why it is not working for you? |
The biggest problem was guaranteeing that |
I would like to enable this use case, but I don't want to use that mechanism and would like to see an API specifically designed for this. |
Now I avoid using any insert_or_spawn_batch when I want to clone or load a snapshot. However, it has to pre allocate/ ensure all the entity ids before cloning. for e in &snapshot.entities {
max_id = max_id.max(e.id);
}
world.entities().reserve_entities((max_id + 1) as u32);
world.flush();
//then we can clone with this
fn table_based_clone<T: Component + Clone + 'static>(
storage: &bevy_ecs::storage::Storages,
cmd: &mut bevy_ecs::system::Commands<'_, '_>,
a: &bevy_ecs::archetype::Archetype,
comp_id: bevy_ecs::component::ComponentId,
) {
let table = &storage.tables[a.table_id()];
let data: Vec<_> = table
.entities()
.iter()
.zip(unsafe { table.get_data_slice_for::<T>(comp_id).unwrap_or_default() })
.map(|(e, x)| {
(e.clone(), unsafe {
x.get().as_ref().unwrap_unchecked().clone()
})
})
.collect();
cmd.insert_batch(data);
} I am wondering if it is a correct way to use |
@chengts95 yes, your cloning approach does work in Bevy 0.16 - that's what I meant when I wrote "the archetype-based approach above from Bevy 0.15 does still seem to work in 0.16 based on a very quick test". What I was saying that I hoped it could be improved by using 0.16's new entity cloning machinery. Sorry if that wasn't clear.
Good to know!
Okay, fair: I see that cloning a world using this approach when there's a large freelist would indeed result in accidentally quadratic performance due to the per entity linear scan in So, in terms of an API specifically designed for cloning worlds: I actually implemented that in my own hecs fork and it looks like this. Basically:
How does something along those lines sound @alice-i-cecile ? (You could complicate/elaborate on such an approach by e.g. letting people remap entity ids or letting them specify that entities in the new world should be "compacted" at clone time, but I'd be inclined to build those as separate capabilities.)
@chengts95 if that approach works for you then great! But I'm not sure it's completely right? As I understand it, your approach is:
Assuming I've got that right, then I'm not sure these two cases are handled correctly:
(I might have this wrong as I don't know Bevy's internals well - feel free to correct me as needed folks) So.. maybe a DIY approach like that could be made to work without having |
This API design is very similar to what I attempted to do in #17316. I currently don't have the time to finish it, but feel free to utilize anything (or nothing) from that PR. The cloning handler api changed a bit since then, but it should still be mostly compatible |
First, when cloning a world, deleted entities are intentionally not included. Second, entity generations are expected to differ, and that’s by design. I prefer not modifiying bevy's internal design to achieve this functionality since the type registry is good enough to handle these. And it doesn't need to know the component id. In my opinion, make every struct clonable is not possible in the practice. |
@eugineerd , oh, thanks! Somehow I completely overlooked that PR when reading this issue. @chengts95 I fear we are talking past each other somewhat. To try and clear things up, I've taken your code samples and written a test for both case A and case B: https://gist.github.com/caspark/9e734acd4309628ab7b8861dc33443ca :
So in other words: your 0.15/0.16 approach that uses Lastly, I notice that you have objected a few times to "all components must be cloneable or the clone will fail", so let me reiterate what I wrote earlier: "I am happy to be guided [as to whether the clone should fail or just skip entities that can't be cloned], or maybe we can let the user choose when they clone the world." (Though as my preference is the opposite to yours, I'd suggest it makes sense to let the user choose.) |
Ok I understand your problem now. The problem is ,for me I only care about components and archetype. This is not what I can fix since the bevy will not have such API to only allocate specific ids now (as i said at the top of the issue it is actually not good). An easy solution will be you synchornize a The problem for the all compoents to have "cloneable" is with the internal Clone traits intergrated into bevy's native API, all component and resource will need to have a function to check if it is clonable (which is kind of bad since it only serves for this purpose). That is why I use the external runtime registry . |
Originally posted by @chengts95 in #15459
The text was updated successfully, but these errors were encountered: