From fde35c8af9c6af38c9e397d560742d893f0a249f Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Mon, 29 Jul 2024 10:51:09 -0400 Subject: [PATCH 1/8] Initial user-space prototype --- Cargo.toml | 11 +++ examples/ui/reactivity.rs | 172 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 examples/ui/reactivity.rs diff --git a/Cargo.toml b/Cargo.toml index 5f162b21a372f..2e2fe6ec988d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2837,6 +2837,17 @@ description = "Illustrates various features of Bevy UI" category = "UI (User Interface)" wasm = true +[[example]] +name = "reactivity" +path = "examples/ui/reactivity.rs" +doc-scrape-examples = true + +[package.metadata.example.reactivity] +name = "Reactivity" +description = "Demonstrates how to create reactive, automatically updating user interfaces in Bevy" +category = "UI (User Interface)" +wasm = true + [[example]] name = "ui_scaling" path = "examples/ui/ui_scaling.rs" diff --git a/examples/ui/reactivity.rs b/examples/ui/reactivity.rs new file mode 100644 index 0000000000000..02ae77fce5b3d --- /dev/null +++ b/examples/ui/reactivity.rs @@ -0,0 +1,172 @@ +//! Reactivity is a technique that allows your UI to automatically update when the data that defines its state changes. +//! +//! This example demonstrates how to use reactivity in Bevy with observers. +//! +//! There are a few key benefits to using reactivity in your UI: +//! +//! - **Deduplication of Spawning and Updating Logic**: When you spawn an entity, you can declare what its value should be. +//! - **Automatic Updates**: When the data that defines your UI state changes, the UI will automatically update to reflect those changes. +//! - **Widget-bound Behavior**: By defining the behavior of a widget in the same place as its data, you can simply spawn the widget and let the spawned observers handle the rest. +//! +//! # Observers +//! +//! Observers are a way to listen for and respond to entity-targeted events. +//! In Bevy, they have several key properties: +//! +//! - You can access both the event and the entity that the event is targeted at. +//! - Observers can only be triggered via commands: any triggers will be deferred until the next synchronization point where exclusive world access is available. +//! - Observers occur immediately after the event is triggered. +//! - Observers can be used to trigger other events, creating a cascade of reactive updates. +//! - Observers can be set to watch for events targeting a specific entity, or for any event of that type. +//! +//! # Incrementalization +//! +//! In order to avoid recomputing the entire UI every frame, Bevy uses a technique called incrementalization. +//! This means that Bevy will only update the parts of the UI that have changed. +//! +//! The key techniques here are **change detection**, which is tracked and triggered by the `Mut` and `ResMut` smart pointers, +//! and **lifecycle hooks**, which are events emitted whenever components are added or removed (including when entities are spawned or despawned). +//! +//! This gives us a very powerful set of standardized events that we can listen for and respond to: +//! +//! - [`OnAdd`]: triggers when a matching component is added to an entity. +//! - [`OnInsert`]: triggers when a component is added to or overwritten on an entity. +//! - [`OnReplace`]: triggers when a component is removed from or overwritten on on an entity. +//! - [`OnRemove`]: triggers when a component is removed from an entity. +//! +//! Note that "overwritten" has a specific meaning here: these are only triggered if the components value is changed via a new insertion operation. +//! Ordinary mutations to the component's value will not trigger these events. +//! +//! However, we can opt into change-detection powered observers by calling `app.generate_on_mutate::()`. +//! This will watch for changes to the component and trigger a [`OnMutate`] event targeting the entity whose component has changed. +//! It's important to note that mutations are observed whenever components are *added* to the entity as well, +//! ensuring that reactive behavior is triggered even when the widget is first spawned. +//! +//! In addition, arbitrary events can be defined and triggered, which is an excellent pattern for behavior that requires a more complex or specialized response. +//! +//! # This example +//! +//! To demonstrate these concepts, we're going to create a simple UI that displays a counter. +//! We'll then create a button that increments the counter when clicked. + +use bevy::prelude::*; +use on_mutate::{GenOnMutate, OnMutate}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .generate_on_mutate::() + .generate_on_mutate::() + .add_systems(Startup, setup_ui) + .run(); +} + +#[derive(Component)] +struct CounterValue(u32); + +fn setup_ui(mut commands: Commands) { + commands.spawn(Camera2dBundle::default()); + + // Counter + let counter_entity = commands + .spawn(TextBundle { ..default() }) + .insert(CounterValue(0)) + .observe( + |trigger: Trigger>, + mut query: Query<(&CounterValue, &mut Text)>| { + let (counter_value, mut text) = query.get_mut(trigger.entity()).unwrap(); + *text = Text::from_section(counter_value.0.to_string(), TextStyle::default()); + }, + ) + .id(); + + // Button + commands + .spawn(ButtonBundle { + style: Style { + width: Val::Px(100.), + height: Val::Px(100.), + justify_self: JustifySelf::End, + ..default() + }, + background_color: Color::WHITE.into(), + ..default() + }) + .observe( + move |trigger: Trigger>, + interaction_query: Query<&Interaction>, + mut counter_query: Query<&mut CounterValue>| { + let interaction = interaction_query.get(trigger.entity()).unwrap(); + if matches!(interaction, Interaction::Pressed) { + // We can move this value into the closure that we define, + // allowing us to create custom behavior for the button. + let mut counter = counter_query.get_mut(counter_entity).unwrap(); + counter.0 += 1; + } + }, + ); +} + +/// This temporary module prototypes a user-space implementation of the [`OnMutate`] event. +/// +/// This comes with two key caveats: +/// +/// 1. Rather than being continually generated on every change between observers, +/// the list of [`OnMutate`] events is generated once at the start of the frame. +/// This restricts our ability to react indefinitely within a single frame, but is a good starting point. +/// 2. [`OnMutate`] will not have a generic parameter: instead, that will be handled via the second [`Trigger`] generic +/// and a static component ID, like the rest of the lifecycle events. This is just cosmetic. +/// +/// To make this pattern hold up in practice, we likely need: +/// +/// 0. Deep integration for the [`OnMutate`] event, so we can check for it in the same way as the other lifecycle events. +/// 1. Resource equivalents to all of the lifecycle hooks. +/// 2. Asset equivalents to all of the lifecycle hooks. +/// 3. Asset change detection. +/// +/// As follow-up, we definitely want: +/// +/// 1. Archetype-level change tracking. +/// 2. A way to automatically detect whether or not change detection triggers are needed. +/// 3. Better tools to gracefully exit observers when standard operations fail. +/// 4. Relations to make defining entity-links more robust and simpler. +/// 5. Nicer picking events to avoid having to use the naive OnMutate pattern. +/// +/// We might also want: +/// +/// 1. Syntax sugar to fetch matching components from the triggered entity in observers +mod on_mutate { + use super::*; + use std::marker::PhantomData; + + /// A trigger emitted when a component is mutated on an entity. + /// + /// This must be explicitly generated using [`GenOnMutate::generate_on_mutate`]. + #[derive(Event, Debug, Clone, Copy)] + pub struct OnMutate(PhantomData); + + impl Default for OnMutate { + fn default() -> Self { + Self(PhantomData) + } + } + + /// A temporary extension trait used to prototype this functionality. + pub trait GenOnMutate { + fn generate_on_mutate(&mut self) -> &mut Self; + } + + impl GenOnMutate for App { + fn generate_on_mutate(&mut self) -> &mut Self { + self.add_systems(First, watch_for_mutations::); + + self + } + } + + fn watch_for_mutations(mut commands: Commands, query: Query>) { + // Note that this is a linear time check, even when no mutations have occurred. + // To accelerate this properly, we need to implement archetype-level change tracking. + commands.trigger_targets(OnMutate::::default(), query.iter().collect::>()); + } +} From 73988de80c211bf00a9b57dba269a2ea664bf976 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Mon, 29 Jul 2024 11:24:22 -0400 Subject: [PATCH 2/8] Update examples README --- examples/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/README.md b/examples/README.md index 56678ffd5ac25..8a4bfd12adff5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -464,6 +464,7 @@ Example | Description [Font Atlas Debug](../examples/ui/font_atlas_debug.rs) | Illustrates how FontAtlases are populated (used to optimize text rendering internally) [Overflow](../examples/ui/overflow.rs) | Simple example demonstrating overflow behavior [Overflow and Clipping Debug](../examples/ui/overflow_debug.rs) | An example to debug overflow and clipping behavior +[Reactivity](../examples/ui/reactivity.rs) | Demonstrates how to create reactive, automatically updating user interfaces in Bevy [Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component [Render UI to Texture](../examples/ui/render_ui_to_texture.rs) | An example of rendering UI as a part of a 3D world [Size Constraints](../examples/ui/size_constraints.rs) | Demonstrates how the to use the size constraints to control the size of a UI node. From fcc340de3c03395038a28e56b485086253902bd1 Mon Sep 17 00:00:00 2001 From: Alice Date: Thu, 1 Aug 2024 16:45:15 -0400 Subject: [PATCH 3/8] Swap example to ECS --- Cargo.toml | 22 +++++++++---------- examples/README.md | 1 - .../responding_to_changes.rs} | 0 3 files changed, 11 insertions(+), 12 deletions(-) rename examples/{ui/reactivity.rs => ecs/responding_to_changes.rs} (100%) diff --git a/Cargo.toml b/Cargo.toml index 2e2fe6ec988d4..974cb9e90deb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2582,6 +2582,17 @@ description = "Demonstrates event propagation with observers" category = "ECS (Entity Component System)" wasm = true +[[example]] +name = "responding_to_changes" +path = "examples/ui/responding_to_changes.rs" +doc-scrape-examples = true + +[package.metadata.example.reactivity] +name = "Responding to Changes" +description = "Demonstrates how and when to use change detection and `OnMutate` hooks and observers" +category = "ECS (Entity Component System)" +wasm = true + [[example]] name = "3d_rotation" path = "examples/transforms/3d_rotation.rs" @@ -2837,17 +2848,6 @@ description = "Illustrates various features of Bevy UI" category = "UI (User Interface)" wasm = true -[[example]] -name = "reactivity" -path = "examples/ui/reactivity.rs" -doc-scrape-examples = true - -[package.metadata.example.reactivity] -name = "Reactivity" -description = "Demonstrates how to create reactive, automatically updating user interfaces in Bevy" -category = "UI (User Interface)" -wasm = true - [[example]] name = "ui_scaling" path = "examples/ui/ui_scaling.rs" diff --git a/examples/README.md b/examples/README.md index 8a4bfd12adff5..56678ffd5ac25 100644 --- a/examples/README.md +++ b/examples/README.md @@ -464,7 +464,6 @@ Example | Description [Font Atlas Debug](../examples/ui/font_atlas_debug.rs) | Illustrates how FontAtlases are populated (used to optimize text rendering internally) [Overflow](../examples/ui/overflow.rs) | Simple example demonstrating overflow behavior [Overflow and Clipping Debug](../examples/ui/overflow_debug.rs) | An example to debug overflow and clipping behavior -[Reactivity](../examples/ui/reactivity.rs) | Demonstrates how to create reactive, automatically updating user interfaces in Bevy [Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component [Render UI to Texture](../examples/ui/render_ui_to_texture.rs) | An example of rendering UI as a part of a 3D world [Size Constraints](../examples/ui/size_constraints.rs) | Demonstrates how the to use the size constraints to control the size of a UI node. diff --git a/examples/ui/reactivity.rs b/examples/ecs/responding_to_changes.rs similarity index 100% rename from examples/ui/reactivity.rs rename to examples/ecs/responding_to_changes.rs From a6c73b5c0ca5997870c9ab1296007854904cd58b Mon Sep 17 00:00:00 2001 From: Alice Date: Thu, 1 Aug 2024 16:46:32 -0400 Subject: [PATCH 4/8] Remove existing mediocre example --- Cargo.toml | 11 ---- examples/ecs/component_change_detection.rs | 58 ---------------------- 2 files changed, 69 deletions(-) delete mode 100644 examples/ecs/component_change_detection.rs diff --git a/Cargo.toml b/Cargo.toml index 974cb9e90deb7..b2397560f6999 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1609,17 +1609,6 @@ description = "Show how to use `apply_deferred` system" category = "ECS (Entity Component System)" wasm = false -[[example]] -name = "component_change_detection" -path = "examples/ecs/component_change_detection.rs" -doc-scrape-examples = true - -[package.metadata.example.component_change_detection] -name = "Component Change Detection" -description = "Change detection on components" -category = "ECS (Entity Component System)" -wasm = false - [[example]] name = "component_hooks" path = "examples/ecs/component_hooks.rs" diff --git a/examples/ecs/component_change_detection.rs b/examples/ecs/component_change_detection.rs deleted file mode 100644 index 096fb4a9fe60a..0000000000000 --- a/examples/ecs/component_change_detection.rs +++ /dev/null @@ -1,58 +0,0 @@ -//! This example illustrates how to react to component change. - -use bevy::prelude::*; -use rand::Rng; - -fn main() { - App::new() - .add_plugins(DefaultPlugins) - .add_systems(Startup, setup) - .add_systems( - Update, - (change_component, change_detection, tracker_monitoring), - ) - .run(); -} - -#[derive(Component, PartialEq, Debug)] -struct MyComponent(f32); - -fn setup(mut commands: Commands) { - commands.spawn(MyComponent(0.)); - commands.spawn(Transform::IDENTITY); -} - -fn change_component(time: Res