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 https://github.com/bevyengine/bevy/issues/2192.
- Partial workaround for https://github.com/bevyengine/bevy/issues/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

- https://github.com/bevyengine/bevy/pull/2234
- https://github.com/bevyengine/bevy/pull/2417
- https://github.com/bevyengine/bevy/pull/4090
- https://github.com/bevyengine/bevy/pull/7999

This PR continues the work done in
https://github.com/bevyengine/bevy/pull/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>
This commit is contained in:
Trashtalk217 2023-09-19 22:17:05 +02:00 committed by GitHub
parent b995827013
commit e4b368721d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 474 additions and 17 deletions

View file

@ -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"

View file

@ -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

View file

@ -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;
@ -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
///
/// ```

View file

@ -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.
@ -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:

View file

@ -112,6 +112,7 @@ mod query;
#[allow(clippy::module_inception)]
mod system;
mod system_param;
mod system_registry;
use std::borrow::Cow;
@ -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;

View file

@ -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
@ -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());
@ -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);
@ -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()
/// });
///
@ -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);
@ -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,
@ -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 {}
@ -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));
}
}

View file

@ -0,0 +1,301 @@
use crate::entity::Entity;
use crate::system::{BoxedSystem, Command, IntoSystem};
use crate::world::World;
use crate::{self as bevy_ecs};
use bevy_ecs_macros::Component;
/// A small wrapper for [`BoxedSystem`] that also keeps track whether or not the system has been initialized.
#[derive(Component)]
struct RegisteredSystem {
initialized: bool,
system: BoxedSystem,
}
/// A system that has been removed from the registry.
/// It contains the system and whether or not it has been initialized.
///
/// This struct is returned by [`World::remove_system`].
pub struct RemovedSystem {
initialized: bool,
system: BoxedSystem,
}
impl RemovedSystem {
/// Is the system initialized?
/// A system is initialized the first time it's ran.
pub fn initialized(&self) -> bool {
self.initialized
}
/// The system removed from the storage.
pub fn system(self) -> BoxedSystem {
self.system
}
}
/// An identifier for a registered system.
///
/// These are opaque identifiers, keyed to a specific [`World`],
/// and are created via [`World::register_system`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SystemId(Entity);
impl World {
/// Registers a system and returns a [`SystemId`] so it can later be called by [`World::run_system`].
///
/// It's possible to register the same systems more than once, they'll be stored seperately.
///
/// This is different from adding systems to a [`Schedule`](crate::schedule::Schedule),
/// because the [`SystemId`] that is returned can be used anywhere in the [`World`] to run the associated system.
/// This allows for running systems in a pushed-based fashion.
/// Using a [`Schedule`](crate::schedule::Schedule) is still preferred for most cases
/// due to its better performance and abillity to run non-conflicting systems simultaneously.
pub fn register_system<M, S: IntoSystem<(), (), M> + 'static>(
&mut self,
system: S,
) -> SystemId {
SystemId(
self.spawn(RegisteredSystem {
initialized: false,
system: Box::new(IntoSystem::into_system(system)),
})
.id(),
)
}
/// Removes a registered system and returns the system, if it exists.
/// After removing a system, the [`SystemId`] becomes invalid and attempting to use it afterwards will result in errors.
/// Re-adding the removed system will register it on a new [`SystemId`].
///
/// If no system corresponds to the given [`SystemId`], this method returns an error.
/// Systems are also not allowed to remove themselves, this returns an error too.
pub fn remove_system(&mut self, id: SystemId) -> Result<RemovedSystem, RegisteredSystemError> {
match self.get_entity_mut(id.0) {
Some(mut entity) => {
let registered_system = entity
.take::<RegisteredSystem>()
.ok_or(RegisteredSystemError::SelfRemove(id))?;
entity.despawn();
Ok(RemovedSystem {
initialized: registered_system.initialized,
system: registered_system.system,
})
}
None => Err(RegisteredSystemError::SystemIdNotRegistered(id)),
}
}
/// Run stored systems by their [`SystemId`].
/// Before running a system, it must first be registered.
/// The method [`World::register_system`] stores a given system and returns a [`SystemId`].
/// This is different from [`RunSystemOnce::run_system_once`](crate::system::RunSystemOnce::run_system_once),
/// because it keeps local state between calls and change detection works correctly.
///
/// # Limitations
///
/// - Stored systems cannot be chained: they can neither have an [`In`](crate::system::In) nor return any values.
/// - Stored systems cannot be recursive, they cannot call themselves through [`Commands::run_system`](crate::system::Commands).
/// - Exclusive systems cannot be used.
///
/// # Examples
///
/// ```rust
/// # use bevy_ecs::prelude::*;
/// #[derive(Resource, Default)]
/// struct Counter(u8);
///
/// fn increment(mut counter: Local<Counter>) {
/// counter.0 += 1;
/// println!("{}", counter.0);
/// }
///
/// let mut world = World::default();
/// let counter_one = world.register_system(increment);
/// let counter_two = world.register_system(increment);
/// world.run_system(counter_one); // -> 1
/// world.run_system(counter_one); // -> 2
/// world.run_system(counter_two); // -> 1
/// ```
///
/// Change detection:
///
/// ```rust
/// # use bevy_ecs::prelude::*;
/// #[derive(Resource, Default)]
/// struct ChangeDetector;
///
/// let mut world = World::default();
/// world.init_resource::<ChangeDetector>();
/// let detector = world.register_system(|change_detector: ResMut<ChangeDetector>| {
/// if change_detector.is_changed() {
/// println!("Something happened!");
/// } else {
/// println!("Nothing happened.");
/// }
/// });
///
/// // Resources are changed when they are first added
/// let _ = world.run_system(detector); // -> Something happened!
/// let _ = world.run_system(detector); // -> Nothing happened.
/// world.resource_mut::<ChangeDetector>().set_changed();
/// let _ = world.run_system(detector); // -> Something happened!
/// ```
pub fn run_system(&mut self, id: SystemId) -> Result<(), RegisteredSystemError> {
// lookup
let mut entity = self
.get_entity_mut(id.0)
.ok_or(RegisteredSystemError::SystemIdNotRegistered(id))?;
// take ownership of system trait object
let RegisteredSystem {
mut initialized,
mut system,
} = entity
.take::<RegisteredSystem>()
.ok_or(RegisteredSystemError::Recursive(id))?;
// run the system
if !initialized {
system.initialize(self);
initialized = true;
}
system.run((), self);
system.apply_deferred(self);
// return ownership of system trait object (if entity still exists)
if let Some(mut entity) = self.get_entity_mut(id.0) {
entity.insert::<RegisteredSystem>(RegisteredSystem {
initialized,
system,
});
}
Ok(())
}
}
/// The [`Command`] type for [`World::run_system`].
///
/// This command runs systems in an exclusive and single threaded way.
/// Running slow systems can become a bottleneck.
#[derive(Debug, Clone)]
pub struct RunSystem {
system_id: SystemId,
}
impl RunSystem {
/// Creates a new [`Command`] struct, which can be added to [`Commands`](crate::system::Commands)
pub fn new(system_id: SystemId) -> Self {
Self { system_id }
}
}
impl Command for RunSystem {
#[inline]
fn apply(self, world: &mut World) {
let _ = world.run_system(self.system_id);
}
}
/// An operation with stored systems failed.
#[derive(Debug)]
pub enum RegisteredSystemError {
/// A system was run by id, but no system with that id was found.
///
/// Did you forget to register it?
SystemIdNotRegistered(SystemId),
/// A system tried to run itself recursively.
Recursive(SystemId),
/// A system tried to remove itself.
SelfRemove(SystemId),
}
mod tests {
use crate as bevy_ecs;
use crate::prelude::*;
#[derive(Resource, Default, PartialEq, Debug)]
struct Counter(u8);
#[test]
fn change_detection() {
#[derive(Resource, Default)]
struct ChangeDetector;
fn count_up_iff_changed(
mut counter: ResMut<Counter>,
change_detector: ResMut<ChangeDetector>,
) {
if change_detector.is_changed() {
counter.0 += 1;
}
}
let mut world = World::new();
world.init_resource::<ChangeDetector>();
world.init_resource::<Counter>();
assert_eq!(*world.resource::<Counter>(), Counter(0));
// Resources are changed when they are first added.
let id = world.register_system(count_up_iff_changed);
let _ = world.run_system(id);
assert_eq!(*world.resource::<Counter>(), Counter(1));
// Nothing changed
let _ = world.run_system(id);
assert_eq!(*world.resource::<Counter>(), Counter(1));
// Making a change
world.resource_mut::<ChangeDetector>().set_changed();
let _ = world.run_system(id);
assert_eq!(*world.resource::<Counter>(), Counter(2));
}
#[test]
fn local_variables() {
// The `Local` begins at the default value of 0
fn doubling(last_counter: Local<Counter>, mut counter: ResMut<Counter>) {
counter.0 += last_counter.0 .0;
last_counter.0 .0 = counter.0;
}
let mut world = World::new();
world.insert_resource(Counter(1));
assert_eq!(*world.resource::<Counter>(), Counter(1));
let id = world.register_system(doubling);
let _ = world.run_system(id);
assert_eq!(*world.resource::<Counter>(), Counter(1));
let _ = world.run_system(id);
assert_eq!(*world.resource::<Counter>(), Counter(2));
let _ = world.run_system(id);
assert_eq!(*world.resource::<Counter>(), Counter(4));
let _ = world.run_system(id);
assert_eq!(*world.resource::<Counter>(), Counter(8));
}
#[test]
fn nested_systems() {
use crate::system::SystemId;
#[derive(Component)]
struct Callback(SystemId);
fn nested(query: Query<&Callback>, mut commands: Commands) {
for callback in query.iter() {
commands.run_system(callback.0);
}
}
let mut world = World::new();
world.insert_resource(Counter(0));
let increment_two = world.register_system(|mut counter: ResMut<Counter>| {
counter.0 += 2;
});
let increment_three = world.register_system(|mut counter: ResMut<Counter>| {
counter.0 += 3;
});
let nested_id = world.register_system(nested);
world.spawn(Callback(increment_two));
world.spawn(Callback(increment_three));
let _ = world.run_system(nested_id);
assert_eq!(*world.resource::<Counter>(), Counter(5));
}
}

View file

@ -179,7 +179,7 @@ macro_rules! define_label {
}
$(#[$label_attr])*
pub trait $label_name: 'static {
pub trait $label_name: Send + Sync + 'static {
/// Converts this type into an opaque, strongly-typed label.
fn as_label(&self) -> $id_name {
let id = self.type_id();

View file

@ -223,6 +223,7 @@ Example | Description
[Hierarchy](../examples/ecs/hierarchy.rs) | Creates a hierarchy of parents and children entities
[Iter Combinations](../examples/ecs/iter_combinations.rs) | Shows how to iterate over combinations of query results
[Nondeterministic System Order](../examples/ecs/nondeterministic_system_order.rs) | Systems run in parallel, but their order isn't always deterministic. Here's how to detect and fix this.
[One Shot Systems](../examples/ecs/one_shot_systems.rs) | Shows how to flexibly run systems without scheduling them
[Parallel Query](../examples/ecs/parallel_query.rs) | Illustrates parallel queries with `ParallelIterator`
[Removal Detection](../examples/ecs/removal_detection.rs) | Query for entities that had a specific component removed earlier in the current frame
[Run Conditions](../examples/ecs/run_conditions.rs) | Run systems only when one or multiple conditions are met

View file

@ -0,0 +1,59 @@
//! Demonstrates the use of "one-shot systems", which run once when triggered.
//!
//! These can be useful to help structure your logic in a push-based fashion,
//! reducing the overhead of running extremely rarely run systems
//! and improving schedule flexibility.
//!
//! See the [`World::run_system`](bevy::ecs::World::run_system) or
//! [`World::run_system_once`](bevy::ecs::World::run_system_once) docs for more
//! details.
use bevy::{
ecs::system::{RunSystemOnce, SystemId},
prelude::*,
};
fn main() {
App::new()
.add_systems(Startup, (count_entities, setup))
.add_systems(PostUpdate, count_entities)
.add_systems(Update, evaluate_callbacks)
.run();
}
// Any ordinary system can be run via commands.run_system or world.run_system.
fn count_entities(all_entities: Query<()>) {
dbg!(all_entities.iter().count());
}
#[derive(Component)]
struct Callback(SystemId);
#[derive(Component)]
struct Triggered;
fn setup(world: &mut World) {
let button_pressed_id = world.register_system(button_pressed);
world.spawn((Callback(button_pressed_id), Triggered));
// This entity does not have a Triggered component, so its callback won't run.
let slider_toggled_id = world.register_system(slider_toggled);
world.spawn(Callback(slider_toggled_id));
world.run_system_once(count_entities);
}
fn button_pressed() {
println!("A button was pressed!");
}
fn slider_toggled() {
println!("A slider was toggled!");
}
/// Runs the systems associated with each `Callback` component if the entity also has a Triggered component.
///
/// This could be done in an exclusive system rather than using `Commands` if preferred.
fn evaluate_callbacks(query: Query<&Callback, With<Triggered>>, mut commands: Commands) {
for callback in query.iter() {
commands.run_system(callback.0);
}
}