Skip to content

Full world cloning #17316

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
wants to merge 17 commits into
base: main
Choose a base branch
from
Open

Full world cloning #17316

wants to merge 17 commits into from

Conversation

eugineerd
Copy link
Contributor

Objective

Fix #16559 by introducing full world cloning. This would allow to "fork" a World by calling try_clone for use cases like running a simulation of a world without affecting the main world.

Solution

This PR introduces ability to clone a &World by performing "full cloning". "Full cloning", in this context, means that either world will be cloned fully (all entities, resources, etc.) or it will not be cloned at all.

fn clone_world(world: &World) {
    let world_clone = world.try_clone().unwrap()
}

There are many ways cloning a world can fail, here's a list of errors that can happen:

/// An error that occurs when cloning world failed.
#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorldCloneError {
    /// World id allocation failed.
    #[error("More `bevy` `World`s have been created than is supported.")]
    WorldIdExhausted,
    /// World has unapplied commands queued.
    #[error("World cannot be cloned while there are unapplied commands queued.")]
    UnappliedCommands,
    /// Component clone handler failed to clone component.
    #[error("Component clone handler for component with ID {0:?} failed to clone the component.")]
    FailedToCloneComponent(ComponentId),
    /// Component clone handler failed to clone resource.
    #[error("Component clone handler for resource with ID {0:?} failed to clone the resource.")]
    FailedToCloneResource(ComponentId),
    /// Resource cloned from different thread than the one used to create it.
    #[error("Tried to clone non-send resource with ID {0:?} from a different thread than the one it was created from.")]
    NonSendResourceCloned(ComponentId),
    /// Component clone handler is set to `Ignore`.
    #[error("Component clone handler for component or resource with ID {0:?} is set to Ignore and can't be cloned.")]
    ComponentCantBeCloned(ComponentId),
}

Resource clone handler

Since full world cloning implies cloning resources, Resource trait has been changed to allow setting component clone handler as well.

pub trait Resource: Send + Sync + 'static {
    /// Called when registering this resource, allowing to override clone function (or disable cloning altogether) for this resource.
    ///
    /// See [Handlers section of `EntityCloneBuilder`](crate::entity::EntityCloneBuilder#handlers) to understand how this affects handler priority.
    fn get_component_clone_handler() -> ComponentCloneHandler {
        ComponentCloneHandler::default_handler()
    }
}

This should be fine since we're moving towards unifying Component and Resource anyway.

Copy-based component clone handler

To speed up component cloning,ComponentCloneHandler can now be set to copy_handler mode and ComponentDescriptor now has a new is_copy flag that is set to true only for components/resources that implement Copy.

Setting is_copy flag uses a somewhat unsupported hack utilizing array Copy specialization. It is not required for the approach to work (we can always fallback to Autoderef-based specialization if this hack ever gets fixed), but it does make code a bit cleaner and allows to set this flag for NonSend resources.

World component clone handler

Another change implemented to facilitate world cloning is a new CompnentCloneFn signature:

// Old
pub type ComponentCloneFn = fn(&mut DeferredWorld, &mut ComponentCloneCtx);
// New
pub type ComponentCloneFn = fn(&World, &mut ComponentCloneCtx);

Commands is now available from ComponentCloneCtx instead of from DeferredWorld.

World cloning reuses the same component clone handlers as entity cloning, however Commands and source and target entity are not available when cloning component in world context. This is a bit unfortunate, but most component clone handlers that rely on commands to perform component cloning would not work well in that context. For example, Parent and Children components should not spawn a new entity - instead they should just clone inner data to new world.

Sometimes it makes sense to set different handlers depending on context, which can be set like so:

fn get_component_clone_handler() -> ComponentCloneHandler {
    ComponentCloneHandler::ignore()
        .with_world_clone_handler(ComponentCloneHandler::clone_handler::<Self>())
}

Technically this is not required - branching can be performed inside the clone handler instead. I'm not entirely sure which way will be better.

Why full world cloning?

The main reason this PR implements "full cloning" and not "partial cloning" (only cloneable components and resources are cloned) is to enable fast cloning and avoid having to deal with many problems for which I don't have a good answer for:

  • If component failed to clone, should it run OnRemove hooks?
  • Should OnAdd/OnInsert hooks run?
  • What about observers?
  • What state should be updated if clone component failed?
  • Should entities that ended up without any components be retained?

I decided that even though this implementation is fairly limited, it should still be useful for the use case presented in the linked issue.

Testing

  • Added a basic test for try_clone.

There should definitely be more tests. Best-case scenario would be to somehow reuse all existing world tests by making a clone right before asserts, but I'm not sure if this is feasible.

Considerations

This PR introduces a lot of changes to various low-level components for what is arguable quite a niche feature. Going forward, all new additions to World would have to be Clone-compatible, which might increase complexity too much.

My question is: is this something we want to support? It might be too much of a burden, but I'm not entirely sure at this point. See "Future work" section for a list of components that would need to be supported before this functionality can be useful for a normal bevy app.

Future work

While this PR allows to clone a World as long as all components and resources are cloneable, this is not yet compatible with most default bevy component and resources. As an example, here is a list of all components and resources that fail to be cloned in a simple hello_world app with just DefaultPlugins added:

"bevy_ecs::observer::runner::Observer",
"bevy_ecs::observer::runner::ObserverState",
"bevy_ecs::schedule::schedule::Schedules",
"bevy_app::main_schedule::MainScheduleOrder",
"bevy_app::main_schedule::FixedMainScheduleOrder",
"bevy_ecs::event::collections::Events<bevy_app::app::AppExit>",
"bevy_ecs::event::registry::EventRegistry",
"bevy_time::TimeUpdateStrategy",
"bevy_hierarchy::valid_parent_check_plugin::ReportHierarchyIssue<bevy_transform::components::global_transform::GlobalTransform>",
"bevy_ecs::event::collections::Events<bevy_hierarchy::events::HierarchyEvent>",
"bevy_diagnostic::diagnostic::DiagnosticsStore",
"bevy_diagnostic::system_information_diagnostics_plugin::SystemInfo",
"bevy_ecs::event::collections::Events<bevy_input::keyboard::KeyboardInput>",
"bevy_ecs::event::collections::Events<bevy_input::keyboard::KeyboardFocusLost>",
"bevy_ecs::event::collections::Events<bevy_input::mouse::MouseButtonInput>",
"bevy_ecs::event::collections::Events<bevy_input::mouse::MouseMotion>",
"bevy_ecs::event::collections::Events<bevy_input::mouse::MouseWheel>",
"bevy_ecs::event::collections::Events<bevy_input::gestures::PinchGesture>",
"bevy_ecs::event::collections::Events<bevy_input::gestures::RotationGesture>",
"bevy_ecs::event::collections::Events<bevy_input::gestures::DoubleTapGesture>",
"bevy_ecs::event::collections::Events<bevy_input::gestures::PanGesture>",
"bevy_ecs::event::collections::Events<bevy_input::gamepad::GamepadEvent>",
"bevy_ecs::event::collections::Events<bevy_input::gamepad::GamepadConnectionEvent>",
"bevy_ecs::event::collections::Events<bevy_input::gamepad::GamepadButtonChangedEvent>",
"bevy_ecs::event::collections::Events<bevy_input::gamepad::GamepadButtonStateChangedEvent>",
"bevy_ecs::event::collections::Events<bevy_input::gamepad::GamepadAxisChangedEvent>",
"bevy_ecs::event::collections::Events<bevy_input::gamepad::RawGamepadEvent>",
"bevy_ecs::event::collections::Events<bevy_input::gamepad::RawGamepadAxisChangedEvent>",
"bevy_ecs::event::collections::Events<bevy_input::gamepad::RawGamepadButtonChangedEvent>",
"bevy_ecs::event::collections::Events<bevy_input::gamepad::GamepadRumbleRequest>",
"bevy_ecs::event::collections::Events<bevy_input::touch::TouchInput>",
"bevy_ecs::event::collections::Events<bevy_window::event::WindowEvent>",
"bevy_ecs::event::collections::Events<bevy_window::event::WindowResized>",
"bevy_ecs::event::collections::Events<bevy_window::event::WindowCreated>",
"bevy_ecs::event::collections::Events<bevy_window::event::WindowClosing>",
"bevy_ecs::event::collections::Events<bevy_window::event::WindowClosed>",
"bevy_ecs::event::collections::Events<bevy_window::event::WindowCloseRequested>",
"bevy_ecs::event::collections::Events<bevy_window::event::WindowDestroyed>",
"bevy_ecs::event::collections::Events<bevy_window::event::RequestRedraw>",
"bevy_ecs::event::collections::Events<bevy_window::event::CursorMoved>",
"bevy_ecs::event::collections::Events<bevy_window::event::CursorEntered>",
"bevy_ecs::event::collections::Events<bevy_window::event::CursorLeft>",
"bevy_ecs::event::collections::Events<bevy_window::event::Ime>",
"bevy_ecs::event::collections::Events<bevy_window::event::WindowFocused>",
"bevy_ecs::event::collections::Events<bevy_window::event::WindowOccluded>",
"bevy_ecs::event::collections::Events<bevy_window::event::WindowScaleFactorChanged>",
"bevy_ecs::event::collections::Events<bevy_window::event::WindowBackendScaleFactorChanged>",
"bevy_ecs::event::collections::Events<bevy_window::event::FileDragAndDrop>",
"bevy_ecs::event::collections::Events<bevy_window::event::WindowMoved>",
"bevy_ecs::event::collections::Events<bevy_window::event::WindowThemeChanged>",
"bevy_ecs::event::collections::Events<bevy_window::event::AppLifecycle>",
"bevy_asset::io::source::AssetSourceBuilders",
"bevy_asset::io::embedded::EmbeddedAssetRegistry",
"bevy_asset::assets::Assets<bevy_asset::folder::LoadedFolder>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_asset::folder::LoadedFolder>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_asset::folder::LoadedFolder>>",
"bevy_asset::assets::Assets<bevy_asset::assets::LoadedUntypedAsset>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_asset::assets::LoadedUntypedAsset>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_asset::assets::LoadedUntypedAsset>>",
"bevy_asset::assets::Assets<()>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<()>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<()>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::UntypedAssetLoadFailedEvent>",
"bevy_asset::assets::Assets<bevy_scene::dynamic_scene::DynamicScene>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_scene::dynamic_scene::DynamicScene>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_scene::dynamic_scene::DynamicScene>>",
"bevy_asset::assets::Assets<bevy_scene::scene::Scene>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_scene::scene::Scene>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_scene::scene::Scene>>",
"bevy_scene::scene_spawner::SceneSpawner",
"bevy_winit::winit_monitors::WinitMonitors",
"bevy_ecs::event::collections::Events<bevy_winit::RawWinitWindowEvent>",
"bevy_winit::accessibility::WinitActionRequestHandlers",
"bevy_ecs::event::collections::Events<bevy_a11y::ActionRequest>",
"bevy_asset::assets::Assets<bevy_render::render_resource::shader::Shader>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_render::render_resource::shader::Shader>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_render::render_resource::shader::Shader>>",
"bevy_render::FutureRenderResources",
"bevy_render::ScratchMainWorld",
"bevy_time::TimeReceiver",
"bevy_hierarchy::valid_parent_check_plugin::ReportHierarchyIssue<bevy_render::view::visibility::InheritedVisibility>",
"bevy_render::view::visibility::PreviousVisibleEntities",
"bevy_render::view::visibility::range::VisibleEntityRanges",
"bevy_asset::assets::Assets<bevy_mesh::mesh::Mesh>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_mesh::mesh::Mesh>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_mesh::mesh::Mesh>>",
"bevy_asset::assets::Assets<bevy_mesh::skinning::SkinnedMeshInverseBindposes>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_mesh::skinning::SkinnedMeshInverseBindposes>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_mesh::skinning::SkinnedMeshInverseBindposes>>",
"bevy_render::render_asset::CachedExtractRenderAssetSystemState<bevy_render::mesh::RenderMesh>",
"bevy_render::sync_world::PendingSyncEntity",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_render::storage::ShaderStorageBuffer>>",
"bevy_asset::assets::Assets<bevy_render::storage::ShaderStorageBuffer>",
"bevy_render::render_asset::CachedExtractRenderAssetSystemState<bevy_render::storage::GpuShaderStorageBuffer>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_render::storage::ShaderStorageBuffer>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_image::image::Image>>",
"bevy_asset::assets::Assets<bevy_image::image::Image>",
"bevy_render::render_asset::CachedExtractRenderAssetSystemState<bevy_render::texture::gpu_image::GpuImage>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_image::image::Image>>",
"bevy_asset::assets::Assets<bevy_image::texture_atlas::TextureAtlasLayout>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_image::texture_atlas::TextureAtlasLayout>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_image::texture_atlas::TextureAtlasLayout>>",
"bevy_asset::assets::Assets<bevy_sprite::mesh2d::color_material::ColorMaterial>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_sprite::mesh2d::color_material::ColorMaterial>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_sprite::mesh2d::color_material::ColorMaterial>>",
"bevy_render::render_asset::CachedExtractRenderAssetSystemState<bevy_sprite::mesh2d::material::PreparedMaterial2d<bevy_sprite::mesh2d::color_material::ColorMaterial>>",
"bevy_asset::assets::Assets<bevy_text::font::Font>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_text::font::Font>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_text::font::Font>>",
"bevy_text::font_atlas_set::FontAtlasSets",
"bevy_text::pipeline::TextPipeline",
"bevy_text::pipeline::CosmicFontSystem",
"bevy_text::pipeline::SwashCache",
"bevy_text::text_access::TextIterScratch",
"bevy_ui::layout::ui_surface::UiSurface",
"bevy_ui::stack::UiStack",
"bevy_pbr::cluster::GlobalVisibleClusterableObjects",
"bevy_asset::assets::Assets<bevy_pbr::pbr_material::StandardMaterial>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_pbr::pbr_material::StandardMaterial>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_pbr::pbr_material::StandardMaterial>>",
"bevy_render::render_asset::CachedExtractRenderAssetSystemState<bevy_pbr::material::PreparedMaterial<bevy_pbr::pbr_material::StandardMaterial>>",
"bevy_pbr::prepass::AnyPrepassPluginLoaded",
"bevy_asset::assets::Assets<bevy_gltf::Gltf>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_gltf::Gltf>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_gltf::Gltf>>",
"bevy_asset::assets::Assets<bevy_gltf::GltfNode>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_gltf::GltfNode>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_gltf::GltfNode>>",
"bevy_asset::assets::Assets<bevy_gltf::GltfPrimitive>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_gltf::GltfPrimitive>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_gltf::GltfPrimitive>>",
"bevy_asset::assets::Assets<bevy_gltf::GltfMesh>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_gltf::GltfMesh>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_gltf::GltfMesh>>",
"bevy_asset::assets::Assets<bevy_gltf::GltfSkin>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_gltf::GltfSkin>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_gltf::GltfSkin>>",
"bevy_audio::audio_output::AudioOutput",
"bevy_asset::assets::Assets<bevy_audio::audio_source::AudioSource>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_audio::audio_source::AudioSource>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_audio::audio_source::AudioSource>>",
"bevy_asset::assets::Assets<bevy_audio::pitch::Pitch>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_audio::pitch::Pitch>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_audio::pitch::Pitch>>",
"bevy_gilrs::Gilrs",
"bevy_gilrs::GilrsGamepads",
"bevy_gilrs::rumble::RunningRumbleEffects",
"bevy_asset::assets::Assets<bevy_animation::AnimationClip>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_animation::AnimationClip>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_animation::AnimationClip>>",
"bevy_asset::assets::Assets<bevy_animation::graph::AnimationGraph>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_animation::graph::AnimationGraph>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_animation::graph::AnimationGraph>>",
"bevy_asset::assets::Assets<bevy_gizmos::GizmoAsset>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetEvent<bevy_gizmos::GizmoAsset>>",
"bevy_ecs::event::collections::Events<bevy_asset::event::AssetLoadFailedEvent<bevy_gizmos::GizmoAsset>>",
"bevy_gizmos::GizmoHandles",
"bevy_gizmos::gizmos::GizmoStorage<bevy_gizmos::config::DefaultGizmoConfigGroup,
"bevy_gizmos::gizmos::GizmoStorage<bevy_gizmos::aabb::AabbGizmoConfigGroup,
"bevy_render::render_asset::CachedExtractRenderAssetSystemState<bevy_gizmos::GpuLineGizmo>",
"bevy_gizmos::gizmos::GizmoStorage<bevy_gizmos::light::LightGizmoConfigGroup,
"bevy_ecs::event::collections::Events<bevy_picking::pointer::PointerInput>",
"bevy_ecs::event::collections::Events<bevy_picking::backend::PointerHits>",
"bevy_picking::hover::HoverMap",
"bevy_picking::hover::PreviousHoverMap",
"bevy_ecs::event::collections::Events<bevy_picking::events::Pointer<bevy_picking::events::Cancel>>",
"bevy_ecs::event::collections::Events<bevy_picking::events::Pointer<bevy_picking::events::Click>>",
"bevy_ecs::event::collections::Events<bevy_picking::events::Pointer<bevy_picking::events::Pressed>>",
"bevy_ecs::event::collections::Events<bevy_picking::events::Pointer<bevy_picking::events::DragDrop>>",
"bevy_ecs::event::collections::Events<bevy_picking::events::Pointer<bevy_picking::events::DragEnd>>",
"bevy_ecs::event::collections::Events<bevy_picking::events::Pointer<bevy_picking::events::DragEnter>>",
"bevy_ecs::event::collections::Events<bevy_picking::events::Pointer<bevy_picking::events::Drag>>",
"bevy_ecs::event::collections::Events<bevy_picking::events::Pointer<bevy_picking::events::DragLeave>>",
"bevy_ecs::event::collections::Events<bevy_picking::events::Pointer<bevy_picking::events::DragOver>>",
"bevy_ecs::event::collections::Events<bevy_picking::events::Pointer<bevy_picking::events::DragStart>>",
"bevy_ecs::event::collections::Events<bevy_picking::events::Pointer<bevy_picking::events::Move>>",
"bevy_ecs::event::collections::Events<bevy_picking::events::Pointer<bevy_picking::events::Out>>",
"bevy_ecs::event::collections::Events<bevy_picking::events::Pointer<bevy_picking::events::Over>>",
"bevy_ecs::event::collections::Events<bevy_picking::events::Pointer<bevy_picking::events::Released>>",
"bevy_render::view::window::screenshot::CapturedScreenshots",
"bevy_render::pipelined_rendering::RenderAppChannels",
"bevy_winit::EventLoopProxyWrapper<bevy_winit::WakeUp>",
"bevy_ecs::event::collections::Events<bevy_winit::WakeUp>",

For some of these components it is trivial to implement Clone, for others it is a lot harder. At this point I'm not sure if component clone handlers will ever be implemented for all of bevy's components.

@eugineerd
Copy link
Contributor Author

@chengts95, would this implementation be good enough for your use case?

@eugineerd eugineerd changed the title Clone world Full world cloning Jan 11, 2025
@BenjaminBrienen
Copy link
Contributor

I thought of a use case for this yesterday, lol. When a player finishes a race in a game like Mariokart, I wonder if it performs a fast simulation to determine the final place of the NPCs that haven't crossed the finish line yet or if it just uses their current position.

@BenjaminBrienen BenjaminBrienen added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events D-Complex Quite challenging from either a design or technical perspective. Ask for help! D-Unsafe Touches with unsafe code in some way S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jan 11, 2025
@chengts95
Copy link

chengts95 commented Jan 12, 2025

@chengts95, would this implementation be good enough for your use case?

I prefer my approach that can avoid modifying internal things. Also, I may want partial cloning to sync only specific types of components/ specific entity sets. That is why I used a similar reflection registry which only requires a common Clone trait. I believe this feature should be optional and modular since many data types cannot be really cloned.

@eugineerd
Copy link
Contributor Author

I may want partial cloning to sync only specific types of components/ specific entity sets

I can see how your proposed approach would be more general, however my main concern is that it doesn't support dynamic components (since they don't have TypeId's). I think that maybe cross-world entity cloning for non-dynamic components can be implemented in EntityCloneBuilder? It uses pretty much the same approach as the one you outlined.

The reason I though that full world cloning would make more sense for your use case is that when some resource or component didn't get cloned for some reason, it might lead to unexpected results. You also mentioned native performance, which is why I tried to make it as fast as possible (and that required trying to clone as much of internal data as possible instead of recomputing stuff).

Then there's also the issue of when to run various hooks (or even if they should be run at all and for which entities) and observers, how to update archetypes if a component for an entity wasn't cloned.

Maybe the functionality introduced in this PR can be extended to support partial cloning, but I think it will get a lot more complicated if we want to preserve native performance.

I believe this feature should be optional and modular since many data types cannot be really cloned

If there is some component that can't be cloned, it is possible to provide a new value instead by setting a custom component clone handler. The main problem is that you would still need to provide some value - it can't just ignore cloning a component (or we would lose performance to archetype moves, although there was also a suggestion to add an uninitialized flag to such components and ignore them at query time).

But in general I do agree that current approach is somewhat inflexible, I'm just not sure how to improve it yet.

@chengts95
Copy link

chengts95 commented Jan 12, 2025

dynamic components

My approach already solved my problems, the only problem for Bevy is that they want to remove the API to spawn with a specific entity id. In addition, my approach also handles the types without the Clone trait such as parent and child without modifications to the original components. I believe a dynamic component already has a dynamic Clone trait or clone function to call. Generally, I avoid virtual functions in ECS design since ECS stores typed components.

Since we have to register the clone function, I prefer to register it out of the world and components. Also, many data types are not cloneable and need additional information to regenerate. I prefer to leave the freedom to the user instead of fusing the logic inside the datatype, it is against the rule of data-oriented design.

In some cases, full cloning is used. In other more common cases, partial cloning is used. It actually depends on the systems that will be called.

@alice-i-cecile alice-i-cecile added the X-Contentious There are nontrivial implications that should be thought through label Jan 12, 2025
@eugineerd
Copy link
Contributor Author

In addition, my approach also handles the types without the Clone trait such as parent and child without modifications to the original components

The proposed approach does not rely on Clone, but it does provide default handler implementations for components that implement Clone or Reflect. It uses autoderef specialization in #[derive(Component)] to select appropriate handler based on implemented traits.

I believe a dynamic component already has a dynamic Clone trait or clone function to call. Generally, I avoid virtual functions in ECS design since ECS stores typed components.

Just to make sure we're talking about the same thing, by dynamic components I mean components registered using
world.register_component_with_descriptor, which can be used to register new components for scripting implementations. These components don't have TypeId's, but they should still be cloneable (scripting implementation would be responsible for setting the appropriate clone handler).

Since we have to register the clone function, I prefer to register it out of the world and components

That would mean we would lose automatic clone handler registration for components that implement Clone or Reflect and they would have to be registered manually in the registry.

Also, many data types are not cloneable and need additional information to regenerate.

As long as the data needed to regenerate a type is stored within source world, it is accessible from custom clone handler: pub type ComponentCloneFn = fn(&World, &mut ComponentCloneCtx).

I prefer to leave the freedom to the user instead of fusing the logic inside the datatype, it is against the rule of data-oriented design.

Custom clone handlers can be set for any registered component by using world.set_component_clone_handler(component_id, handler), you don't need to implement fn get_component_clone_handler.


I'm just clearing up some misunderstandings since this functionality is based on #16132 and I don't explain everything that was implemented there in this PR.

@Liam19
Copy link

Liam19 commented Jan 12, 2025

This might be useful for saving worlds? In my game I've been manually extracting all entities that have saveable components into a separate world along with their children, de-spawning any children that don't need to be saved, then serialising.
Not sure if that was a good approach but wanted to avoid invalidating the entity hierarchy during a save, as I had some problems with Children components pointing to de-spawned entities once I load the save file from disk. #12235 might solve this problem though.

@BenjaminBrienen BenjaminBrienen added the S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged label Jan 12, 2025
@chengts95
Copy link

chengts95 commented Jan 12, 2025 via email

}

/// HACK: Determine if T is [`Copy`] by (ab)using array copy specialization.
/// This utilizes a maybe bug in std which is maybe unsound when used with lifetimes (see: <https://github.com/rust-lang/rust/issues/132442>).
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since Component must be 'static, and at least in this PR we're only calling is_copy on Components, that means the "using this trick on structs with lifetimes is unsound" concern wouldn't have any practical impact here, right?

Obviously it is still a hack, but it's also private to this module and I'd say the doc comment adequately describes the hazards involved with using it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend to replace this hack with autoderef specialization since its about to be fixed: rust-lang/rust#135634. There are still some other ways to abuse std's specialization, but it's not a good idea to do so.

@caspark
Copy link

caspark commented May 11, 2025

I read this PR to judge whether I'd be able to rebase it and take it over the finish line (as @eugineerd suggested), and here's what looks like needs to be done code-wise still to make this useful & robust in practice:

  • Remove the array-specialization compiler bug/loophole used to work around lack of specialization to set ComponentDescriptor::is_copy, and replace it with auto-deref specialization for Copy types instead. (looks like there's already some of that in the PR, haven't checked if it's getting used..)
  • Changes related to bevy_hierarchy for cloning parent relationships need to be reworked to work with ECS relationships instead.
  • Support cloning the following bevy_ecs types:
    • bevy_ecs::event::collections::Events - straightforward
    • bevy_ecs::event::registry::EventRegistry - straightforward
    • bevy_ecs::observer::runner::Observer - observer stores a system: Box<dyn Any + Send + Sync + 'static>, so have to wrap that in an Arc<Mutex<..>> instead? (But then that wouldn't work for nostd...)
    • bevy_ecs::observer::runner::ObserverState - straightforward
    • bevy_ecs::schedule::schedule::Schedules - very painful because it stores boxed systems, boxed system/system-set conditions, etc. It seems saner to say that the clone impl for Schedules is actually that the same schedule is reused for all worlds, so that cloning a world means that the new world is actually linked to the old world? Or maybe we just say we can't clone this and always skip it?
  • Extra tests needed:
    • verify entity ids from old world refer to entity ids in the new world (even if the entity id's generation is >1 because the entity was despawned and respawned)
    • verify new entities are assigned the same ids in the old and new world (for both straightforward generic case, entity ids available in freelist)
    • verify entity iteration order is the same between old and new worlds (should come for free by preserving entity ids, but this is half the reason I care about cloning worlds so best encode the invariant as a test)
    • tests to ensure that relationships,events, observers, hooks, schedules are cloned correctly and behave as expected
  • Possibly more rebasing onto main.. I haven't actually tried doing that yet. For example over in Add an API to clone worlds #16559 @eugineerd said "the cloning handler api changed a bit since then, but it should still be mostly compatible".

But that all seems secondary to the one big question:

  • Is Bevy willing to go down this path of supporting cloning worlds like this? It is a rather more invasive maintenance-wise (and architecture-wise) approach than figuring out a performant way (e.g. like this) to allocate entities with specific entity indexes & generations and then batch spawning appropriate components onto those entities (as @chengts95 advocates for). And supporting cloning here would probably also result in users requesting other engine (and ecosystem) components to support being Clone-d (see Details expando under the Future Work heading) - so it doesn't seem like a one and done deal.

If the answer is "yes, we want to support that", then we also need to figure out (from most to least important):

  • Stick with the full world cloning as implemented here, or support partial world cloning?
  • How do we handle cloning the schedule. A world without a schedule seems not so useful, so I see 3 options:
    • "cloned worlds share schedules, and a change to one schedule affects all worlds". Possibly what most users would want, but not flexible.
    • "cloning world skips cloning the schedules, and so you need to set up the schedules in the cloned world again yourself". Not really what I'd expect from regular a clone() but it is flexible.
    • "if there's a schedule in the world, you can't clone it at all". Not helpful, but clear: users would need to manually remove the schedule resource from the world before cloning it.
  • How do we handle cloning observers - Arc<Mutex<Box<dyn Any + Send + Sync + 'static>>> so that observer functions are shared between cloned worlds? (that's what I'd expect, but what about nostd?) Or skip them? Or fail the clone if there are any observers? Or let users decide via some customizable observer clone handler?

@eugineerd
Copy link
Contributor Author

...here's what looks like needs to be done code-wise still to make this useful & robust in practice

This is a solid plan of action as far as I can see.

...wrap that in an Arc<Mutex<..>> instead? (But then that wouldn't work for nostd...)

Arc and Mutex should be supported by bevy_platform, although I'm unsure about how wrapping observer systems in that will affect performance. That being said, I also don't really know if there is any alternative to doing it that way.

...the cloning handler api changed a bit...

As for the api changes in clone handlers - some work in this PR went into making clone handlers take &World instead of &DeferredWorld, however that is no longer relevant since we don't pass any kind of World to handlers anymore. Now the only question left with that is where to apply deferred commands - in source world or the target world? Theoretically it would make sense to apply them in target world, but I'm not entirely sure.

Can't really say much about any other changes ECS went through since the time this PR was opened, but I don't think anything changed significantly enough to break this code too much.

...one big question

I don't have a good answer for this, which is a large part of why this PR stalled in the first place. I also don't really have any projects where I would've liked to do world cloning, so I don't feel qualified enough to answer this questions in the first place.

@chengts95
Copy link

In my opinion, the clone must be implemented in a isolated runtime or user-coded context registry. Injecting trait to component is not necessary and not helpful. It depends on users' decision to define how to replicate the states. It is very obvious that many objects and structures are semantically bound to move-only or controlled-copy behavior, which is the default behavior for modern languages and even modern C++. You cannot distinguish deepcopy and shallow copy from a trait which erased such constriant.

In addition, it is not possible for a type to fully implement a meaningful Clone in many cases.
For example, if we need to ensure that references from the current context remain valid in a new context, then external mappings and reconstruction logic become unavoidable.

Consider this: a Schedule or an Observer can be modeled as an archetype — a pure data description of how a behavior is constructed. (I have verified this design in C++ with my custom schedule, and it can even go beyond the boundary of specific ECS.)
The actual runtime behavior (e.g., function pointers, closures) may be uncloneable, but the structure used to build them is cloneable.

Therefore, the correct approach is to copy the data-driven description and re-register the behavior in the new context.

Injecting OOP-style implicit behavior — such as cloning boxed function pointers without structure awareness — goes directly against the principles of a data-driven and data-oriented ECS architecture.

@caspark
Copy link

caspark commented May 12, 2025

Injecting trait to component is not necessary and not helpful. ... You cannot distinguish deepcopy and shallow copy from a trait which erased such constriant. ... Injecting OOP-style implicit behavior — such as cloning boxed function pointers without structure awareness — goes directly against the principles of a data-driven and data-oriented ECS architecture.

@chengts95 in 0.16 Bevy already gained an official way to clone entities: see 0.16's release notes, Entity Cloning section for a summary. How to clone each component can be customized on a per-component (and per-clone-attempt) basis too; see EntityCloner docs. So none of that is new to this PR; this PR just extends that a little to allow "component clone behavior when cloning world" to be different from "component clone behavior when cloning a single entity".


As for the full vs partial cloning of the World (and the related infectiousness of Cloning, as in "so much of Bevy's types will end up needing to be Clone in order for World::try_clone() to be useful"): @eugineerd rather than short-circuit-failing when we find a component we can't clone, perhaps we should just accumulate and return the ComponentIds of components that couldn't be cloned? Then we can allow the user to decide what to do about that (user could manually clone & insert some, reinsert schedules, skip others, etc). Cloning a world is definitely a "only happens 0-3 times in a game's codebase" type of thing so it seems okay for it to be less ergonomic than the way-more-common clone-an-entity use case.

It seems like that approach would work well for Resources (no hooks/observers on Resources.. right? so that sidesteps all the partial cloning concerns for them), and I suspect that in practice it will mostly be Resources that can't be cloned properly. That gives an easy answer for dealing with the (almost certainly present) Schedule resource: it just gets skipped, so the clone doesn't fail.

But for actual Components on entities, the concerns about partial cloning in the PR description would still be valid though, so the approach would be "Uncloneable Resources are skipped and their ComponentIds are returned to the user, but uncloneable Entity Components cause the world clone to fail". Partial cloning of entity components could be figured out later when someone asks for it - at which point we could ask them how they would expect hooks/observers to behave.

Thoughts? (of course this is all presupposing that Bevy wants world cloning in the first place)

@eugineerd
Copy link
Contributor Author

eugineerd commented May 12, 2025

rather than short-circuit-failing when we find a component we can't clone, perhaps we should just accumulate and return the ComponentIds of components that couldn't be cloned

This is certainty a solution, however the reason I decided to go with short-circuiting is because allowing only some components to be cloned complicates the code a lot and makes this approach less performant overall. For example, for each archetype we would need to keep track of all entities that had at least 1 component that failed cloning and move them to a different archetype. That would mean that we can't just clone Storages anymore, since we would also have to reconcile the changes with Archetypes by removing entities from there as well. Although maybe it's a better solution than infecting everything with Clone (even though I would've preferred all components to have Clone bound in the first place, I understand that while it'd be very useful for some projects, it might hinder some others).

no hooks/observers on Resources.. right?

There is work ongoing to make resources more like components, which would allow them to have hooks as well (see #17485). That would also mean there's no need to special-case resources and the same approach that works for components would work for resources as well.

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 C-Feature A new feature, making something new possible D-Complex Quite challenging from either a design or technical perspective. Ask for help! D-Unsafe Touches with unsafe code in some way S-Needs-Review Needs reviewer attention (from anyone!) to move forward S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged X-Contentious There are nontrivial implications that should be thought through
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add an API to clone worlds
6 participants