
Fixes #17720 ## Objective Spawning RelationshipTargets from scenes currently fails to preserve RelationshipTarget ordering (ex: `Children` has an arbitrary order). This is because it uses the normal hook flow to set up the collection, which means we are pushing onto the collection in _spawn order_ (which is currently in archetype order, which will often produce mismatched orderings). We need to preserve the ordering in the original RelationshipTarget collection. Ideally without expensive checking / fixups. ## Solution One solution would be to spawn in hierarchy-order. However this gets complicated as there can be multiple hierarchies, and it also means we can't spawn in more cache-friendly orders (ex: the current per-archetype spawning, or future even-smarter per-table spawning). Additionally, same-world cloning has _slightly_ more nuanced needs (ex: recursively clone linked relationships, while maintaining _original_ relationships outside of the tree via normal hooks). The preferred approach is to directly spawn the remapped RelationshipTarget collection, as this trivially preserves the ordering. Unfortunately we can't _just_ do that, as when we spawn the children with their Relationships (ex: `ChildOf`), that will insert a duplicate. We could "fixup" the collection retroactively by just removing the back half of duplicates, but this requires another pass / more lookups / allocating twice as much space. Additionally, it becomes complicated because observers could insert additional children, making it harder (aka more expensive) to determine which children are dupes and which are not. The path I chose is to support "opting out" of the relationship target hook in the contexts that need that, as this allows us to just cheaply clone the mapped collection. The relationship hook can look for this configuration when it runs and skip its logic when that happens. A "simple" / small-amount-of-code way to do this would be to add a "skip relationship spawn" flag to World. Sadly, any hook / observer that runs _as the result of an insert_ would also read this flag. We really need a way to scope this setting to a _specific_ insert. Therefore I opted to add a new `RelationshipInsertHookMode` enum and an `entity.insert_with_relationship_insert_hook_mode` variant. Obviously this is verbose and ugly. And nobody wants _more_ insert variants. But sadly this was the best I could come up with from a performance and capability perspective. If you have alternatives let me know! There are three variants: 1. `RelationshipInsertHookMode::Run`: always run relationship insert hooks (this is the default) 2. `RelationshipInsertHookMode::Skip`: do not run any relationship insert hooks for this insert (this is used by spawner code) 3. `RelationshipInsertHookMode::RunIfNotLinked`: only run hooks for _unlinked_ relationships (this is used in same-world recursive entity cloning to preserve relationships outside of the deep-cloned tree) Note that I have intentionally only added "insert with relationship hook mode" variants to the cases we absolutely need (everything else uses the default `Run` mode), just to keep the code size in check. I do not think we should add more without real _very necessary_ use cases. I also made some other minor tweaks: 1. I split out `SourceComponent` from `ComponentCloneCtx`. Reading the source component no longer needlessly blocks mutable access to `ComponentCloneCtx`. 2. Thanks to (1), I've removed the `RefCell` wrapper over the cloned component queue. 3. (1) also allowed me to write to the EntityMapper while queuing up clones, meaning we can reserve entities during the component clone and write them to the mapper _before_ inserting the component, meaning cloned collections can be mapped on insert. 4. I've removed the closure from `write_target_component_ptr` to simplify the API / make it compatible with the split `SourceComponent` approach. 5. I've renamed `EntityCloner::recursive` to `EntityCloner::linked_cloning` to connect that feature more directly with `RelationshipTarget::LINKED_SPAWN` 6. I've removed `EntityCloneBehavior::RelationshipTarget`. This was always intended to be temporary, and this new behavior removes the need for it. --------- Co-authored-by: Viktor Gustavsson <villor94@gmail.com>
149 lines
5.8 KiB
Rust
149 lines
5.8 KiB
Rust
//! This example illustrates the different ways you can employ component lifecycle hooks.
|
|
//!
|
|
//! Whenever possible, prefer using Bevy's change detection or Events for reacting to component changes.
|
|
//! Events generally offer better performance and more flexible integration into Bevy's systems.
|
|
//! Hooks are useful to enforce correctness but have limitations (only one hook per component,
|
|
//! less ergonomic than events).
|
|
//!
|
|
//! Here are some cases where components hooks might be necessary:
|
|
//!
|
|
//! - Maintaining indexes: If you need to keep custom data structures (like a spatial index) in
|
|
//! sync with the addition/removal of components.
|
|
//!
|
|
//! - Enforcing structural rules: When you have systems that depend on specific relationships
|
|
//! between components (like hierarchies or parent-child links) and need to maintain correctness.
|
|
|
|
use bevy::{
|
|
ecs::component::{ComponentHook, HookContext, Mutable, StorageType},
|
|
prelude::*,
|
|
};
|
|
use std::collections::HashMap;
|
|
|
|
#[derive(Debug)]
|
|
/// Hooks can also be registered during component initialization by
|
|
/// using [`Component`] derive macro:
|
|
/// ```no_run
|
|
/// #[derive(Component)]
|
|
/// #[component(on_add = ..., on_insert = ..., on_replace = ..., on_remove = ...)]
|
|
/// ```
|
|
struct MyComponent(KeyCode);
|
|
|
|
impl Component for MyComponent {
|
|
const STORAGE_TYPE: StorageType = StorageType::Table;
|
|
type Mutability = Mutable;
|
|
|
|
/// Hooks can also be registered during component initialization by
|
|
/// implementing the associated method
|
|
fn on_add() -> Option<ComponentHook> {
|
|
// We don't have an `on_add` hook so we'll just return None.
|
|
// Note that this is the default behavior when not implementing a hook.
|
|
None
|
|
}
|
|
}
|
|
|
|
#[derive(Resource, Default, Debug, Deref, DerefMut)]
|
|
struct MyComponentIndex(HashMap<KeyCode, Entity>);
|
|
|
|
#[derive(Event)]
|
|
struct MyEvent;
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins(DefaultPlugins)
|
|
.add_systems(Startup, setup)
|
|
.add_systems(Update, trigger_hooks)
|
|
.init_resource::<MyComponentIndex>()
|
|
.add_event::<MyEvent>()
|
|
.run();
|
|
}
|
|
|
|
fn setup(world: &mut World) {
|
|
// In order to register component hooks the component must:
|
|
// - not be currently in use by any entities in the world
|
|
// - not already have a hook of that kind registered
|
|
// This is to prevent overriding hooks defined in plugins and other crates as well as keeping things fast
|
|
world
|
|
.register_component_hooks::<MyComponent>()
|
|
// There are 4 component lifecycle hooks: `on_add`, `on_insert`, `on_replace` and `on_remove`
|
|
// A hook has 2 arguments:
|
|
// - a `DeferredWorld`, this allows access to resource and component data as well as `Commands`
|
|
// - a `HookContext`, this provides access to the following contextual information:
|
|
// - the entity that triggered the hook
|
|
// - the component id of the triggering component, this is mostly used for dynamic components
|
|
// - the location of the code that caused the hook to trigger
|
|
//
|
|
// `on_add` will trigger when a component is inserted onto an entity without it
|
|
.on_add(
|
|
|mut world,
|
|
HookContext {
|
|
entity,
|
|
component_id,
|
|
caller,
|
|
..
|
|
}| {
|
|
// You can access component data from within the hook
|
|
let value = world.get::<MyComponent>(entity).unwrap().0;
|
|
println!(
|
|
"{component_id:?} added to {entity} with value {value:?}{}",
|
|
caller
|
|
.map(|location| format!("due to {location}"))
|
|
.unwrap_or_default()
|
|
);
|
|
// Or access resources
|
|
world
|
|
.resource_mut::<MyComponentIndex>()
|
|
.insert(value, entity);
|
|
// Or send events
|
|
world.send_event(MyEvent);
|
|
},
|
|
)
|
|
// `on_insert` will trigger when a component is inserted onto an entity,
|
|
// regardless of whether or not it already had it and after `on_add` if it ran
|
|
.on_insert(|world, _| {
|
|
println!("Current Index: {:?}", world.resource::<MyComponentIndex>());
|
|
})
|
|
// `on_replace` will trigger when a component is inserted onto an entity that already had it,
|
|
// and runs before the value is replaced.
|
|
// Also triggers when a component is removed from an entity, and runs before `on_remove`
|
|
.on_replace(|mut world, context| {
|
|
let value = world.get::<MyComponent>(context.entity).unwrap().0;
|
|
world.resource_mut::<MyComponentIndex>().remove(&value);
|
|
})
|
|
// `on_remove` will trigger when a component is removed from an entity,
|
|
// since it runs before the component is removed you can still access the component data
|
|
.on_remove(
|
|
|mut world,
|
|
HookContext {
|
|
entity,
|
|
component_id,
|
|
caller,
|
|
..
|
|
}| {
|
|
let value = world.get::<MyComponent>(entity).unwrap().0;
|
|
println!(
|
|
"{component_id:?} removed from {entity} with value {value:?}{}",
|
|
caller
|
|
.map(|location| format!("due to {location}"))
|
|
.unwrap_or_default()
|
|
);
|
|
// You can also issue commands through `.commands()`
|
|
world.commands().entity(entity).despawn();
|
|
},
|
|
);
|
|
}
|
|
|
|
fn trigger_hooks(
|
|
mut commands: Commands,
|
|
keys: Res<ButtonInput<KeyCode>>,
|
|
index: Res<MyComponentIndex>,
|
|
) {
|
|
for (key, entity) in index.iter() {
|
|
if !keys.pressed(*key) {
|
|
commands.entity(*entity).remove::<MyComponent>();
|
|
}
|
|
}
|
|
for key in keys.get_just_pressed() {
|
|
commands.spawn(MyComponent(*key));
|
|
}
|
|
}
|