-
-
Couldn't load subscription status.
- Fork 4.2k
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -193,6 +193,25 @@ pub trait System: Send + Sync + 'static { | |
| /// However, it can be an essential escape hatch when, for example, | ||
| /// you are trying to synchronize representations using change detection and need to avoid infinite recursion. | ||
| fn set_last_run(&mut self, last_run: Tick); | ||
|
|
||
| /// Returns true if this system's input has changed since its last run and should therefore be run again to "react" to those changes. | ||
| /// | ||
| /// # Safety | ||
| /// | ||
| /// - The caller must ensure that [`world`](UnsafeWorldCell) has permission to access any world data | ||
| /// registered in the access returned from [`System::initialize`]. There must be no conflicting | ||
| /// simultaneous accesses while the system is running. | ||
| /// - 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| } | ||
|
|
||
| /// 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 { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we really want to allow mutable access in |
||
| // SAFETY: We have exclusive access to the entire world. | ||
| unsafe { self.should_react_unsafe(world.as_unsafe_world_cell(), this_run) } | ||
| } | ||
| } | ||
|
|
||
| /// [`System`] types that do not modify the [`World`] when run. | ||
|
|
@@ -484,4 +503,58 @@ mod tests { | |
| let expected = "System bevy_ecs::system::system::tests::run_system_once_invalid_params::system did not run due to failed parameter validation: Parameter `Res<T>` failed validation: Resource does not exist\nIf this is an expected state, wrap the parameter in `Option<T>` and handle `None` when it happens, or wrap the parameter in `When<T>` to skip the system when it happens."; | ||
| assert_eq!(expected, result.unwrap_err().to_string()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn system_should_react() { | ||
| struct A(usize); | ||
| impl Resource for A {} | ||
| struct B(usize); | ||
| impl Resource for B {} | ||
| let mut world = World::new(); | ||
| world.insert_resource(A(0)); | ||
| world.insert_resource(B(0)); | ||
|
|
||
| let mut system = IntoSystem::into_system(|_a: Res<A>, _b: Res<B>| {}); | ||
| system.initialize(&mut world); | ||
| let current_tick = world.change_tick(); | ||
| assert!( | ||
| system.should_react(&mut world, current_tick), | ||
| "system should react because the system has not yet seen the initial values of A or B" | ||
| ); | ||
| // run the system as a "reaction" | ||
| system.run((), &mut world); | ||
| let current_tick = world.change_tick(); | ||
| assert!( | ||
| !system.should_react(&mut world, current_tick), | ||
| "system should not react because nothing has changed since the last run", | ||
| ); | ||
|
|
||
| world.resource_mut::<A>().0 += 1; | ||
| let current_tick = world.change_tick(); | ||
| assert!( | ||
| system.should_react(&mut world, current_tick), | ||
| "system should react because A changed since the last system run" | ||
| ); | ||
| system.run((), &mut world); | ||
|
|
||
| let current_tick = world.change_tick(); | ||
| assert!( | ||
| !system.should_react(&mut world, current_tick), | ||
| "system should not react because nothing has changed since the last run" | ||
| ); | ||
|
|
||
| world.resource_mut::<B>().0 += 1; | ||
| let current_tick = world.change_tick(); | ||
| assert!( | ||
| system.should_react(&mut world, current_tick), | ||
| "system should react because B changed since the last system run" | ||
| ); | ||
| system.run((), &mut world); | ||
|
|
||
| let current_tick = world.change_tick(); | ||
| assert!( | ||
| !system.should_react(&mut world, current_tick), | ||
| "system should not react because nothing has changed since the last run" | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -308,6 +308,23 @@ pub unsafe trait SystemParam: Sized { | |
| world: UnsafeWorldCell<'world>, | ||
| change_tick: Tick, | ||
| ) -> Self::Item<'world, 'state>; | ||
|
|
||
| /// Returns true if this system param has changed since the system was last run. | ||
| /// | ||
| /// # Safety | ||
| /// | ||
| /// - The passed [`UnsafeWorldCell`] must have access to any world data registered | ||
| /// in [`init_access`](SystemParam::init_access). | ||
| /// - `world` must be the same [`World`] that was used to initialize [`state`](SystemParam::init_state). | ||
| unsafe fn should_react( | ||
| _state: &Self::State, | ||
| _system_meta: &SystemMeta, | ||
| _world: UnsafeWorldCell, | ||
| _last_run: Tick, | ||
| _this_run: Tick, | ||
| ) -> bool { | ||
| false | ||
| } | ||
| } | ||
|
|
||
| /// A [`SystemParam`] that only reads a given [`World`]. | ||
|
|
@@ -821,6 +838,20 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { | |
| changed_by: caller.map(|caller| caller.deref()), | ||
| } | ||
| } | ||
|
|
||
| unsafe fn should_react( | ||
| state: &Self::State, | ||
| _system_meta: &SystemMeta, | ||
| world: UnsafeWorldCell, | ||
| last_run: Tick, | ||
| this_run: Tick, | ||
| ) -> bool { | ||
| let resource_data = world.storages().resources.get(*state).unwrap(); | ||
| resource_data | ||
| .get_ticks() | ||
| .unwrap() | ||
| .is_changed(last_run, this_run) | ||
| } | ||
| } | ||
|
|
||
| // SAFETY: Res ComponentId access is applied to SystemMeta. If this Res | ||
|
|
@@ -899,6 +930,20 @@ unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> { | |
| changed_by: value.changed_by, | ||
| } | ||
| } | ||
|
|
||
| unsafe fn should_react( | ||
| state: &Self::State, | ||
| _system_meta: &SystemMeta, | ||
| world: UnsafeWorldCell, | ||
| last_run: Tick, | ||
| this_run: Tick, | ||
| ) -> bool { | ||
| let resource_data = world.storages().resources.get(*state).unwrap(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| resource_data | ||
| .get_ticks() | ||
| .unwrap() | ||
| .is_changed(last_run, this_run) | ||
| } | ||
| } | ||
|
|
||
| /// SAFETY: only reads world | ||
|
|
@@ -2163,6 +2208,12 @@ macro_rules! impl_system_param_tuple { | |
| )] | ||
| ($($param::get_param($param, system_meta, world, change_tick),)*) | ||
| } | ||
|
|
||
| #[inline] | ||
| unsafe fn should_react(state: &Self::State, system_meta: &SystemMeta, _world: UnsafeWorldCell, _last_run: Tick, _this_run: Tick) -> bool { | ||
| let ($($param,)*) = &state; | ||
| false $(|| <$param as SystemParam>::should_react($param, system_meta, _world, _last_run, _this_run))* | ||
| } | ||
| } | ||
| }; | ||
| } | ||
|
|
@@ -2327,6 +2378,16 @@ unsafe impl<P: SystemParam + 'static> SystemParam for StaticSystemParam<'_, '_, | |
| // SAFETY: Defer to the safety of P::SystemParam | ||
| StaticSystemParam(unsafe { P::get_param(state, system_meta, world, change_tick) }) | ||
| } | ||
|
|
||
| unsafe fn should_react( | ||
| state: &Self::State, | ||
| system_meta: &SystemMeta, | ||
| world: UnsafeWorldCell, | ||
| last_run: Tick, | ||
| this_run: Tick, | ||
| ) -> bool { | ||
| P::should_react(state, system_meta, world, last_run, this_run) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You propagated Or maybe you were already counting those under "Next Steps: Make more params reactive". (Hmm, it might be hard to make |
||
| } | ||
| } | ||
|
|
||
| // SAFETY: No world access. | ||
|
|
||
There was a problem hiding this comment.
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