Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added App::with_component_hooks #16977

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Next Next commit
Added with_component_hooks method for easier access to component hooks
  • Loading branch information
Freya Pines authored and Freya Pines committed Dec 26, 2024
commit 9a4a1f7b3fa623bfa7639bf87560001e5efd292e
12 changes: 11 additions & 1 deletion crates/bevy_app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use alloc::{
};
pub use bevy_derive::AppLabel;
use bevy_ecs::{
component::RequiredComponentsError,
component::{ComponentHook, ComponentHooks, RequiredComponentsError},
event::{event_update_system, EventCursor},
intern::Interned,
prelude::*,
Expand Down Expand Up @@ -1322,6 +1322,16 @@ impl App {
self.world_mut().add_observer(observer);
self
}

pub fn with_component_hooks<T, F>(&mut self, hooks: F) -> &mut Self
where
F: Fn(&mut ComponentHooks) -> (),
T: Component,
{
let component_hooks = self.world_mut().register_component_hooks::<T>();
hooks(component_hooks);
self
}
}

type RunnerFn = Box<dyn FnOnce(App) -> AppExit>;
Expand Down
91 changes: 91 additions & 0 deletions examples/ecs/with_component_hooks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//! This example illustrates the way to register component hooks directly from the app.
//!
//! Whenever possible, prefer using Bevy's change detection or Events for reacting to component changes.
//! Events generally offer better performance and more flexible integration into Bevy's systems.
//! Hooks are useful to enforce correctness but have limitations (only one hook per component,
//! less ergonomic than events).
//!
//! Here are some cases where components hooks might be necessary:
//!
//! - Maintaining indexes: If you need to keep custom data structures (like a spatial index) in
//! sync with the addition/removal of components.
//!
//! - Enforcing structural rules: When you have systems that depend on specific relationships
//! between components (like hierarchies or parent-child links) and need to maintain correctness.

use bevy::{
ecs::component::{ComponentHooks, Mutable, StorageType},
prelude::*,
};
use std::collections::HashMap;

#[derive(Debug)]
struct MyComponent(KeyCode);

#[derive(Resource, Default, Debug, Deref, DerefMut)]
struct MyComponentIndex(HashMap<KeyCode, Entity>);

#[derive(Event)]
struct MyEvent;

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Update, trigger_hooks)
.with_component_hooks::<MyComponent>(|hooks| {
hooks
.on_add(|mut world, entity, component_id| {
// You can access component data from within the hook
let value = world.get::<MyComponent>(entity).unwrap().0;
println!(
"Component: {component_id:?} added to: {entity:?} with value {value:?}"
);
// Or access resources
world
.resource_mut::<MyComponentIndex>()
.insert(value, entity);
// Or send events
world.send_event(MyEvent);
})
// `on_insert` will trigger when a component is inserted onto an entity,
// regardless of whether or not it already had it and after `on_add` if it ran
.on_insert(|world, _, _| {
println!("Current Index: {:?}", world.resource::<MyComponentIndex>());
})
// `on_replace` will trigger when a component is inserted onto an entity that already had it,
// and runs before the value is replaced.
// Also triggers when a component is removed from an entity, and runs before `on_remove`
.on_replace(|mut world, entity, _| {
let value = world.get::<MyComponent>(entity).unwrap().0;
world.resource_mut::<MyComponentIndex>().remove(&value);
})
// `on_remove` will trigger when a component is removed from an entity,
// since it runs before the component is removed you can still access the component data
.on_remove(|mut world, entity, component_id| {
let value = world.get::<MyComponent>(entity).unwrap().0;
println!(
"Component: {component_id:?} removed from: {entity:?} with value {value:?}"
);
// You can also issue commands through `.commands()`
world.commands().entity(entity).despawn();
});
})
.init_resource::<MyComponentIndex>()
.add_event::<MyEvent>()
.run();
}

fn trigger_hooks(
mut commands: Commands,
keys: Res<ButtonInput<KeyCode>>,
index: Res<MyComponentIndex>,
) {
for (key, entity) in index.iter() {
if !keys.pressed(*key) {
commands.entity(*entity).remove::<MyComponent>();
}
}
for key in keys.get_just_pressed() {
commands.spawn(MyComponent(*key));
}
}