component_hooks/
component_hooks.rs

1//! This example illustrates the different ways you can employ component lifecycle hooks.
2//!
3//! Whenever possible, prefer using Bevy's change detection or Events for reacting to component changes.
4//! Events generally offer better performance and more flexible integration into Bevy's systems.
5//! Hooks are useful to enforce correctness but have limitations (only one hook per component,
6//! less ergonomic than events).
7//!
8//! Here are some cases where components hooks might be necessary:
9//!
10//! - Maintaining indexes: If you need to keep custom data structures (like a spatial index) in
11//!   sync with the addition/removal of components.
12//!
13//! - Enforcing structural rules: When you have systems that depend on specific relationships
14//!   between components (like hierarchies or parent-child links) and need to maintain correctness.
15
16use bevy::{
17    ecs::component::{ComponentHook, HookContext, Mutable, StorageType},
18    prelude::*,
19};
20use std::collections::HashMap;
21
22#[derive(Debug)]
23/// Hooks can also be registered during component initialization by
24/// using [`Component`] derive macro:
25/// ```no_run
26/// #[derive(Component)]
27/// #[component(on_add = ..., on_insert = ..., on_replace = ..., on_remove = ...)]
28/// ```
29struct MyComponent(KeyCode);
30
31impl Component for MyComponent {
32    const STORAGE_TYPE: StorageType = StorageType::Table;
33    type Mutability = Mutable;
34
35    /// Hooks can also be registered during component initialization by
36    /// implementing the associated method
37    fn on_add() -> Option<ComponentHook> {
38        // We don't have an `on_add` hook so we'll just return None.
39        // Note that this is the default behavior when not implementing a hook.
40        None
41    }
42}
43
44#[derive(Resource, Default, Debug, Deref, DerefMut)]
45struct MyComponentIndex(HashMap<KeyCode, Entity>);
46
47#[derive(Event)]
48struct MyEvent;
49
50fn main() {
51    App::new()
52        .add_plugins(DefaultPlugins)
53        .add_systems(Startup, setup)
54        .add_systems(Update, trigger_hooks)
55        .init_resource::<MyComponentIndex>()
56        .add_event::<MyEvent>()
57        .run();
58}
59
60fn setup(world: &mut World) {
61    // In order to register component hooks the component must:
62    // - not be currently in use by any entities in the world
63    // - not already have a hook of that kind registered
64    // This is to prevent overriding hooks defined in plugins and other crates as well as keeping things fast
65    world
66        .register_component_hooks::<MyComponent>()
67        // There are 4 component lifecycle hooks: `on_add`, `on_insert`, `on_replace` and `on_remove`
68        // A hook has 2 arguments:
69        // - a `DeferredWorld`, this allows access to resource and component data as well as `Commands`
70        // - a `HookContext`, this provides access to the following contextual information:
71        //   - the entity that triggered the hook
72        //   - the component id of the triggering component, this is mostly used for dynamic components
73        //   - the location of the code that caused the hook to trigger
74        //
75        // `on_add` will trigger when a component is inserted onto an entity without it
76        .on_add(
77            |mut world,
78             HookContext {
79                 entity,
80                 component_id,
81                 caller,
82                 ..
83             }| {
84                // You can access component data from within the hook
85                let value = world.get::<MyComponent>(entity).unwrap().0;
86                println!(
87                    "{component_id:?} added to {entity} with value {value:?}{}",
88                    caller
89                        .map(|location| format!("due to {location}"))
90                        .unwrap_or_default()
91                );
92                // Or access resources
93                world
94                    .resource_mut::<MyComponentIndex>()
95                    .insert(value, entity);
96                // Or send events
97                world.send_event(MyEvent);
98            },
99        )
100        // `on_insert` will trigger when a component is inserted onto an entity,
101        // regardless of whether or not it already had it and after `on_add` if it ran
102        .on_insert(|world, _| {
103            println!("Current Index: {:?}", world.resource::<MyComponentIndex>());
104        })
105        // `on_replace` will trigger when a component is inserted onto an entity that already had it,
106        // and runs before the value is replaced.
107        // Also triggers when a component is removed from an entity, and runs before `on_remove`
108        .on_replace(|mut world, context| {
109            let value = world.get::<MyComponent>(context.entity).unwrap().0;
110            world.resource_mut::<MyComponentIndex>().remove(&value);
111        })
112        // `on_remove` will trigger when a component is removed from an entity,
113        // since it runs before the component is removed you can still access the component data
114        .on_remove(
115            |mut world,
116             HookContext {
117                 entity,
118                 component_id,
119                 caller,
120                 ..
121             }| {
122                let value = world.get::<MyComponent>(entity).unwrap().0;
123                println!(
124                    "{component_id:?} removed from {entity} with value {value:?}{}",
125                    caller
126                        .map(|location| format!("due to {location}"))
127                        .unwrap_or_default()
128                );
129                // You can also issue commands through `.commands()`
130                world.commands().entity(entity).despawn();
131            },
132        );
133}
134
135fn trigger_hooks(
136    mut commands: Commands,
137    keys: Res<ButtonInput<KeyCode>>,
138    index: Res<MyComponentIndex>,
139) {
140    for (key, entity) in index.iter() {
141        if !keys.pressed(*key) {
142            commands.entity(*entity).remove::<MyComponent>();
143        }
144    }
145    for key in keys.get_just_pressed() {
146        commands.spawn(MyComponent(*key));
147    }
148}