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}