Skip to content

Reactive Systems / SystemParams and Resource impl #19723

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

cart
Copy link
Member

@cart cart commented Jun 18, 2025

Objective

We increasingly would like to write "reactive systems" that can detect when their inputs have been changed. This would enable things like "reactive UI" patterns.

Relates to #19712 (but does not implement a run condition ... see Next Steps)

Solution

This is the implementation from my Reactivity Proposal (without the other tangential bits like system entity targets or controversial bits like the reactive Com SystemParam).

  • This adds SystemParam::should_react and System::should_react, making it possible to detect whether a param or system has changed since the last system run in such a way that should prompt a reactive run of a system.
  • It also adds an unsafe variant System::should_react_unsafe, which uses UnsafeWorldCell and can be run in parallel contexts.
  • It adds reactive impls for Res and FunctionSystems

This notably does not add new traits. Reactivity is something that SystemParams and Systems opt in to by overriding default impls that return false for should_react. It is implemented this way for a number of reasons:

  1. It allows all system parameters to be used in reactive contexts, preventing the need for redundant trait impls. People will want to access arbitrary (non-reactive) data in their reactions, and we should let them! Parameters are non-reactive by default.
  2. This allows us to use all existing system infrastructure in a way that works with reactions (ex: piping, adapters, combinators, anything involving boxed systems, etc).
  3. This keeps our code complexity in check: we don't add more traits or redundant impls to the zoo.

Note that I do not think this is necessarily the solution to the "reactive UI" problem. It might be a piece of the puzzle. This PR is about extending the system API to support reacting to input changes.

Open Questions:

  • How will we enable mixing and matching reactive and non-reactive inputs to system? For example Res and ResMut are currently reactive in this PR. Would we use something like NoReact<Res<Time>>, Res<Counter> to prevent reacting to the time parameter while reacting to the Counter parameter? Should we opt parameters in to reactivity via React<Res<Time>>, Res<Counter>? Should we have reactive variants (ex: Res<Time>, ReactRes<Counter>).
  • Should we use the word "react" here (ex: System::should_react)? "Reactivity" intersects with the UI space, and this feature may or may not be used for the official reactive Bevy UI solution.

Next Steps

  • I made this callable from arbitrary parallel contexts (ex: to support building a run condition wrapper system). However run conditions don't have access to the state of the system they are the condition for, so there is no way for it to know whether the system has been run. We can't just use a duplicate of the system in the run condition, as the change ticks would not update. We can't update the wrapped reactive system's change ticks when should_react is true (aka the ShouldReactRunCondition returns true), because we can't be sure the system will actually run (as the run condition could be combined with other run conditions). We would need to be able to somehow access the actual target system's change ticks from within run condition systems, which I'm not sure is something we want to do. I'm not sure we even want/need a run condition, as most "reactive" scenarios I can imagine would be their own system execution orchestrators (ex: reactive UI running in an exclusive system that polls reactive systems for changes and runs reactions to completion).
  • Make more params reactive by implementing should_react()
  • Add "reactive queries" that track when matched entities have changed. These would likely require additional overhead and infrastructure, so it probably makes sense to make this a new system param. But thats an open question!
  • Consider adding something like my Reactivity Proposal's "system entity inputs", which system params can access. This would enable writing things like:
// reactions that run for specific entities
fn reactive_entity_system(health: Com<Health>) {
  log!("new health: {}", health.0);
}
world.spawn((
  Health(10),
  reactions![reactive_entity_system]
))

// observers that can directly access entity components for the triggered entity
let e1 = world.spawn(Velocity::default())
  .observe(|trigger: On<Jump>, mut velocity: ComMut<Velocity>| {
    velocity.0 = Vec3::new(0.0, 10.0, 0.0); 
  })
  .id();

world.trigger_targets(Jump, e1);
  • Consider upstreaming something like my Reactivity Proposal's reaction executor.

@cart cart added the A-ECS Entities, components, systems, and events label Jun 18, 2025
@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-UI Graphical user interfaces, styles, layouts, and widgets S-Needs-Review Needs reviewer attention (from anyone!) to move forward X-Controversial There is active debate or serious implications around merging this PR M-Needs-Release-Note Work that should be called out in the blog due to impact labels Jun 18, 2025
@alice-i-cecile alice-i-cecile added this to the 0.17 milestone Jun 18, 2025
@alice-i-cecile alice-i-cecile removed the M-Needs-Release-Note Work that should be called out in the blog due to impact label Jun 18, 2025
@alice-i-cecile
Copy link
Member

Upon review, this isn't usable enough to warrant a release note yet :) Still a very nice, clean step forward!

@alice-i-cecile
Copy link
Member

Would we use something like NoReact<Res>, Res to prevent reacting to the time parameter while reacting to the Counter parameter? Should we opt parameters in to reactivity via React<Res>, Res? Should we have reactive variants (ex: Res, ReactRes).

I would like this to be opt-out using a wrapper param. We recently merged When this cycle for a similar thing WRT fallible systems, and I think we should use the same pattern here.

@cart
Copy link
Member Author

cart commented Jun 18, 2025

Upon review, this isn't usable enough to warrant a release note yet :) Still a very nice, clean step forward!

Yeah I think we need to sort out opt-in/out param UX first

Carter0

This comment was marked as off-topic.

Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the right shape of solution (and much more thought out than my proposal): I'm happy to land this as an incremental step forward.

@Diddykonga
Copy link

Diddykonga commented Jun 19, 2025

Very nice, I like the ability to poll a System if any of its SystemParams have Updated/Changed.
If/When we get Queries as Entities, and Observers used to to keep them Updated/Changed, we can likely expand this out to push-Systems.
Archetypes Update -> ArchetypeEvent
On<ArchetypeEvent> -> Queries Update -> QueryEvent
On<QueryEvent> -> Systems Update/Run -> (??? SystemEvent, updates/runs dependent Systems)

@TimJentzsch
Copy link
Contributor

Feel free to ignore this, but the Com/ComMut syntax is strange to me. I get that it's short for Component, but my first thought on seeing it was that it was some other fancy thing I didn't know about. Maybe I'm just not used to it though

Why not just have Query<&Velocity> instead like everything else?

I agree, it took me a while to figure out what Com was supposed to mean. Abbreviations are always dangerous in that aspect.

Copy link
Contributor

@chescock chescock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool!

  • Should we use the word "react" here (ex: System::should_react)? "Reactivity" intersects with the UI space, and this feature may or may not be used for the official reactive Bevy UI solution.

A more neutral option here might be has_param_changed, in analogy to the existing is_changed terminology. (Or maybe you're planning to extend this in a way where that would no longer match?)

last_run: Tick,
this_run: Tick,
) -> bool {
P::should_react(state, system_meta, world, last_run, this_run)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You propagated should_react through tuples and StaticSystemParam, but there are a handful of other wrapper types that we might also want to propagate through: When, Option, Result, Vec, DynSystemParam, ParamSet<(tuples)>, and ParamSet<Vec>. We'll probably want to propagate through #[derive(SystemParam)], too.

Or maybe you were already counting those under "Next Steps: Make more params reactive".

(Hmm, it might be hard to make Option<Res<T>> react to the resource being removed, so maybe that one is even trickier.)

Comment on lines +734 to +744
if let Some(state) = &self.state {
<F::Param as SystemParam>::should_react(
&state.param,
&self.system_meta,
world,
last_run,
this_run,
)
} else {
false
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be a bit more idiomatic using is_some_and

Suggested change
if let Some(state) = &self.state {
<F::Param as SystemParam>::should_react(
&state.param,
&self.system_meta,
world,
last_run,
this_run,
)
} else {
false
}
self.state.as_ref().is_some_and(|state| {
F::Param::should_react(&state.param, &self.system_meta, world, last_run, this_run)
})

}

/// Returns true if this system's input has changed since its last run and should therefore be run again to "react" to those changes.
fn should_react(&self, world: &mut World, this_run: Tick) -> bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really want to allow mutable access in should_react? In contrast, validate_param only permits read access, so that it can be called with &World instead of needing &mut World.

Copy link
Contributor

@ElliottjPierce ElliottjPierce left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a good, uncontroversial, small step forward. IIUC, this is effectively going to turn into pull style observers. To be honest, I don't know how useful that would be over normal observers, but I've seen you're reactivity ideas, so I'm trusting that you know where this is headed.

On the subject of including "react" in the names, here's my two cents: Systems params will have either changed since the last run or not (a bool). Wether or not a system should react or not feels more like three options: ("I'm not reactive", "I'm reactive and should run", "I'm reactive but shouldn't run".) So personally, I would prefer either doing should_react() -> Option<bool>, where None would mean the system should run since it's not reactive, or you could try any_params_changed() -> bool and is_reactive() -> bool. I don't think it's a huge deal either way, but that's my thought.

For what it's worth, Com and ComMut confused me at first too. Two ideas to consider: If this really is just a component value, why not Ref and Mut. Or even &T and &mut T? Or, if it is intended to support all QueryData types, maybe This<Q: QueryData> or OfTriggeredEntity<Q> or OfWatched<Q>, etc. I don't what what we'd land on, and we don't need to decide now, but those are some ideas.

In any event, this looks good to me. It's nice to see incremental improvements like this.

last_run: Tick,
this_run: Tick,
) -> bool {
let resource_data = world.storages().resources.get(*state).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a better way to handle this besides unwrapping? Other unwraps here give a nice error message. It seems possible that the should_react could cause a crash with an opaque error message before get_param would crash with a user friendly error message. It may also be worth providing a nice error message when getting the ticks fails. Same goes for Res as well.

/// - If [`System::is_exclusive`] returns `true`, then it must be valid to call
/// [`UnsafeWorldCell::world_mut`] on `world`.
unsafe fn should_react_unsafe(&self, _world: UnsafeWorldCell, _this_run: Tick) -> bool {
false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False is probably the correct default if we plan to make reactive runs (like those "run if any params changed" ideas) fully opt-in. It's worth mentioning that if we default this to true, we could just make every system run if any params change. That's maybe not what you're shooting for, but I figured I'd point it out.

@viridia
Copy link
Contributor

viridia commented Jun 19, 2025

I was going to suggest adapting the core_widgets example to use this as a way to validate the idea. This currently uses systems and change detection (including removal detection). However, the more I think about it, the more I realize it would be a regression.

The current code updates widgets whose state have changed, using a combination of Changed, Added, and RemovedComponents. Switching this to use should_react() means that I get less information than I did previously - it only lets me know that some widget changed, but not which one. So I end up updating every button any time a single button gets hovered.

To do this efficiently, I'd need to be able to use .should_react() as a query filter, that is, only showing me rows where something changed.

The real problem, then, is that the core_widgets systems design doesn't fit the use case that .should_react() was designed for. In the previous experiments we have done, our reactive architecture was made up of many small atomized systems, where each system concerns itself with a small number of entities, perhaps only one. It wasn't meant for the "classic" ECS system which is doing massive bulk operations on many entities.

These small-scoped systems have their own problem, which is what I think you were getting at with Com: the fact that in order to access data on a single component for a single entity, you have to cast a wide net using queries. If reactions are automatically attached to queries this means that your system is constantly going to be reacting to things that are irrelevant.

@alice-i-cecile alice-i-cecile added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Blessed Has a large architectural impact or tradeoffs, but the design has been endorsed by decision makers and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward X-Controversial There is active debate or serious implications around merging this PR labels Jun 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events A-UI Graphical user interfaces, styles, layouts, and widgets C-Feature A new feature, making something new possible S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Blessed Has a large architectural impact or tradeoffs, but the design has been endorsed by decision makers
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants