diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 4cdc3a2473d55..f4f15e56c3916 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -1,6 +1,6 @@ use crate::{ - First, Main, MainSchedulePlugin, PlaceholderPlugin, Plugin, Plugins, PluginsState, SubApp, - SubApps, + First, Last, Main, MainSchedulePlugin, PlaceholderPlugin, Plugin, Plugins, PluginsState, + SubApp, SubApps, }; pub use bevy_derive::AppLabel; use bevy_ecs::{ @@ -97,6 +97,8 @@ impl Default for App { #[cfg(feature = "reflect_functions")] app.init_resource::(); + app.init_resource::(); + app.add_plugins(MainSchedulePlugin); app.add_systems( First, @@ -104,6 +106,7 @@ impl Default for App { .in_set(bevy_ecs::event::EventUpdates) .run_if(bevy_ecs::event::event_update_condition), ); + app.add_systems(Last, bevy_ecs::reactivity::update_reactive_components); app.add_event::(); app diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index f18bde71f707a..88e747ffefcc0 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -27,6 +27,7 @@ pub mod intern; pub mod label; pub mod observer; pub mod query; +pub mod reactivity; #[cfg(feature = "bevy_reflect")] pub mod reflect; pub mod removal_detection; @@ -51,6 +52,7 @@ pub mod prelude { event::{Event, EventMutator, EventReader, EventWriter, Events}, observer::{Observer, Trigger}, query::{Added, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, + reactivity::ReactiveComponent, removal_detection::RemovedComponents, schedule::{ apply_deferred, common_conditions::*, Condition, IntoSystemConfigs, IntoSystemSet, diff --git a/crates/bevy_ecs/src/reactivity.rs b/crates/bevy_ecs/src/reactivity.rs new file mode 100644 index 0000000000000..63c6724d21f7f --- /dev/null +++ b/crates/bevy_ecs/src/reactivity.rs @@ -0,0 +1,193 @@ +//! Utilities for automatically updating components in response to other ECS data changing. + +use crate as bevy_ecs; +use bevy_ecs::{ + component::{ComponentHooks, StorageType, Tick}, + prelude::*, +}; +use bevy_ecs_macros::Resource; +use bevy_utils::HashMap; +use core::any::TypeId; + +/// TODO: Docs. +pub struct ReactiveComponent { + source: Entity, + expression: Option Output) + Send + Sync + 'static>>, +} + +impl ReactiveComponent { + /// TODO: Docs. + pub fn new( + source: Entity, + expression: impl (Fn(&Input) -> Output) + Send + Sync + 'static, + ) -> Self { + Self { + source, + expression: Some(Box::new(expression)), + } + } + + fn on_add_hook(entity: Entity, world: &mut World) { + let (source, expression) = { + let mut entity = world.entity_mut(entity); + let mut this = entity.get_mut::().unwrap(); + (this.source, this.expression.take().unwrap()) + }; + + // Compute and insert initial output + let input = world + .get_entity(source) + .expect("TODO: Source entity despawned") + .get::() + .expect("TODO: Source component removed"); + let output = (expression)(input); + world.entity_mut(entity).insert(output); + + // Register the subscription + let subscription = move |world: &mut World, last_run, this_run| { + let mut input = world + .get_entity(source) + .expect("TODO: Source entity despawned") + .get_ref::() + .expect("TODO: Source component removed"); + input.ticks.last_run = last_run; + input.ticks.this_run = this_run; + + let changed = input.is_changed(); + if changed { + let output = (expression)(&input); + world.entity_mut(entity).insert(output); + } + changed + }; + world + .resource_mut::() + .0 + .insert((entity, TypeId::of::()), Box::new(subscription)); + } + + fn on_remove_hook(entity: Entity, world: &mut World) { + // Deregister the subscription + world + .resource_mut::() + .0 + .remove(&(entity, TypeId::of::())); + + // Remove the computed output + if let Ok(mut entity) = world.get_entity_mut(entity) { + entity.remove::(); + } + } +} + +impl Component for ReactiveComponent { + const STORAGE_TYPE: StorageType = StorageType::Table; + + fn register_component_hooks(hooks: &mut ComponentHooks) { + hooks + .on_add(|mut world, entity, _| { + world + .commands() + .queue(move |world: &mut World| Self::on_add_hook(entity, world)); + }) + .on_remove(|mut world, entity, _| { + world + .commands() + .queue(move |world: &mut World| Self::on_remove_hook(entity, world)); + }); + } +} + +/// System to check for changes to [`ReactiveComponent`] expressions and if changed, recompute it. +pub fn update_reactive_components(world: &mut World) { + world.resource_scope(|world, expressions: Mut| { + let mut last_run = world.last_change_tick(); + loop { + let this_run = world.change_tick(); + let mut any_reaction = false; + + for expression in expressions.0.values() { + any_reaction = any_reaction || (expression)(world, last_run, this_run); + } + + if !any_reaction { + break; + } + + last_run = world.increment_change_tick(); + } + }); +} + +/// TODO: Docs. +#[derive(Resource, Default)] +pub struct ReactiveComponentExpressions( + HashMap< + (Entity, TypeId), + Box bool) + Send + Sync + 'static>, + >, +); + +#[cfg(test)] +mod tests { + use crate as bevy_ecs; + use bevy_ecs::{ + prelude::*, + reactivity::{update_reactive_components, ReactiveComponentExpressions}, + }; + + #[derive(Component, PartialEq, Eq, Debug)] + struct Foo(u32); + + #[derive(Component, PartialEq, Eq, Debug)] + struct Bar(u32); + + #[test] + fn test_reactive_component() { + let mut world = World::new(); + world.init_resource::(); + + let source = world.spawn(Foo(0)).id(); + let sink = world + .spawn(ReactiveComponent::new(source, |foo: &Foo| Bar(foo.0))) + .id(); + + world.flush(); + + assert_eq!(world.entity(sink).get::(), Some(&Bar(0))); + + world.get_mut::(source).unwrap().0 += 1; + + update_reactive_components(&mut world); + + assert_eq!(world.entity(sink).get::(), Some(&Bar(1))); + } + + #[test] + fn test_reactive_component_chaining() { + let mut world = World::new(); + world.init_resource::(); + + let a = world.spawn(Foo(0)).id(); + let b = world + .spawn(ReactiveComponent::new(a, |foo: &Foo| Foo(foo.0 + 1))) + .id(); + let c = world + .spawn(ReactiveComponent::new(b, |foo: &Foo| Foo(foo.0 + 1))) + .id(); + + world.flush(); + + assert_eq!(world.entity(a).get::(), Some(&Foo(0))); + assert_eq!(world.entity(b).get::(), Some(&Foo(1))); + assert_eq!(world.entity(c).get::(), Some(&Foo(2))); + + world.get_mut::(a).unwrap().0 = 3; + + update_reactive_components(&mut world); + + assert_eq!(world.entity(a).get::(), Some(&Foo(3))); + assert_eq!(world.entity(b).get::(), Some(&Foo(4))); + assert_eq!(world.entity(c).get::(), Some(&Foo(5))); + } +}