bevyengine / bevyengine/bevy

EntityCloner remapping sensitive to component registration order.

Open
#24,210 0 comments 0 reactions 0 assignees View on GitHub
A-ECS C-Bug D-Modest S-Ready-For-Implementation
Dominant language
Rust
Stars
48.2k
Forks
4.8k
Avg merge
3d 16h
Merged PRs (30d)
171

Description

## Bevy version and features

`bevy = { version = "0.18", default-features = false, features = ["std", "bevy_log", "multi_threaded"] }`

## What you did

When cloning an entity heirachy with the EntityCloner api, remapping of Entity references on the root entity can fail or succeed depending on the registration order of components on the root entity.

## Minimum example to reproduce

Both tests below clone an entity heirachy, where the root entity holds a reference to a child entity via a `RootLink` component which is annotated with `#[entities]`. The `child` Entity field of `RootLink` will fail or succeed in remapping depending on whether it was added before or after the `ChildOf` component

```
use bevy::{ecs::entity::EntityCloner, prelude::*};

#[derive(Component, Clone, Copy, Debug)]
struct RootLink {
#[entities]
child: Entity,
}

fn clone_root(world: &mut World, root: Entity) -> Entity {
let clone = world.spawn_empty().id();
EntityCloner::build_opt_out(world)
.linked_cloning(true)
.clone_entity(root, clone);
clone
}

#[test]
fn linked_clone_remaps_root_entity_field_when_hierarchy_is_registered_first() {
let mut world = World::new();

let root = world.spawn_empty().id();
let child = world.spawn(ChildOf(root)).id();
world.entity_mut(root).insert(RootLink { child });

let clone = clone_root(&mut world, root);

let cloned_child = world.get::(clone).unwrap().child;
assert_ne!(cloned_child, child);
assert_eq!(
world.get::(cloned_child).map(ChildOf::parent),
Some(clone)
);
}

#[test]
fn linked_clone_leaves_root_entity_field_unmapped_when_root_component_is_registered_first() {
let mut world = World::new();

let root = world
.spawn(RootLink {
child: Entity::PLACEHOLDER,
})
.id();
let child = world.spawn(ChildOf(root)).id();
world.entity_mut(root).insert(RootLink { child });

let clone = clone_root(&mut world, root);

let cloned_child = world.get::(clone).unwrap().child;
assert_eq!(cloned_child, child);
assert_eq!(
world.get::(cloned_child).map(ChildOf::parent),
Some(root)
);
}

//A final test using the add_child api:
#[test]
fn linked_clone_leaves_root_entity_field_unmapped_when_root_component_is_registered_first_api() {
let mut world = World::new();
let mut commands = world.commands();
let child = commands.spawn_empty().id();
let root = commands
.spawn(RootLink { child: child })
.add_child(child)
.id();
world.flush();
let clone = clone_root(&mut world, root);
let cloned_child = world.get::(clone).unwrap().child;
assert_eq!(cloned_child, child);
assert_eq!(
world.get::(cloned_child).map(ChildOf::parent),
Some(root)
);
}
```

## Discussion on discord:

https://discord.com/channels/691052431525675048/1502218838392049725

With possible cause:
https://discord.com/channels/691052431525675048/1502218838392049725/1502430680225812680

> **eugineerd:** I think this happens because when hierarchy is cloned Children adds an entity mapping to the mapper used by EntityCloner. When a component is cloned, it's map_entities function is called, which uses this mapper to obtain correct entity to use, but if there isn't an entity mapped then it returns the original Entity value.
>
> So in this case since Children is added to the entity first, it's also the first to be cloned, which sets correct entity value in the mapper. When ActionGroup is cloned, it can find the correct entity since it has been set by Children before.
>
> So yeah, this is definitely a bug. #[relationship(..)] components aren't affected by it, so it makes sense why it wasn't encountered before I guess.

Contributor guide

Open the contributing guide

Research direction

Start with ecs::entity::EntityCloner and the linked-cloning path, then run the three reproduction tests from the issue with the stated Bevy features. Trace how hierarchy components and #[entities] fields update the entity mapper during cloning. Done means RootLink references are remapped correctly regardless of component registration order, including the add_child case, with regression tests passing.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
game-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.