Skip to content

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

Open
alice-i-cecile opened this issue Nov 30, 2024 · 18 comments · May be fixed by #17316
Open

Add an API to clone worlds #16559

alice-i-cecile opened this issue Nov 30, 2024 · 18 comments · May be fixed by #17316
Labels
A-ECS Entities, components, systems, and events D-Complex Quite challenging from either a design or technical perspective. Ask for help! S-Needs-Design This issue requires design work to think about how it would best be accomplished

Comments

@alice-i-cecile
Copy link
Member

I have a strong motivation to use shared entity ids for multiple worlds

Can you explain this use case? What are you ultimately trying to achieve, and why is this strategy important to do this? The EntityMapper trait is designed for the serialization / networking use cases, and has been how Bevy serializes and deserializes scenes since Bevy 0.1 as far as I'm aware.

  
    clone_entities(app.world(), app2.world_mut());
    reg.clone_world(app.world(), app2.world_mut());

I simply want to clone worlds, the worlds should have identical IDs for multiple algorithm designs, and I want native performance. The serialized and deserialized scenes are too heavy. Generally, bevy relies on reflection to serialize and deserialize, which is an overkill solution. Serialization is not an option for cloning a world for high-performance applications.

I have applications for power system simulation in Bevy. The ECS can become very successful in industrial applications that are not related to video games. Many algorithms such as Monte-Carlo simulation and N-1 stability verification require duplicating the data. In the design i have implemented, I only rely on the Clone trait to duplicate data. If the design permits, I will even choose to aggressively copy the archetype table as a whole and reconstruct the data in a new world.

Originally posted by @chengts95 in #15459

@alice-i-cecile
Copy link
Member Author

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.

@alice-i-cecile alice-i-cecile added A-ECS Entities, components, systems, and events D-Complex Quite challenging from either a design or technical perspective. Ask for help! S-Needs-Design This issue requires design work to think about how it would best be accomplished labels Nov 30, 2024
@alice-i-cecile alice-i-cecile added this to the 0.16 milestone Nov 30, 2024
@Trashtalk217
Copy link
Contributor

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.

@chengts95
Copy link

chengts95 commented Nov 30, 2024

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);


    }
}

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.

@alice-i-cecile
Copy link
Member Author

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 :(

@chengts95
Copy link

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.

@alice-i-cecile
Copy link
Member Author

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.

@chengts95
Copy link

chengts95 commented Nov 30, 2024

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.

@eugineerd eugineerd linked a pull request Jan 11, 2025 that will close this issue
@chengts95
Copy link

chengts95 commented Jan 12, 2025

I experimented with directly cloning components from archetype tables instead of querying individual entities from the World. This approach proved feasible and resulted in significant performance improvements.

Benchmark Setup:

  • Entity Count: 30,000
  • Components per Entity: 5-6 f64 components
  • Archetype number: 6
  • Comparison:
    • Original Method: Querying entities and components from the World.
    • Optimized Method: Directly accessing the archetype's corresponding Table for cloning.

Results:

Method Minimum Time Average Time Maximum Time
Querying from World 4ms 4.6ms 40ms
Archetype Table Clone 2ms 2.5ms 10ms

This optimization is achieved by bypassing the overhead of accessing entity metadata and directly cloning components from Table or SparseSet storage types and inject as commands.

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 World for individual entities and their components, which could be beneficial in performance-critical scenarios.

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
Would love to hear feedback or thoughts on further optimizations or edge cases I might have missed!

@alice-i-cecile alice-i-cecile removed this from the 0.16 milestone Feb 24, 2025
@caspark
Copy link

caspark commented May 9, 2025

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 ComponentCloneFn can't exactly be used as-is - cc @eugineerd in case you have thoughts on that).

@alice-i-cecile are you still on board with enabling this world clone use case? (including un-deprecating insert_or_spawn_batch(), though possibly keeping its warning)

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

  1. full disclosure: I've been using hecs for the last year with some small patches to enable cloning the world via registering component clone functions used on its archetypes (much like the above code sample) and to preserve the entity freelist across serialization/deserialization. But I'm investigating swapping to bevy_ecs for the built-in hooks and observers support. (And with bevy_ecs originally being a fork of hecs, I'm hopeful I can use basically the same approach for bevy_ecs as I've got working for hecs now.)

@chengts95
Copy link

I still can use the clone function i manually wrote in bevy 0.16, could you tell why it is not working for you?

@eugineerd
Copy link
Contributor

eugineerd commented May 9, 2025

although, hmm, from a quick glance at #16132 diff, it looks like ComponentCloneFn can't exactly be used as-is

The biggest problem was guaranteeing that ComponentIds match between different worlds. It would require them to have identical Components to work. Other than that, I don't think anything else is blocking it from working in a cross-world scenario.

@alice-i-cecile
Copy link
Member Author

@alice-i-cecile are you still on board with enabling this world clone use case? (including un-deprecating insert_or_spawn_batch(), though possibly keeping its warning)

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.

@chengts95
Copy link

chengts95 commented May 9, 2025

@alice-i-cecile are you still on board with enabling this world clone use case? (including un-deprecating insert_or_spawn_batch(), though possibly keeping its warning)

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 reserve_entities.

@caspark
Copy link

caspark commented May 10, 2025

I still can use the clone function i manually wrote in bevy 0.16, could you tell why it is not working for you?

@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.

The biggest problem was guaranteeing that ComponentIds match between different worlds. It would require them to have identical Components to work. Other than that, I don't think anything else is blocking it from working in a cross-world scenario.

Good to know!

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.

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 alloc_at and alloc_at_without_replacement. And I see those functions have been removed in main anyway (which concerns me a bit because it makes me suspect I can't get identical entity ids when I deserialize a saved world after transferring it over the network - but I will investigate that separately).

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:

  • Cloning mutably borrows World, to ensure that there are no outstanding borrows of any component data.
  • It's World::try_clone rather than World::clone to handle components that can't be cloned: I wanted the clone to fail rather than silently omit components.
    • In Bevy land I am not sure which you would prefer; I am happy to be guided either way (or maybe we can let the user choose when they clone the world?).
  • For non-component data, I just use the Clone trait to clone the various bits of World - so for example the set of used and free entities are copied as-is, ensuring that entity generations match for the new world (which is key to making sure references to entities work for both old and new world).
  • That Cloner is basically the same as the ComponentRegistry outlined above: it captures the clone function for each component.
    • In Bevy land I would try to reuse the existing machinery added in Entity cloning #16132 for this, so that registering component types via a Cloner/ComponentRegistry should not be necessary.
      • Having the first World create the new World directly (instead of letting the user pass in a "clone target world") ought to neatly address the problem that @eugineerd mentioned: that should guarantee that ComponentIds are the same for the new world as in the old.
    • I also used a special impl for Copy components, so that a whole column (a table in Bevy terms, I think?) of N Copy Components can be just one memcpy to their new storage instead of having to use N Clone calls. Not sure if this will be doable in Bevy though (and to be honest I never benchmarked to see if it actually made a meaningful difference).

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.)

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.

@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:

  1. you find the highest entity id in the old world
  2. in the new world you reserve_entities all entity ids up to and including that highest entity id
  3. for (only!) each entity currently in use in the old world, you copy the components into the new world

Assuming I've got that right, then I'm not sure these two cases are handled correctly:

  • A) if the old world has a deleted entity then in step 2 that entity would still exist in the new world (albeit with no components). You'd need a 4th step that's something like "for each entity in the new world, if it doesn't exist in the old world, then despawn it from the new world".
  • B) generations of entities: if the old world has an entity of generation 2 then in the new world that equivalent entity will have a generation of 1. So any old Entity instances won't actually resolve to that same entity in the new world. This could maybe be fixed by exposing new APIs to copy and set the freelist (i.e. Entities::pending) - e.g. something like this hecs impl - and copying the freelist from the old world to the new world as a new step 0... (but I say maybe because in hecs land the freelist needed to be set after spawning entities to avoid a panic somewhere. )

(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 insert_or_spawn_batch available, but it seems to me that having first-class world cloning built in to Bevy would be easier to use, and possibly perform better too?

@eugineerd
Copy link
Contributor

eugineerd commented May 10, 2025

So, in terms of an API specifically designed for cloning worlds

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

@chengts95
Copy link

chengts95 commented May 10, 2025

@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:

  1. you find the highest entity id in the old world
  2. in the new world you reserve_entities all entity ids up to and including that highest entity id
  3. for (only!) each entity currently in use in the old world, you copy the components into the new world

Assuming I've got that right, then I'm not sure these two cases are handled correctly:

  • A) if the old world has a deleted entity then in step 2 that entity would still exist in the new world (albeit with no components). You'd need a 4th step that's something like "for each entity in the new world, if it doesn't exist in the old world, then despawn it from the new world".
  • B) generations of entities: if the old world has an entity of generation 2 then in the new world that equivalent entity will have a generation of 1. So any old Entity instances won't actually resolve to that same entity in the new world. This could maybe be fixed by exposing new APIs to copy and set the freelist (i.e. Entities::pending) - e.g. something like this hecs impl - and copying the freelist from the old world to the new world as a new step 0... (but I say maybe because in hecs land the freelist needed to be set after spawning entities to avoid a panic somewhere. )

(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 insert_or_spawn_batch available, but it seems to me that having first-class world cloning built in to Bevy would be easier to use, and possibly perform better too?

First, when cloning a world, deleted entities are intentionally not included.
Entity deletion should be handled explicitly — e.g., via synchronized deletion commands between worlds.
If needed, you can maintain a blacklist of deleted entity IDs and skip them during cloning — but this only matters if deleted entities are still referenced for some reason in the old world. Despawning is not a behavior that should be included in the Clone.

Second, entity generations are expected to differ, and that’s by design.
I cannot ensure the same version since the different versions of 2 world is a desired behavior. If I do need to prevent ID reuse after deletion, I can skip those IDs using the same blacklist mentioned above. So from my perspective, both concerns are valid — but manageable and consistent with the desired cloning behavior.

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.
I don’t mind using ·#[derive(Clone)]· where appropriate. What I reject is making Clone a requirement for all components, because that violates the separation of data and behavior in ECS.

@caspark
Copy link

caspark commented May 11, 2025

@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 :

  • With const USE_INSERT_BATCH: bool = true; there so that insert_batch is used, you can see both test cases fail (and actually case B is worse than I anticipated because the cloning just panics).
  • With const USE_INSERT_BATCH: bool = false; (reverting to your original cloning code that used insert_or_spawn_batch), those tests cases work.

So in other words: your 0.15/0.16 approach that uses insert_or_spawn_batch() is perfect, but your 0.17-compatible approach that uses insert_batch() doesn't seem to work. I am not saying this is your "fault", or expecting you to fix it, justify that, or otherwise provide support for it. I think those failures are just a consequence of Bevy's choice to remove Entities::alloc_at and Entities::alloc_at_without_replacement without providing an alternative approach to accomplish that.

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.)

@chengts95
Copy link

chengts95 commented May 11, 2025

@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 :

  • With const USE_INSERT_BATCH: bool = true; there so that insert_batch is used, you can see both test cases fail (and actually case B is worse than I anticipated because the cloning just panics).
  • With const USE_INSERT_BATCH: bool = false; (reverting to your original cloning code that used insert_or_spawn_batch), those tests cases work.

So in other words: your 0.15/0.16 approach that uses insert_or_spawn_batch() is perfect, but your 0.17-compatible approach that uses insert_batch() doesn't seem to work. I am not saying this is your "fault", or expecting you to fix it, justify that, or otherwise provide support for it. I think those failures are just a consequence of Bevy's choice to remove Entities::alloc_at and Entities::alloc_at_without_replacement without providing an alternative approach to accomplish that.

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 deleted mark to the new world and disable it instantly (like use a tombstone flag), and clean up all the tombstones when system is free to do it. This methodology is similar to the situation in the database. In that case, version might be useful since you will know if it is modified and reused after it is marked as deleted.

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 .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events D-Complex Quite challenging from either a design or technical perspective. Ask for help! S-Needs-Design This issue requires design work to think about how it would best be accomplished
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants