Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions crates/bevy_app/src/app.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -97,13 +97,16 @@ impl Default for App {
#[cfg(feature = "reflect_functions")]
app.init_resource::<AppFunctionRegistry>();

app.init_resource::<bevy_ecs::reactivity::ReactiveComponentExpressions>();

app.add_plugins(MainSchedulePlugin);
app.add_systems(
First,
event_update_system
.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::<AppExit>();

app
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_ecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down
193 changes: 193 additions & 0 deletions crates/bevy_ecs/src/reactivity.rs
Original file line number Diff line number Diff line change
@@ -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<Input: Component, Output: Component> {
source: Entity,
expression: Option<Box<dyn (Fn(&Input) -> Output) + Send + Sync + 'static>>,
}

impl<Input: Component, Output: Component> ReactiveComponent<Input, Output> {
/// 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::<Self>().unwrap();
(this.source, this.expression.take().unwrap())
};

// Compute and insert initial output
let input = world
.get_entity(source)
.expect("TODO: Source entity despawned")
.get::<Input>()
.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::<Input>()
.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::<ReactiveComponentExpressions>()
.0
.insert((entity, TypeId::of::<Self>()), Box::new(subscription));
}

fn on_remove_hook(entity: Entity, world: &mut World) {
// Deregister the subscription
world
.resource_mut::<ReactiveComponentExpressions>()
.0
.remove(&(entity, TypeId::of::<Self>()));

// Remove the computed output
if let Ok(mut entity) = world.get_entity_mut(entity) {
entity.remove::<Output>();
}
}
}

impl<Input: Component, Output: Component> Component for ReactiveComponent<Input, Output> {
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<ReactiveComponentExpressions>| {
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<dyn (Fn(&mut World, Tick, Tick) -> 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::<ReactiveComponentExpressions>();

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::<Bar>(), Some(&Bar(0)));

world.get_mut::<Foo>(source).unwrap().0 += 1;

update_reactive_components(&mut world);

assert_eq!(world.entity(sink).get::<Bar>(), Some(&Bar(1)));
}

#[test]
fn test_reactive_component_chaining() {
let mut world = World::new();
world.init_resource::<ReactiveComponentExpressions>();

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::<Foo>(), Some(&Foo(0)));
assert_eq!(world.entity(b).get::<Foo>(), Some(&Foo(1)));
assert_eq!(world.entity(c).get::<Foo>(), Some(&Foo(2)));

world.get_mut::<Foo>(a).unwrap().0 = 3;

update_reactive_components(&mut world);

assert_eq!(world.entity(a).get::<Foo>(), Some(&Foo(3)));
assert_eq!(world.entity(b).get::<Foo>(), Some(&Foo(4)));
assert_eq!(world.entity(c).get::<Foo>(), Some(&Foo(5)));
}
}
Loading