From 2f3584091119ab82a1836917e57fc2e11de3568f Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Fri, 9 Aug 2024 19:51:46 +0200 Subject: [PATCH] Use observers for callbacks (#249) Co-authored-by: Ben Frankel --- docs/design.md | 32 +++++++++++++++++++++++++------- src/screens/credits.rs | 6 ++---- src/screens/title.rs | 17 ++++++----------- src/theme/interaction.rs | 39 +++++++++------------------------------ src/theme/widgets.rs | 5 ++--- 5 files changed, 44 insertions(+), 55 deletions(-) diff --git a/docs/design.md b/docs/design.md index 97651969..669738fd 100644 --- a/docs/design.md +++ b/docs/design.md @@ -237,28 +237,46 @@ as custom commands don't return `Entity` or `EntityCommands`. This kind of usage ### Pattern When spawning an entity that can be interacted with, such as a button that can be pressed, -register a [one-shot system](https://bevyengine.org/news/bevy-0-12/#one-shot-systems) to handle the interaction: +use an observer to handle the interaction: ```rust fn spawn_button(mut commands: Commands) { - let pay_money = commands.register_one_shot_system(pay_money); - commands.button("Pay up!", pay_money); + // See the Widgets pattern for information on the `button` method + commands.button("Pay up!").observe(pay_money); +} + +fn pay_money(_trigger: Trigger, mut money: ResMut) { + money.0 -= 10.0; } ``` -The resulting `SystemId` is added as a newtype component on the button entity. -See the definition of [`OnPress`](../src/theme/interaction.rs) for how this is done. +The event `OnPress`, which is [defined in this template](../src/theme/interaction.rs), +is triggered when the button is [`Interaction::Pressed`](https://docs.rs/bevy/latest/bevy/prelude/enum.Interaction.html#variant.Pressed). + +If you have many interactions that only change a state, consider using the following helper function: + +```rust +fn spawn_button(mut commands: Commands) { + commands.button("Play the game").observe(enter_state(Screen::Playing)); +} + +fn enter_state( + new_state: S, +) -> impl Fn(Trigger, ResMut>) { + move |_trigger, mut next_state| next_state.set(new_state.clone()) +} +``` ### Reasoning This pattern is inspired by [bevy_mod_picking](https://github.com/aevyrie/bevy_mod_picking). -By adding the system handling the interaction to the entity as a component, +By pairing the system handling the interaction with the entity as an observer, the code running on interactions can be scoped to the exact context of the interaction. For example, the code for what happens when you press a *specific* button is directly attached to that exact button. This also keeps the interaction logic close to the entity that is interacted with, -allowing for better code organization. If you want multiple buttons to do the same thing, consider triggering an event in their callbacks. +allowing for better code organization. ## Dev Tools diff --git a/src/screens/credits.rs b/src/screens/credits.rs index 3a83ced2..470c8db8 100644 --- a/src/screens/credits.rs +++ b/src/screens/credits.rs @@ -11,8 +11,6 @@ pub(super) fn plugin(app: &mut App) { } fn show_credits_screen(mut commands: Commands) { - let enter_title = commands.register_one_shot_system(enter_title); - commands .ui_root() .insert(StateScoped(Screen::Credits)) @@ -26,7 +24,7 @@ fn show_credits_screen(mut commands: Commands) { children.label("Ducky sprite - CC0 by Caz Creates Games"); children.label("Music - CC BY 3.0 by Kevin MacLeod"); - children.button("Back", enter_title); + children.button("Back").observe(enter_title); }); commands.play_bgm(BgmHandles::PATH_CREDITS); @@ -36,6 +34,6 @@ fn stop_bgm(mut commands: Commands) { commands.stop_bgm(); } -fn enter_title(mut next_screen: ResMut>) { +fn enter_title(_trigger: Trigger, mut next_screen: ResMut>) { next_screen.set(Screen::Title); } diff --git a/src/screens/title.rs b/src/screens/title.rs index fd0d0367..599f766b 100644 --- a/src/screens/title.rs +++ b/src/screens/title.rs @@ -10,32 +10,27 @@ pub(super) fn plugin(app: &mut App) { } fn show_title_screen(mut commands: Commands) { - let enter_playing = commands.register_one_shot_system(enter_playing); - let enter_credits = commands.register_one_shot_system(enter_credits); - #[cfg(not(target_family = "wasm"))] - let exit_app = commands.register_one_shot_system(exit_app); - commands .ui_root() .insert(StateScoped(Screen::Title)) .with_children(|children| { - children.button("Play", enter_playing); - children.button("Credits", enter_credits); + children.button("Play").observe(enter_playing); + children.button("Credits").observe(enter_credits); #[cfg(not(target_family = "wasm"))] - children.button("Exit", exit_app); + children.button("Exit").observe(exit_app); }); } -fn enter_playing(mut next_screen: ResMut>) { +fn enter_playing(_trigger: Trigger, mut next_screen: ResMut>) { next_screen.set(Screen::Playing); } -fn enter_credits(mut next_screen: ResMut>) { +fn enter_credits(_trigger: Trigger, mut next_screen: ResMut>) { next_screen.set(Screen::Credits); } #[cfg(not(target_family = "wasm"))] -fn exit_app(mut app_exit: EventWriter) { +fn exit_app(_trigger: Trigger, mut app_exit: EventWriter) { app_exit.send(AppExit::Success); } diff --git a/src/theme/interaction.rs b/src/theme/interaction.rs index 28da4564..ff5a7018 100644 --- a/src/theme/interaction.rs +++ b/src/theme/interaction.rs @@ -4,16 +4,14 @@ use crate::{assets::SfxHandles, audio::sfx::SfxCommands as _}; pub(super) fn plugin(app: &mut App) { app.register_type::(); - app.register_type::(); app.add_systems( Update, ( - apply_on_press, + trigger_on_press, apply_interaction_palette, trigger_interaction_sfx, ), ); - app.observe(despawn_one_shot_system); } /// Palette for widget interactions. Add this to an entity that supports @@ -27,24 +25,18 @@ pub struct InteractionPalette { pub pressed: Color, } -/// Component that calls a [one-shot system](https://bevyengine.org/news/bevy-0-12/#one-shot-systems) -/// when the [`Interaction`] component on the same entity changes to -/// [`Interaction::Pressed`]. Use this in conjuction with -/// [`Commands::register_one_shot_system`] to create a callback for e.g. a -/// button press. -#[derive(Component, Debug, Reflect, Deref, DerefMut)] -#[reflect(Component, from_reflect = false)] -// The reflect attributes are currently needed due to -// [`SystemId` not implementing `Reflect`](https://github.com/bevyengine/bevy/issues/14496) -pub struct OnPress(#[reflect(ignore)] pub SystemId); +/// Event triggered on a UI entity when the [`Interaction`] component on the same entity changes to +/// [`Interaction::Pressed`]. Observe this event to detect e.g. button presses. +#[derive(Event)] +pub struct OnPress; -fn apply_on_press( - interaction_query: Query<(&Interaction, &OnPress), Changed>, +fn trigger_on_press( + interaction_query: Query<(Entity, &Interaction), Changed>, mut commands: Commands, ) { - for (interaction, &OnPress(system_id)) in &interaction_query { + for (entity, interaction) in &interaction_query { if matches!(interaction, Interaction::Pressed) { - commands.run_system(system_id); + commands.trigger_targets(OnPress, entity); } } } @@ -77,16 +69,3 @@ fn trigger_interaction_sfx( } } } - -/// Remove the one-shot system entity when the [`OnPress`] component is removed. -/// This is necessary as otherwise, the system would still exist after the button -/// is removed, causing a memory leak. -fn despawn_one_shot_system( - trigger: Trigger, - mut commands: Commands, - on_press_query: Query<&OnPress>, -) { - let on_press = on_press_query.get(trigger.entity()).unwrap(); - let one_shot_system_entity = on_press.entity(); - commands.entity(one_shot_system_entity).despawn_recursive(); -} diff --git a/src/theme/widgets.rs b/src/theme/widgets.rs index 80e5bf2a..addaedd0 100644 --- a/src/theme/widgets.rs +++ b/src/theme/widgets.rs @@ -14,7 +14,7 @@ use super::{ /// An extension trait for spawning UI widgets. pub trait Widgets { /// Spawn a simple button with text. - fn button(&mut self, text: impl Into, on_press: SystemId) -> EntityCommands; + fn button(&mut self, text: impl Into) -> EntityCommands; /// Spawn a simple header label. Bigger than [`Widgets::label`]. fn header(&mut self, text: impl Into) -> EntityCommands; @@ -24,7 +24,7 @@ pub trait Widgets { } impl Widgets for T { - fn button(&mut self, text: impl Into, on_press: SystemId) -> EntityCommands { + fn button(&mut self, text: impl Into) -> EntityCommands { let mut entity = self.spawn(( Name::new("Button"), ButtonBundle { @@ -43,7 +43,6 @@ impl Widgets for T { hovered: BUTTON_HOVERED_BACKGROUND, pressed: BUTTON_PRESSED_BACKGROUND, }, - OnPress(on_press), )); entity.with_children(|children| { children.spawn((