Skip to content

Commit e4b3687

Browse files
Trashtalk217alice-i-cecileNiliradMinerSebasaevyrie
authored
One Shot Systems (#8963)
I'm adopting this ~~child~~ PR. # Objective - Working with exclusive world access is not always easy: in many cases, a standard system or three is more ergonomic to write, and more modularly maintainable. - For small, one-off tasks (commonly handled with scripting), running an event-reader system incurs a small but flat overhead cost and muddies the schedule. - Certain forms of logic (e.g. turn-based games) want very fine-grained linear and/or branching control over logic. - SystemState is not automatically cached, and so performance can suffer and change detection breaks. - Fixes #2192. - Partial workaround for #279. ## Solution - Adds a SystemRegistry resource to the World, which stores initialized systems keyed by their SystemSet. - Allows users to call world.run_system(my_system) and commands.run_system(my_system), without re-initializing or losing state (essential for change detection). - Add a Callback type to enable convenient use of dynamic one shot systems and reduce the mental overhead of working with Box<dyn SystemSet>. - Allow users to run systems based on their SystemSet, enabling more complex user-made abstractions. ## Future work - Parameterized one-shot systems would improve reusability and bring them closer to events and commands. The API could be something like run_system_with_input(my_system, my_input) and use the In SystemParam. - We should evaluate the unification of commands and one-shot systems since they are two different ways to run logic on demand over a World. ### Prior attempts - #2234 - #2417 - #4090 - #7999 This PR continues the work done in #7999. --------- Co-authored-by: Alice Cecile <[email protected]> Co-authored-by: Federico Rinaldi <[email protected]> Co-authored-by: MinerSebas <[email protected]> Co-authored-by: Aevyrie <[email protected]> Co-authored-by: Alejandro Pascual Pozo <[email protected]> Co-authored-by: Rob Parrett <[email protected]> Co-authored-by: François <[email protected]> Co-authored-by: Dmytro Banin <[email protected]> Co-authored-by: James Liu <[email protected]>
1 parent b995827 commit e4b3687

File tree

10 files changed

+474
-17
lines changed

10 files changed

+474
-17
lines changed

Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1284,6 +1284,16 @@ description = "Shows how to iterate over combinations of query results"
12841284
category = "ECS (Entity Component System)"
12851285
wasm = true
12861286

1287+
[[example]]
1288+
name = "one_shot_systems"
1289+
path = "examples/ecs/one_shot_systems.rs"
1290+
1291+
[package.metadata.example.one_shot_systems]
1292+
name = "One Shot Systems"
1293+
description = "Shows how to flexibly run systems without scheduling them"
1294+
category = "ECS (Entity Component System)"
1295+
wasm = false
1296+
12871297
[[example]]
12881298
name = "parallel_query"
12891299
path = "examples/ecs/parallel_query.rs"

crates/bevy_ecs/src/schedule/set.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ impl<T> Hash for SystemTypeSet<T> {
8181
// all systems of a given type are the same
8282
}
8383
}
84+
8485
impl<T> Clone for SystemTypeSet<T> {
8586
fn clone(&self) -> Self {
8687
*self

crates/bevy_ecs/src/system/commands/mod.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::{
55
self as bevy_ecs,
66
bundle::Bundle,
77
entity::{Entities, Entity},
8+
system::{RunSystem, SystemId},
89
world::{EntityWorldMut, FromWorld, World},
910
};
1011
use bevy_ecs_macros::SystemParam;
@@ -517,11 +518,19 @@ impl<'w, 's> Commands<'w, 's> {
517518
self.queue.push(RemoveResource::<R>::new());
518519
}
519520

521+
/// Runs the system corresponding to the given [`SystemId`].
522+
/// Systems are ran in an exclusive and single threaded way.
523+
/// Running slow systems can become a bottleneck.
524+
///
525+
/// Calls [`World::run_system`](crate::system::World::run_system).
526+
pub fn run_system(&mut self, id: SystemId) {
527+
self.queue.push(RunSystem::new(id));
528+
}
529+
520530
/// Pushes a generic [`Command`] to the command queue.
521531
///
522532
/// `command` can be a built-in command, custom struct that implements [`Command`] or a closure
523533
/// that takes [`&mut World`](World) as an argument.
524-
///
525534
/// # Example
526535
///
527536
/// ```

crates/bevy_ecs/src/system/function_system.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,10 @@ impl SystemMeta {
7272
// (to avoid the need for unwrapping to retrieve SystemMeta)
7373
/// Holds on to persistent state required to drive [`SystemParam`] for a [`System`].
7474
///
75-
/// This is a very powerful and convenient tool for working with exclusive world access,
75+
/// This is a powerful and convenient tool for working with exclusive world access,
7676
/// allowing you to fetch data from the [`World`] as if you were running a [`System`].
77+
/// However, simply calling `world::run_system(my_system)` using a [`World::run_system`](crate::system::World::run_system)
78+
/// can be significantly simpler and ensures that change detection and command flushing work as expected.
7779
///
7880
/// Borrow-checking is handled for you, allowing you to mutably access multiple compatible system parameters at once,
7981
/// and arbitrary system parameters (like [`EventWriter`](crate::event::EventWriter)) can be conveniently fetched.
@@ -89,6 +91,8 @@ impl SystemMeta {
8991
/// - [`Local`](crate::system::Local) variables that hold state
9092
/// - [`EventReader`](crate::event::EventReader) system parameters, which rely on a [`Local`](crate::system::Local) to track which events have been seen
9193
///
94+
/// Note that this is automatically handled for you when using a [`World::run_system`](crate::system::World::run_system).
95+
///
9296
/// # Example
9397
///
9498
/// Basic usage:

crates/bevy_ecs/src/system/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ mod query;
112112
#[allow(clippy::module_inception)]
113113
mod system;
114114
mod system_param;
115+
mod system_registry;
115116

116117
use std::borrow::Cow;
117118

@@ -124,6 +125,7 @@ pub use function_system::*;
124125
pub use query::*;
125126
pub use system::*;
126127
pub use system_param::*;
128+
pub use system_registry::*;
127129

128130
use crate::world::World;
129131

crates/bevy_ecs/src/system/system.rs

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,30 @@ impl<In: 'static, Out: 'static> Debug for dyn System<In = In, Out = Out> {
165165
/// This function is not an efficient method of running systems and its meant to be used as a utility
166166
/// for testing and/or diagnostics.
167167
///
168+
/// Systems called through [`run_system_once`](crate::system::RunSystemOnce::run_system_once) do not hold onto any state,
169+
/// as they are created and destroyed every time [`run_system_once`](crate::system::RunSystemOnce::run_system_once) is called.
170+
/// Practically, this means that [`Local`](crate::system::Local) variables are
171+
/// reset on every run and change detection does not work.
172+
///
173+
/// ```
174+
/// # use bevy_ecs::prelude::*;
175+
/// # use bevy_ecs::system::RunSystemOnce;
176+
/// #[derive(Resource, Default)]
177+
/// struct Counter(u8);
178+
///
179+
/// fn increment(mut counter: Local<Counter>) {
180+
/// counter.0 += 1;
181+
/// println!("{}", counter.0);
182+
/// }
183+
///
184+
/// let mut world = World::default();
185+
/// world.run_system_once(increment); // prints 1
186+
/// world.run_system_once(increment); // still prints 1
187+
/// ```
188+
///
189+
/// If you do need systems to hold onto state between runs, use the [`World::run_system`](crate::system::World::run_system)
190+
/// and run the system by their [`SystemId`](crate::system::SystemId).
191+
///
168192
/// # Usage
169193
/// Typically, to test a system, or to extract specific diagnostics information from a world,
170194
/// you'd need a [`Schedule`](crate::schedule::Schedule) to run the system. This can create redundant boilerplate code
@@ -180,9 +204,9 @@ impl<In: 'static, Out: 'static> Debug for dyn System<In = In, Out = Out> {
180204
/// This usage is helpful when trying to test systems or functions that operate on [`Commands`](crate::system::Commands):
181205
/// ```
182206
/// # use bevy_ecs::prelude::*;
183-
/// # use bevy_ecs::system::RunSystem;
207+
/// # use bevy_ecs::system::RunSystemOnce;
184208
/// let mut world = World::default();
185-
/// let entity = world.run_system(|mut commands: Commands| {
209+
/// let entity = world.run_system_once(|mut commands: Commands| {
186210
/// commands.spawn_empty().id()
187211
/// });
188212
/// # assert!(world.get_entity(entity).is_some());
@@ -193,7 +217,7 @@ impl<In: 'static, Out: 'static> Debug for dyn System<In = In, Out = Out> {
193217
/// This usage is helpful when trying to run an arbitrary query on a world for testing or debugging purposes:
194218
/// ```
195219
/// # use bevy_ecs::prelude::*;
196-
/// # use bevy_ecs::system::RunSystem;
220+
/// # use bevy_ecs::system::RunSystemOnce;
197221
///
198222
/// #[derive(Component)]
199223
/// struct T(usize);
@@ -202,7 +226,7 @@ impl<In: 'static, Out: 'static> Debug for dyn System<In = In, Out = Out> {
202226
/// world.spawn(T(0));
203227
/// world.spawn(T(1));
204228
/// world.spawn(T(1));
205-
/// let count = world.run_system(|query: Query<&T>| {
229+
/// let count = world.run_system_once(|query: Query<&T>| {
206230
/// query.iter().filter(|t| t.0 == 1).count()
207231
/// });
208232
///
@@ -213,7 +237,7 @@ impl<In: 'static, Out: 'static> Debug for dyn System<In = In, Out = Out> {
213237
///
214238
/// ```
215239
/// # use bevy_ecs::prelude::*;
216-
/// # use bevy_ecs::system::RunSystem;
240+
/// # use bevy_ecs::system::RunSystemOnce;
217241
///
218242
/// #[derive(Component)]
219243
/// struct T(usize);
@@ -226,26 +250,26 @@ impl<In: 'static, Out: 'static> Debug for dyn System<In = In, Out = Out> {
226250
/// world.spawn(T(0));
227251
/// world.spawn(T(1));
228252
/// world.spawn(T(1));
229-
/// let count = world.run_system(count);
253+
/// let count = world.run_system_once(count);
230254
///
231255
/// # assert_eq!(count, 2);
232256
/// ```
233-
pub trait RunSystem: Sized {
257+
pub trait RunSystemOnce: Sized {
234258
/// Runs a system and applies its deferred parameters.
235-
fn run_system<T: IntoSystem<(), Out, Marker>, Out, Marker>(self, system: T) -> Out {
236-
self.run_system_with((), system)
259+
fn run_system_once<T: IntoSystem<(), Out, Marker>, Out, Marker>(self, system: T) -> Out {
260+
self.run_system_once_with((), system)
237261
}
238262

239263
/// Runs a system with given input and applies its deferred parameters.
240-
fn run_system_with<T: IntoSystem<In, Out, Marker>, In, Out, Marker>(
264+
fn run_system_once_with<T: IntoSystem<In, Out, Marker>, In, Out, Marker>(
241265
self,
242266
input: In,
243267
system: T,
244268
) -> Out;
245269
}
246270

247-
impl RunSystem for &mut World {
248-
fn run_system_with<T: IntoSystem<In, Out, Marker>, In, Out, Marker>(
271+
impl RunSystemOnce for &mut World {
272+
fn run_system_once_with<T: IntoSystem<In, Out, Marker>, In, Out, Marker>(
249273
self,
250274
input: In,
251275
system: T,
@@ -261,10 +285,11 @@ impl RunSystem for &mut World {
261285
#[cfg(test)]
262286
mod tests {
263287
use super::*;
288+
use crate as bevy_ecs;
264289
use crate::prelude::*;
265290

266291
#[test]
267-
fn run_system() {
292+
fn run_system_once() {
268293
struct T(usize);
269294

270295
impl Resource for T {}
@@ -275,8 +300,53 @@ mod tests {
275300
}
276301

277302
let mut world = World::default();
278-
let n = world.run_system_with(1, system);
303+
let n = world.run_system_once_with(1, system);
279304
assert_eq!(n, 2);
280305
assert_eq!(world.resource::<T>().0, 1);
281306
}
307+
308+
#[derive(Resource, Default, PartialEq, Debug)]
309+
struct Counter(u8);
310+
311+
#[allow(dead_code)]
312+
fn count_up(mut counter: ResMut<Counter>) {
313+
counter.0 += 1;
314+
}
315+
316+
#[test]
317+
fn run_two_systems() {
318+
let mut world = World::new();
319+
world.init_resource::<Counter>();
320+
assert_eq!(*world.resource::<Counter>(), Counter(0));
321+
world.run_system_once(count_up);
322+
assert_eq!(*world.resource::<Counter>(), Counter(1));
323+
world.run_system_once(count_up);
324+
assert_eq!(*world.resource::<Counter>(), Counter(2));
325+
}
326+
327+
#[allow(dead_code)]
328+
fn spawn_entity(mut commands: Commands) {
329+
commands.spawn_empty();
330+
}
331+
332+
#[test]
333+
fn command_processing() {
334+
let mut world = World::new();
335+
assert_eq!(world.entities.len(), 0);
336+
world.run_system_once(spawn_entity);
337+
assert_eq!(world.entities.len(), 1);
338+
}
339+
340+
#[test]
341+
fn non_send_resources() {
342+
fn non_send_count_down(mut ns: NonSendMut<Counter>) {
343+
ns.0 -= 1;
344+
}
345+
346+
let mut world = World::new();
347+
world.insert_non_send_resource(Counter(10));
348+
assert_eq!(*world.non_send_resource::<Counter>(), Counter(10));
349+
world.run_system_once(non_send_count_down);
350+
assert_eq!(*world.non_send_resource::<Counter>(), Counter(9));
351+
}
282352
}

0 commit comments

Comments
 (0)