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
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1872,6 +1872,17 @@ description = "Define component hooks to manage component lifecycle events"
category = "ECS (Entity Component System)"
wasm = false

[[example]]
name = "with_component_hooks"
path = "examples/ecs/with_component_hooks.rs"
doc-scrape-examples = true

[package.metadata.example.with_component_hooks]
name = "With Component Hooks"
description = "Demonstrate how to use the with_component_hooks API"
category = "ECS (Entity Component System)"
wasm = false

[[example]]
name = "custom_schedule"
path = "examples/ecs/custom_schedule.rs"
Expand Down
34 changes: 33 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::{ComponentHooks, RequiredComponentsError},
event::{event_update_system, EventCursor},
intern::Interned,
prelude::*,
Expand Down Expand Up @@ -1322,6 +1322,38 @@ impl App {
self.world_mut().add_observer(observer);
self
}

/// Allows access to [`World::register_component_hooks`] method directly from the app.
///
/// # Examples
///
/// ```rust
/// # use bevy_app::prelude::*;
/// # use bevy_ecs::prelude::*;
/// # use bevy_ecs::component::ComponentHooks;
/// # use bevy_utils::default;
/// #
/// # let mut app = App::new();
/// #
/// #
/// # #[derive(Component)]
/// # struct MyComponent;
/// #
/// // An observer system can be any system where the first parameter is a trigger
/// app.with_component_hooks::<MyComponent>(|hooks: &mut ComponentHooks| {
/// hooks.on_add(|mut world, entity, component_id| {
/// println!("Component: {component_id:?} added to : {entity:?}");
/// });
/// });
/// ```
pub fn with_component_hooks<T>(&mut self, hooks: impl Fn(&mut ComponentHooks)) -> &mut Self
where
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
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ Example | Description
[System Parameter](../examples/ecs/system_param.rs) | Illustrates creating custom system parameters with `SystemParam`
[System Piping](../examples/ecs/system_piping.rs) | Pipe the output of one system into a second, allowing you to handle any errors gracefully
[System Stepping](../examples/ecs/system_stepping.rs) | Demonstrate stepping through systems in order of execution.
[With Component Hooks](../examples/ecs/with_component_hooks.rs) | Demonstrate how to use the with_component_hooks API

## Games

Expand Down
88 changes: 88 additions & 0 deletions examples/ecs/with_component_hooks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//! 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::prelude::*;
use std::collections::HashMap;

#[derive(Debug, Component)]
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));
}
}
Loading