Skip to content

Commit

Permalink
One Shot Systems (#8963)
Browse files Browse the repository at this point in the history
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 <alice.i.cecile@gmail.com>
Co-authored-by: Federico Rinaldi <gisquerin@gmail.com>
Co-authored-by: MinerSebas <66798382+MinerSebas@users.noreply.github.com>
Co-authored-by: Aevyrie <aevyrie@gmail.com>
Co-authored-by: Alejandro Pascual Pozo <alejandro.pascual.pozo@gmail.com>
Co-authored-by: Rob Parrett <robparrett@gmail.com>
Co-authored-by: François <mockersf@gmail.com>
Co-authored-by: Dmytro Banin <banind@cs.washington.edu>
Co-authored-by: James Liu <contact@jamessliu.com>
  • Loading branch information
10 people authored Sep 19, 2023
1 parent b995827 commit e4b3687
Show file tree
Hide file tree
Showing 10 changed files with 474 additions and 17 deletions.
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,16 @@ description = "Shows how to iterate over combinations of query results"
category = "ECS (Entity Component System)"
wasm = true

[[example]]
name = "one_shot_systems"
path = "examples/ecs/one_shot_systems.rs"

[package.metadata.example.one_shot_systems]
name = "One Shot Systems"
description = "Shows how to flexibly run systems without scheduling them"
category = "ECS (Entity Component System)"
wasm = false

[[example]]
name = "parallel_query"
path = "examples/ecs/parallel_query.rs"
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_ecs/src/schedule/set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ impl<T> Hash for SystemTypeSet<T> {
// all systems of a given type are the same
}
}

impl<T> Clone for SystemTypeSet<T> {
fn clone(&self) -> Self {
*self
Expand Down
11 changes: 10 additions & 1 deletion crates/bevy_ecs/src/system/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::{
self as bevy_ecs,
bundle::Bundle,
entity::{Entities, Entity},
system::{RunSystem, SystemId},
world::{EntityWorldMut, FromWorld, World},
};
use bevy_ecs_macros::SystemParam;
Expand Down Expand Up @@ -517,11 +518,19 @@ impl<'w, 's> Commands<'w, 's> {
self.queue.push(RemoveResource::<R>::new());
}

/// Runs the system corresponding to the given [`SystemId`].
/// Systems are ran in an exclusive and single threaded way.
/// Running slow systems can become a bottleneck.
///
/// Calls [`World::run_system`](crate::system::World::run_system).
pub fn run_system(&mut self, id: SystemId) {
self.queue.push(RunSystem::new(id));
}

/// Pushes a generic [`Command`] to the command queue.
///
/// `command` can be a built-in command, custom struct that implements [`Command`] or a closure
/// that takes [`&mut World`](World) as an argument.
///
/// # Example
///
/// ```
Expand Down
6 changes: 5 additions & 1 deletion crates/bevy_ecs/src/system/function_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,10 @@ impl SystemMeta {
// (to avoid the need for unwrapping to retrieve SystemMeta)
/// Holds on to persistent state required to drive [`SystemParam`] for a [`System`].
///
/// This is a very powerful and convenient tool for working with exclusive world access,
/// This is a powerful and convenient tool for working with exclusive world access,
/// allowing you to fetch data from the [`World`] as if you were running a [`System`].
/// However, simply calling `world::run_system(my_system)` using a [`World::run_system`](crate::system::World::run_system)
/// can be significantly simpler and ensures that change detection and command flushing work as expected.
///
/// Borrow-checking is handled for you, allowing you to mutably access multiple compatible system parameters at once,
/// and arbitrary system parameters (like [`EventWriter`](crate::event::EventWriter)) can be conveniently fetched.
Expand All @@ -89,6 +91,8 @@ impl SystemMeta {
/// - [`Local`](crate::system::Local) variables that hold state
/// - [`EventReader`](crate::event::EventReader) system parameters, which rely on a [`Local`](crate::system::Local) to track which events have been seen
///
/// Note that this is automatically handled for you when using a [`World::run_system`](crate::system::World::run_system).
///
/// # Example
///
/// Basic usage:
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_ecs/src/system/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ mod query;
#[allow(clippy::module_inception)]
mod system;
mod system_param;
mod system_registry;

use std::borrow::Cow;

Expand All @@ -124,6 +125,7 @@ pub use function_system::*;
pub use query::*;
pub use system::*;
pub use system_param::*;
pub use system_registry::*;

use crate::world::World;

Expand Down
98 changes: 84 additions & 14 deletions crates/bevy_ecs/src/system/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,30 @@ impl<In: 'static, Out: 'static> Debug for dyn System<In = In, Out = Out> {
/// This function is not an efficient method of running systems and its meant to be used as a utility
/// for testing and/or diagnostics.
///
/// Systems called through [`run_system_once`](crate::system::RunSystemOnce::run_system_once) do not hold onto any state,
/// as they are created and destroyed every time [`run_system_once`](crate::system::RunSystemOnce::run_system_once) is called.
/// Practically, this means that [`Local`](crate::system::Local) variables are
/// reset on every run and change detection does not work.
///
/// ```
/// # use bevy_ecs::prelude::*;
/// # use bevy_ecs::system::RunSystemOnce;
/// #[derive(Resource, Default)]
/// struct Counter(u8);
///
/// fn increment(mut counter: Local<Counter>) {
/// counter.0 += 1;
/// println!("{}", counter.0);
/// }
///
/// let mut world = World::default();
/// world.run_system_once(increment); // prints 1
/// world.run_system_once(increment); // still prints 1
/// ```
///
/// If you do need systems to hold onto state between runs, use the [`World::run_system`](crate::system::World::run_system)
/// and run the system by their [`SystemId`](crate::system::SystemId).
///
/// # Usage
/// Typically, to test a system, or to extract specific diagnostics information from a world,
/// you'd need a [`Schedule`](crate::schedule::Schedule) to run the system. This can create redundant boilerplate code
Expand All @@ -180,9 +204,9 @@ impl<In: 'static, Out: 'static> Debug for dyn System<In = In, Out = Out> {
/// This usage is helpful when trying to test systems or functions that operate on [`Commands`](crate::system::Commands):
/// ```
/// # use bevy_ecs::prelude::*;
/// # use bevy_ecs::system::RunSystem;
/// # use bevy_ecs::system::RunSystemOnce;
/// let mut world = World::default();
/// let entity = world.run_system(|mut commands: Commands| {
/// let entity = world.run_system_once(|mut commands: Commands| {
/// commands.spawn_empty().id()
/// });
/// # assert!(world.get_entity(entity).is_some());
Expand All @@ -193,7 +217,7 @@ impl<In: 'static, Out: 'static> Debug for dyn System<In = In, Out = Out> {
/// This usage is helpful when trying to run an arbitrary query on a world for testing or debugging purposes:
/// ```
/// # use bevy_ecs::prelude::*;
/// # use bevy_ecs::system::RunSystem;
/// # use bevy_ecs::system::RunSystemOnce;
///
/// #[derive(Component)]
/// struct T(usize);
Expand All @@ -202,7 +226,7 @@ impl<In: 'static, Out: 'static> Debug for dyn System<In = In, Out = Out> {
/// world.spawn(T(0));
/// world.spawn(T(1));
/// world.spawn(T(1));
/// let count = world.run_system(|query: Query<&T>| {
/// let count = world.run_system_once(|query: Query<&T>| {
/// query.iter().filter(|t| t.0 == 1).count()
/// });
///
Expand All @@ -213,7 +237,7 @@ impl<In: 'static, Out: 'static> Debug for dyn System<In = In, Out = Out> {
///
/// ```
/// # use bevy_ecs::prelude::*;
/// # use bevy_ecs::system::RunSystem;
/// # use bevy_ecs::system::RunSystemOnce;
///
/// #[derive(Component)]
/// struct T(usize);
Expand All @@ -226,26 +250,26 @@ impl<In: 'static, Out: 'static> Debug for dyn System<In = In, Out = Out> {
/// world.spawn(T(0));
/// world.spawn(T(1));
/// world.spawn(T(1));
/// let count = world.run_system(count);
/// let count = world.run_system_once(count);
///
/// # assert_eq!(count, 2);
/// ```
pub trait RunSystem: Sized {
pub trait RunSystemOnce: Sized {
/// Runs a system and applies its deferred parameters.
fn run_system<T: IntoSystem<(), Out, Marker>, Out, Marker>(self, system: T) -> Out {
self.run_system_with((), system)
fn run_system_once<T: IntoSystem<(), Out, Marker>, Out, Marker>(self, system: T) -> Out {
self.run_system_once_with((), system)
}

/// Runs a system with given input and applies its deferred parameters.
fn run_system_with<T: IntoSystem<In, Out, Marker>, In, Out, Marker>(
fn run_system_once_with<T: IntoSystem<In, Out, Marker>, In, Out, Marker>(
self,
input: In,
system: T,
) -> Out;
}

impl RunSystem for &mut World {
fn run_system_with<T: IntoSystem<In, Out, Marker>, In, Out, Marker>(
impl RunSystemOnce for &mut World {
fn run_system_once_with<T: IntoSystem<In, Out, Marker>, In, Out, Marker>(
self,
input: In,
system: T,
Expand All @@ -261,10 +285,11 @@ impl RunSystem for &mut World {
#[cfg(test)]
mod tests {
use super::*;
use crate as bevy_ecs;
use crate::prelude::*;

#[test]
fn run_system() {
fn run_system_once() {
struct T(usize);

impl Resource for T {}
Expand All @@ -275,8 +300,53 @@ mod tests {
}

let mut world = World::default();
let n = world.run_system_with(1, system);
let n = world.run_system_once_with(1, system);
assert_eq!(n, 2);
assert_eq!(world.resource::<T>().0, 1);
}

#[derive(Resource, Default, PartialEq, Debug)]
struct Counter(u8);

#[allow(dead_code)]
fn count_up(mut counter: ResMut<Counter>) {
counter.0 += 1;
}

#[test]
fn run_two_systems() {
let mut world = World::new();
world.init_resource::<Counter>();
assert_eq!(*world.resource::<Counter>(), Counter(0));
world.run_system_once(count_up);
assert_eq!(*world.resource::<Counter>(), Counter(1));
world.run_system_once(count_up);
assert_eq!(*world.resource::<Counter>(), Counter(2));
}

#[allow(dead_code)]
fn spawn_entity(mut commands: Commands) {
commands.spawn_empty();
}

#[test]
fn command_processing() {
let mut world = World::new();
assert_eq!(world.entities.len(), 0);
world.run_system_once(spawn_entity);
assert_eq!(world.entities.len(), 1);
}

#[test]
fn non_send_resources() {
fn non_send_count_down(mut ns: NonSendMut<Counter>) {
ns.0 -= 1;
}

let mut world = World::new();
world.insert_non_send_resource(Counter(10));
assert_eq!(*world.non_send_resource::<Counter>(), Counter(10));
world.run_system_once(non_send_count_down);
assert_eq!(*world.non_send_resource::<Counter>(), Counter(9));
}
}
Loading

0 comments on commit e4b3687

Please sign in to comment.