docs/bevy/examples/ecs/component_hooks.rs 5.9 K · 149 lines · raw · history
| 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 | |
| 16 | use bevy::{ |
| 17 | ecs::component::{Mutable, StorageType}, |
| 18 | ecs::lifecycle::{ComponentHook, HookContext}, |
| 19 | prelude::*, |
| 20 | }; |
| 21 | use 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_discard = ..., on_remove = ...)] |
| 29 | /// ``` |
| 30 | struct MyComponent(KeyCode); |
| 31 | |
| 32 | impl 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)] |
| 46 | struct MyComponentIndex(HashMap<KeyCode, Entity>); |
| 47 | |
| 48 | #[derive(Message)] |
| 49 | struct MyMessage; |
| 50 | |
| 51 | fn 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 | |
| 61 | fn 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_discard` 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_discard` 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_discard(|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 | |
| 136 | fn 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 | } |