bevyengine / bevyengine/bevy

DynamicScene::write_to_world does not trigger ChildOf on_insert hooks, leaving parent Children component unpopulated

Open
#23,258 1 comment 0 reactions 0 assignees View on GitHub
A-ECS A-Scenes C-Bug D-Complex S-Needs-Design
Dominant language
Rust
Stars
48.2k
Forks
4.8k
Avg merge
3d 22h
Merged PRs (30d)
161

Description

## Bevy version and features

- Bevy 0.18.0, default features

## Relevant system information

- Rust 1.86 (stable)
- Windows 11 Home 10.0.26200
- `AdapterInfo { name: "NVIDIA GeForce RTX 3060", vendor: 4318, device: 9351, device_type: DiscreteGpu, driver: "NVIDIA", driver_info: "595.71", backend:
Vulkan }`

## What you did

Saved a scene containing a parent-child hierarchy using `DynamicSceneBuilder`. The parent entity ("Group") has `Name` + `Transform`. Children have
`ChildOf(parent)` + `Name` + `Transform` + custom components. Loaded the scene back using `DynamicScene::write_to_world`.

## What went wrong

**Expected:** After `write_to_world`, the parent entity should have a `Children` component populated by the `ChildOf` on_insert hook, and Bevy's
`propagate_transforms` system should compute correct `GlobalTransform` values for children.

**Actual:** The parent entity never receives a `Children` component. `ChildOf` is present on the children and correctly references the parent, but the hook
that normally maintains `Children` does not fire. This causes:

1. `GlobalTransform` for all children stays at `(0, 0, 0)` permanently — `propagate_transforms` never walks into them because `Children` is empty/missing on
the parent.
2. Children render at the world origin regardless of their local `Transform`.
3. `InheritedVisibility` propagation also breaks (B0004 warnings).

Diagnostic output showing the problem persists across multiple frames:

```text
Frame 1 (immediately after write_to_world):
Group (33v0): local=(0,0,0) global=(0,0,0) children=0 (Children never populated)
Cone (34v0): local=(-5.35,0,0) global=(0,0,0) parent=33v0
Cube (37v0): local=(5.57,0,0) global=(0,0,0) parent=33v0

Frame 5 (several frames later):
Group (33v0): local=(0,0,0) global=(0,0,0) children=0 (still empty)
Cone (34v0): local=(-5.35,0,0) global=(0,0,0) parent=33v0 (never propagated)
Cube (37v0): local=(5.57,0,0) global=(0,0,0) parent=33v0 (never propagated)

The issue is that write_to_world inserts ChildOf via the reflection path (ReflectComponent::insert), which does not trigger the component's on_insert hook
that would call add_child on the parent.

Additional information

Workaround: After write_to_world, remove and re-insert ChildOf on each child entity to force the hooks to fire:

let children_with_parents: Vec<(Entity, Entity)> = entity_map
.values()
.filter_map(|&entity| {
world.entity(entity).get::().map(|c| (entity, c.parent()))
})
.collect();

for (child, parent) in children_with_parents {
world.entity_mut(child).remove::();
world.entity_mut(child).insert(ChildOf(parent));
}

This immediately populates Children on the parent and transform/visibility propagation works correctly.

Scope: This affects any scene with parent-child hierarchies loaded via DynamicScene::write_to_world. Flat scenes (no ChildOf) are unaffected.
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.