From 296eb9057d1e52e80b538530235713fdfb72d9d0 Mon Sep 17 00:00:00 2001 From: Trashtalk217 Date: Tue, 19 Sep 2023 22:17:05 +0200 Subject: [PATCH] One Shot Systems (#8963) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. - 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 Co-authored-by: Federico Rinaldi Co-authored-by: MinerSebas <66798382+MinerSebas@users.noreply.github.com> Co-authored-by: Aevyrie Co-authored-by: Alejandro Pascual Pozo Co-authored-by: Rob Parrett Co-authored-by: François Co-authored-by: Dmytro Banin Co-authored-by: James Liu --- Cargo.toml | 10 + crates/bevy_ecs/src/schedule/set.rs | 1 + crates/bevy_ecs/src/system/commands/mod.rs | 11 +- crates/bevy_ecs/src/system/function_system.rs | 6 +- crates/bevy_ecs/src/system/mod.rs | 2 + crates/bevy_ecs/src/system/system.rs | 98 +++++- crates/bevy_ecs/src/system/system_registry.rs | 301 ++++++++++++++++++ crates/bevy_utils/src/label.rs | 2 +- examples/README.md | 1 + examples/ecs/one_shot_systems.rs | 59 ++++ 10 files changed, 474 insertions(+), 17 deletions(-) create mode 100644 crates/bevy_ecs/src/system/system_registry.rs create mode 100644 examples/ecs/one_shot_systems.rs diff --git a/Cargo.toml b/Cargo.toml index b6967a880d1e6..d06f251e497ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/bevy_ecs/src/schedule/set.rs b/crates/bevy_ecs/src/schedule/set.rs index aa7ef806155f8..769507ff48835 100644 --- a/crates/bevy_ecs/src/schedule/set.rs +++ b/crates/bevy_ecs/src/schedule/set.rs @@ -81,6 +81,7 @@ impl Hash for SystemTypeSet { // all systems of a given type are the same } } + impl Clone for SystemTypeSet { fn clone(&self) -> Self { *self diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 8eb1881273f1b..512dadff88017 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -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::::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 /// /// ``` diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 2ef46c971863a..aa11b1a03f3cb 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -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: diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index e07445aa6500c..ac73d676c9d6e 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -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; diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index a4e1b0625efa1..0c8c839f95e42 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -165,6 +165,30 @@ impl Debug for dyn System { /// 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.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 Debug for dyn System { /// 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 Debug for dyn System { /// 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 Debug for dyn System { /// 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 Debug for dyn System { /// /// ``` /// # use bevy_ecs::prelude::*; -/// # use bevy_ecs::system::RunSystem; +/// # use bevy_ecs::system::RunSystemOnce; /// /// #[derive(Component)] /// struct T(usize); @@ -226,26 +250,26 @@ impl Debug for dyn System { /// 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, Out, Marker>(self, system: T) -> Out { - self.run_system_with((), system) + fn run_system_once, 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, In, Out, Marker>( + fn run_system_once_with, In, Out, Marker>( self, input: In, system: T, ) -> Out; } -impl RunSystem for &mut World { - fn run_system_with, In, Out, Marker>( +impl RunSystemOnce for &mut World { + fn run_system_once_with, 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::().0, 1); } + + #[derive(Resource, Default, PartialEq, Debug)] + struct Counter(u8); + + #[allow(dead_code)] + fn count_up(mut counter: ResMut) { + counter.0 += 1; + } + + #[test] + fn run_two_systems() { + let mut world = World::new(); + world.init_resource::(); + assert_eq!(*world.resource::(), Counter(0)); + world.run_system_once(count_up); + assert_eq!(*world.resource::(), Counter(1)); + world.run_system_once(count_up); + assert_eq!(*world.resource::(), 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) { + ns.0 -= 1; + } + + let mut world = World::new(); + world.insert_non_send_resource(Counter(10)); + assert_eq!(*world.non_send_resource::(), Counter(10)); + world.run_system_once(non_send_count_down); + assert_eq!(*world.non_send_resource::(), Counter(9)); + } } diff --git a/crates/bevy_ecs/src/system/system_registry.rs b/crates/bevy_ecs/src/system/system_registry.rs new file mode 100644 index 0000000000000..f0522606d978f --- /dev/null +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -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 + '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 { + match self.get_entity_mut(id.0) { + Some(mut entity) => { + let registered_system = entity + .take::() + .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.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::(); + /// let detector = world.register_system(|change_detector: ResMut| { + /// 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::().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::() + .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 { + 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, + change_detector: ResMut, + ) { + if change_detector.is_changed() { + counter.0 += 1; + } + } + + let mut world = World::new(); + world.init_resource::(); + world.init_resource::(); + assert_eq!(*world.resource::(), 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(1)); + // Nothing changed + let _ = world.run_system(id); + assert_eq!(*world.resource::(), Counter(1)); + // Making a change + world.resource_mut::().set_changed(); + let _ = world.run_system(id); + assert_eq!(*world.resource::(), Counter(2)); + } + + #[test] + fn local_variables() { + // The `Local` begins at the default value of 0 + fn doubling(last_counter: Local, mut counter: ResMut) { + 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(1)); + let id = world.register_system(doubling); + let _ = world.run_system(id); + assert_eq!(*world.resource::(), Counter(1)); + let _ = world.run_system(id); + assert_eq!(*world.resource::(), Counter(2)); + let _ = world.run_system(id); + assert_eq!(*world.resource::(), Counter(4)); + let _ = world.run_system(id); + assert_eq!(*world.resource::(), 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.0 += 2; + }); + let increment_three = world.register_system(|mut counter: ResMut| { + 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(5)); + } +} diff --git a/crates/bevy_utils/src/label.rs b/crates/bevy_utils/src/label.rs index 29b2d1b5dee37..019965804e58f 100644 --- a/crates/bevy_utils/src/label.rs +++ b/crates/bevy_utils/src/label.rs @@ -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(); diff --git a/examples/README.md b/examples/README.md index d345cd5707edd..ed3229fe4a50d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 diff --git a/examples/ecs/one_shot_systems.rs b/examples/ecs/one_shot_systems.rs new file mode 100644 index 0000000000000..f7f76d4fa7a98 --- /dev/null +++ b/examples/ecs/one_shot_systems.rs @@ -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>, mut commands: Commands) { + for callback in query.iter() { + commands.run_system(callback.0); + } +}