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::{Mutable, StorageType},
18    ecs::lifecycle::{ComponentHook, HookContext},
19    prelude::*,
20};
21use std::collections::HashMap;
22
23#[derive(Debug)]
24/// Hooks can also be registered during component initialization by
25/// using [`Component`] derive macro:
26/// ```no_run
27/// #[derive(Component)]
28/// #[component(on_add = ..., on_insert = ..., on_replace = ..., on_remove = ...)]
29/// ```
30struct MyComponent(KeyCode);
31
32impl Component for MyComponent {
33    const STORAGE_TYPE: StorageType = StorageType::Table;
34    type Mutability = Mutable;
35
36    /// Hooks can also be registered during component initialization by
37    /// implementing the associated method
38    fn on_add() -> Option<ComponentHook> {
39        // We don't have an `on_add` hook so we'll just return None.
40        // Note that this is the default behavior when not implementing a hook.
41        None
42    }
43}
44
45#[derive(Resource, Default, Debug, Deref, DerefMut)]
46struct MyComponentIndex(HashMap<KeyCode, Entity>);
47
48#[derive(Message)]
49struct MyMessage;
50
51fn main() {
52    App::new()
53        .add_plugins(DefaultPlugins)
54        .add_systems(Startup, setup)
55        .add_systems(Update, trigger_hooks)
56        .init_resource::<MyComponentIndex>()
57        .add_message::<MyMessage>()
58        .run();
59}
60
61fn setup(world: &mut World) {
62    // In order to register component hooks the component must:
63    // - not be currently in use by any entities in the world
64    // - not already have a hook of that kind registered
65    // This is to prevent overriding hooks defined in plugins and other crates as well as keeping things fast
66    world
67        .register_component_hooks::<MyComponent>()
68        // There are 4 component lifecycle hooks: `on_add`, `on_insert`, `on_replace` and `on_remove`
69        // A hook has 2 arguments:
70        // - a `DeferredWorld`, this allows access to resource and component data as well as `Commands`
71        // - a `HookContext`, this provides access to the following contextual information:
72        //   - the entity that triggered the hook
73        //   - the component id of the triggering component, this is mostly used for dynamic components
74        //   - the location of the code that caused the hook to trigger
75        //
76        // `on_add` will trigger when a component is inserted onto an entity without it
77        .on_add(
78            |mut world,
79             HookContext {
80                 entity,
81                 component_id,
82                 caller,
83                 ..
84             }| {
85                // You can access component data from within the hook
86                let value = world.get::<MyComponent>(entity).unwrap().0;
87                println!(
88                    "{component_id:?} added to {entity} with value {value:?}{}",
89                    caller
90                        .map(|location| format!("due to {location}"))
91                        .unwrap_or_default()
92                );
93                // Or access resources
94                world
95                    .resource_mut::<MyComponentIndex>()
96                    .insert(value, entity);
97                // Or send messages
98                world.write_message(MyMessage);
99            },
100        )
101        // `on_insert` will trigger when a component is inserted onto an entity,
102        // regardless of whether or not it already had it and after `on_add` if it ran
103        .on_insert(|world, _| {
104            println!("Current Index: {:?}", world.resource::<MyComponentIndex>());
105        })
106        // `on_replace` will trigger when a component is inserted onto an entity that already had it,
107        // and runs before the value is replaced.
108        // Also triggers when a component is removed from an entity, and runs before `on_remove`
109        .on_replace(|mut world, context| {
110            let value = world.get::<MyComponent>(context.entity).unwrap().0;
111            world.resource_mut::<MyComponentIndex>().remove(&value);
112        })
113        // `on_remove` will trigger when a component is removed from an entity,
114        // since it runs before the component is removed you can still access the component data
115        .on_remove(
116            |mut world,
117             HookContext {
118                 entity,
119                 component_id,
120                 caller,
121                 ..
122             }| {
123                let value = world.get::<MyComponent>(entity).unwrap().0;
124                println!(
125                    "{component_id:?} removed from {entity} with value {value:?}{}",
126                    caller
127                        .map(|location| format!("due to {location}"))
128                        .unwrap_or_default()
129                );
130                // You can also issue commands through `.commands()`
131                world.commands().entity(entity).despawn();
132            },
133        );
134}
135
136fn trigger_hooks(
137    mut commands: Commands,
138    keys: Res<ButtonInput<KeyCode>>,
139    index: Res<MyComponentIndex>,
140) {
141    for (key, entity) in index.iter() {
142        if !keys.pressed(*key) {
143            commands.entity(*entity).remove::<MyComponent>();
144        }
145    }
146    for key in keys.get_just_pressed() {
147        commands.spawn(MyComponent(*key));
148    }
149}