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}