diff --git a/Cargo.toml b/Cargo.toml index ac0fd63b8cdd3..ff1b5697a02d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1055,6 +1055,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_app/src/lib.rs b/crates/bevy_app/src/lib.rs index 8348a7c16e15e..6eaaf911fdb0a 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -7,6 +7,7 @@ mod config; mod plugin; mod plugin_group; mod schedule_runner; +mod system_registry; #[cfg(feature = "bevy_ci_testing")] mod ci_testing; diff --git a/crates/bevy_app/src/system_registry.rs b/crates/bevy_app/src/system_registry.rs new file mode 100644 index 0000000000000..83514afa7b706 --- /dev/null +++ b/crates/bevy_app/src/system_registry.rs @@ -0,0 +1,35 @@ +use crate::App; +use bevy_ecs::{ + prelude::*, + system::{SystemId, SystemRegistryError}, +}; + +impl App { + /// Register a system with any number of [`SystemLabel`]s. + /// + /// Calls [`SystemRegistry::register_system`](bevy_ecs::system::SystemRegistry::register_system). + pub fn register_system + 'static>( + &mut self, + system: S, + ) -> &mut Self { + self.world.register_system(system); + self + } + + /// Runs the supplied system on the [`World`] a single time. + /// + /// Calls [`SystemRegistry::run_system`](bevy_ecs::system::SystemRegistry::run_system). + #[inline] + pub fn run_system + 'static>(&mut self, system: S) -> &mut Self { + self.world.run_system(system); + self + } + + /// Run the systems corresponding to the label stored in the provided [`Callback`] + /// + /// Calls [`SystemRegistry::run_callback`](bevy_ecs::system::SystemRegistry::run_callback). + #[inline] + pub fn run_system_by_id(&mut self, system_id: SystemId) -> Result<(), SystemRegistryError> { + self.world.run_system_by_id(system_id) + } +} diff --git a/crates/bevy_ecs/src/schedule/set.rs b/crates/bevy_ecs/src/schedule/set.rs index 23058a3de9fbc..784d52f9bac4f 100644 --- a/crates/bevy_ecs/src/schedule/set.rs +++ b/crates/bevy_ecs/src/schedule/set.rs @@ -98,6 +98,7 @@ impl Hash for SystemTypeSet { // all systems of a given type are the same } } + impl Clone for SystemTypeSet { fn clone(&self) -> Self { Self(PhantomData) diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 73df904177cd1..968d35e61ced6 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::{IntoSystem, RunSystem, RunSystemById, SystemId}, world::{FromWorld, World}, }; use bevy_ecs_macros::SystemParam; @@ -504,6 +505,27 @@ impl<'w, 's> Commands<'w, 's> { }); } + /// Adds a command directly to the command queue. + /// Runs the supplied system on the [`World`] a single time. + /// + /// Calls [`SystemRegistry::run_system`](crate::SystemRegistry::run_system). + pub fn run_system< + M: Send + Sync + 'static, + S: IntoSystem<(), (), M> + Send + Sync + 'static, + >( + &mut self, + system: S, + ) { + self.queue.push(RunSystem::new(system)); + } + + /// Run the systems corresponding to the label stored in the provided [`Callback`] + /// + /// Calls [`SystemRegistry::run_callback`](crate::SystemRegistry::run_callback). + pub fn run_system_by_id(&mut self, system_id: SystemId) { + self.queue.push(RunSystemById::new(system_id)); + } + /// Pushes a generic [`Command`] to the command queue. /// /// `command` can be a built-in command, custom struct that implements [`Command`] or a closure diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index fe6701df605c2..06b3675be436a 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -61,8 +61,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 [`SystemRegistry`](crate::system::SystemRegistry) +/// 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. @@ -78,6 +80,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 [`SystemRegistry`](crate::system::SystemRegistry). +/// /// # Example /// /// Basic usage: diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index 0e88adf24e81d..50a305d24776e 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -108,6 +108,7 @@ mod query; mod system; mod system_param; mod system_piping; +mod system_registry; pub use combinator::*; pub use commands::*; @@ -118,6 +119,7 @@ pub use query::*; pub use system::*; pub use system_param::*; pub use system_piping::*; +pub use system_registry::*; /// Ensure that a given function is a [system](System). /// 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..509f7729610bc --- /dev/null +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -0,0 +1,392 @@ +use std::any::TypeId; +use std::hash::Hash; +use std::marker::PhantomData; + +use crate::system::{BoxedSystem, Command, IntoSystem}; +use crate::world::{Mut, World}; +// Needed for derive(Component) macro +use crate::{self as bevy_ecs, TypeIdMap}; +use bevy_ecs_macros::Resource; + +/// Stores initialized [`System`]s, so they can be reused and run in an ad-hoc fashion. +/// +/// Systems are keyed by their [`SystemSet`]: +/// - all systems within a given set will be run (in linear registration order) when a given set is run +/// - repeated calls with the same function type will reuse cached state, including for change detection +/// +/// Any [`Commands`](crate::system::Commands) generated by these systems (but not other systems), will immediately be applied. +/// +/// This type is stored as a [`Resource`](crate::system::Resource) on each [`World`], initialized by default. +/// However, it will likely be easier to use the corresponding methods on [`World`], +/// to avoid having to worry about split mutable borrows yourself. +/// +/// # Limitations +/// +/// - stored systems cannot be chained: they can neither have an [`In`](crate::system::In) nor return any values +/// - stored systems cannot recurse: they cannot run other systems via the [`SystemRegistry`] methods on `World` or `Commands` +/// - exclusive systems cannot be used +/// +/// # Examples +/// +/// You can run a single system directly on the World, +/// applying its effect and caching its state for the next time +/// you call this method (internally, this is based on [`SystemTypeSet`]). +/// +/// ```rust +/// use bevy_ecs::prelude::*; +/// +/// let mut world = World::new(); +/// +/// #[derive(Default, PartialEq, Debug)] +/// struct Counter(u8); +/// +/// fn count_up(mut counter: ResMut){ +/// counter.0 += 1; +/// } +/// +/// world.init_resource::(); +/// world.run_system(count_up); +/// +/// assert_eq!(Counter(1), *world.resource()); +/// ``` +/// +/// These systems immediately apply commands and cache state, +/// ensuring that change detection and [`Local`](crate::system::Local) variables work correctly. +/// +/// ```rust +/// use bevy_ecs::prelude::*; +/// +/// let mut world = World::new(); +/// +/// #[derive(Component)] +/// struct Marker; +/// +/// fn spawn_7_entities(mut commands: Commands) { +/// for _ in 0..7 { +/// commands.spawn(Marker); +/// } +/// } +/// +/// fn assert_7_spawned(query: Query<(), Added>){ +/// let n_spawned = query.iter().count(); +/// assert_eq!(n_spawned, 7); +/// } +/// +/// world.run_system(spawn_7_entities); +/// world.run_system(assert_7_spawned); +/// ``` +#[derive(Resource, Default)] +pub struct SystemRegistry { + systems: Vec<(bool, BoxedSystem)>, + indices: TypeIdMap, +} + +impl SystemRegistry { + /// Registers a system in the [`SystemRegistry`], so then it can be later run. + /// + /// This allows the system to be run by its [`SystemTypeSet`] using the `run_systems_by_set` method. + /// Repeatedly registering a system will have no effect. + /// + /// When [`run_systems_by_set`](SystemRegistry::run_systems_by_set) is called, + /// all registered systems that match that set will be evaluated (in insertion order). + /// + /// To provide explicit set(s), use [`register_system_with_sets`](SystemRegistry::register_system_with_sets). + #[inline] + pub fn register + 'static>(&mut self, system: S) -> SystemId { + let type_id = TypeId::of::(); + + let index = *self.indices.entry(type_id).or_insert_with(|| { + let index = self.systems.len(); + self.systems + .push((false, Box::new(IntoSystem::into_system(system)))); + index + }); + + SystemId::new(index) + } + + /// Runs the supplied system on the [`World`] a single time. + /// + /// You do not need to register systems before they are run in this way. + /// Instead, systems will be automatically registered according to their [`SystemTypeSet`] the first time this method is called on them. + /// + /// System state will be reused between runs, ensuring that [`Local`](crate::system::Local) variables and change detection works correctly. + /// + /// If, via manual system registration, you have somehow managed to insert more than one system with the same [`SystemTypeSet`], + /// only the first will be run. + pub fn run + 'static>(&mut self, world: &mut World, system: S) { + let id = self.register(system); + self.run_by_id(world, id) + .expect("System was registered before running"); + } + + /// Run the systems corresponding to the set stored in the provided [`Callback`] + /// + /// Systems must be registered before they can be run by their set, + /// including via this method. + /// + /// Systems will be run sequentially in registration order if more than one registered system matches the provided set. + #[inline] + pub fn run_by_id( + &mut self, + world: &mut World, + id: SystemId, + ) -> Result<(), SystemRegistryError> { + match self.systems.get_mut(id.index()) { + Some((initialized, matching_system)) => { + if !*initialized { + matching_system.initialize(world); + *initialized = true; + } + matching_system.run((), world); + matching_system.apply_buffers(world); + Ok(()) + } + None => Err(SystemRegistryError::SystemIdNotRegistered(id)), + } + } +} + +#[derive(Debug, Copy, Clone, Hash, Ord, PartialOrd, Eq, PartialEq)] +pub struct SystemId(usize); + +impl SystemId { + #[inline] + pub const fn new(index: usize) -> SystemId { + SystemId(index) + } + + #[inline] + pub fn index(self) -> usize { + self.0 + } +} + +impl World { + #[inline] + pub fn register_system + 'static>( + &mut self, + system: S, + ) -> SystemId { + self.resource_mut::().register(system) + } + + /// Runs the supplied system on the [`World`] a single time. + /// + /// Calls [`SystemRegistry::run_system`]. + #[inline] + pub fn run_system + 'static>(&mut self, system: S) { + self.resource_scope(|world, mut registry: Mut| { + registry.run(world, system); + }); + } + + /// Run the systems corresponding to the set stored in the provided [`Callback`] + /// + /// Calls [`SystemRegistry::run_callback`]. + #[inline] + pub fn run_system_by_id(&mut self, id: SystemId) -> Result<(), SystemRegistryError> { + self.resource_scope(|world, mut registry: Mut| { + registry.run_by_id(world, id) + }) + } +} + +/// The [`Command`] type for [`SystemRegistry::run_system`] +#[derive(Debug, Clone)] +pub struct RunSystem + Send + Sync + 'static> { + _phantom_marker: PhantomData, + system: S, +} + +impl + Send + Sync + 'static> RunSystem { + /// Creates a new [`Command`] struct, which can be added to [`Commands`](crate::system::Commands) + #[inline] + #[must_use] + pub fn new(system: S) -> Self { + Self { + _phantom_marker: PhantomData::default(), + system, + } + } +} + +impl + Send + Sync + 'static> Command + for RunSystem +{ + #[inline] + fn write(self, world: &mut World) { + world.run_system(self.system); + } +} + +/// The [`Command`] type for [`SystemRegistry::run_systems_by_set`] +#[derive(Debug, Clone)] +pub struct RunSystemById { + pub system_id: SystemId, +} + +impl RunSystemById { + pub fn new(system_id: SystemId) -> Self { + Self { system_id } + } +} + +impl Command for RunSystemById { + #[inline] + fn write(self, world: &mut World) { + world.resource_scope(|world, mut registry: Mut| { + registry + .run_by_id(world, self.system_id) + // Ideally this error should be handled more gracefully, + // but that's blocked on a full error handling solution for commands + .unwrap(); + }); + } +} + +/// An operation on a [`SystemRegistry`] failed +#[derive(Debug)] +pub enum SystemRegistryError { + /// A system was run by set, but no system with that set was found. + /// + /// Did you forget to register it? + SystemIdNotRegistered(SystemId), +} + +mod tests { + use crate as bevy_ecs; + use crate::prelude::*; + + #[derive(SystemSet, Hash, Debug, Eq, PartialEq, Clone)] + struct CountSet; + + #[derive(Resource, Default, PartialEq, Debug)] + struct Counter(u8); + + #[allow(dead_code)] + fn count_up(mut counter: ResMut) { + counter.0 += 1; + } + + #[test] + fn run_system() { + let mut world = World::new(); + world.init_resource::(); + assert_eq!(*world.resource::(), Counter(0)); + world.run_system(count_up); + assert_eq!(*world.resource::(), Counter(1)); + } + + #[test] + /// We need to ensure that the system registry is accessible + /// even after being used once. + fn run_two_systems() { + let mut world = World::new(); + world.init_resource::(); + assert_eq!(*world.resource::(), Counter(0)); + world.run_system(count_up); + assert_eq!(*world.resource::(), Counter(1)); + world.run_system(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(); + world.init_resource::(); + assert_eq!(world.entities.len(), 0); + world.run_system(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(non_send_count_down); + assert_eq!(*world.non_send_resource::(), Counter(9)); + } + + #[test] + fn change_detection() { + #[derive(Resource, Default)] + struct ChangeDetector; + + #[allow(dead_code)] + 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. + world.run_system(count_up_iff_changed); + assert_eq!(*world.resource::(), Counter(1)); + // Nothing changed + world.run_system(count_up_iff_changed); + assert_eq!(*world.resource::(), Counter(1)); + // Making a change + world.resource_mut::().set_changed(); + world.run_system(count_up_iff_changed); + assert_eq!(*world.resource::(), Counter(2)); + } + + #[test] + fn local_variables() { + // The `Local` begins at the default value of 0 + fn doubling(mut 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)); + world.run_system(doubling); + assert_eq!(*world.resource::(), Counter(1)); + world.run_system(doubling); + assert_eq!(*world.resource::(), Counter(2)); + world.run_system(doubling); + assert_eq!(*world.resource::(), Counter(4)); + world.run_system(doubling); + assert_eq!(*world.resource::(), Counter(8)); + } + + #[test] + // This is a known limitation; + // if this test passes the docs must be updated + // to reflect the ability to chain run_system commands + #[should_panic] + fn system_recursion() { + fn count_to_ten(mut counter: ResMut, mut commands: Commands) { + counter.0 += 1; + if counter.0 < 10 { + commands.run_system(count_to_ten); + } + } + + let mut world = World::new(); + world.init_resource::(); + assert_eq!(*world.resource::(), Counter(0)); + world.run_system(count_to_ten); + assert_eq!(*world.resource::(), Counter(10)); + } +} diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 0f78884e16944..18e309a814f28 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -19,7 +19,7 @@ use crate::{ removal_detection::RemovedComponentEvents, schedule::{Schedule, ScheduleLabel, Schedules}, storage::{ResourceData, Storages}, - system::Resource, + system::{Resource, SystemRegistry}, }; use bevy_ptr::{OwningPtr, Ptr}; use bevy_utils::tracing::warn; @@ -70,7 +70,7 @@ pub struct World { impl Default for World { fn default() -> Self { - Self { + let mut world = Self { id: WorldId::new().expect("More `bevy` `World`s have been created than is supported"), entities: Entities::new(), components: Default::default(), @@ -84,7 +84,10 @@ impl Default for World { change_tick: AtomicU32::new(1), last_change_tick: 0, last_check_tick: 0, - } + }; + // This resource is required by bevy_ecs itself, so cannot be included in a plugin + world.init_resource::(); + world } } diff --git a/crates/bevy_utils/src/label.rs b/crates/bevy_utils/src/label.rs index 631ef7a426245..62887bde5cb54 100644 --- a/crates/bevy_utils/src/label.rs +++ b/crates/bevy_utils/src/label.rs @@ -142,7 +142,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 bc03d2382c372..92b56e19c55d1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -208,6 +208,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 paralell, but their order isn't always deteriministic. 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..65297617559a4 --- /dev/null +++ b/examples/ecs/one_shot_systems.rs @@ -0,0 +1,73 @@ +//! 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 [`SystemRegistry`](bevy::ecs::SystemRegistry) docs for more details. + +use bevy::{ + ecs::system::{SystemId, SystemRegistry}, + prelude::*, +}; + +fn main() { + App::new() + .add_startup_system(count_entities) + .add_startup_system(setup) + // One shot systems are interchangeable with ordinarily scheduled systems. + // Change detection, Local and NonSend all work as expected. + .add_system(count_entities.in_base_set(CoreSet::PostUpdate)) + // One-shot systems can be used to build complex abstractions + // to match the needs of your design. + // Here, we model a very simple component-linked callback architecture. + .add_system(evaluate_callbacks) + .run(); +} + +// Any ordinary system can be run via commands.run_system or world.run_system. +// +// Chained systems, exclusive systems and systems which themselves run systems cannot be called in this way. +fn count_entities(all_entities: Query<()>) { + dbg!(all_entities.iter().count()); +} + +#[derive(Component)] +struct Callback(SystemId); + +#[derive(Component)] +struct Triggered; + +fn setup(mut system_registry: ResMut, mut commands: Commands) { + commands.spawn(( + // The Callback component is defined in bevy_ecs, + // but wrapping this (or making your own customized variant) is easy. + // Just stored a boxed SystemLabel! + Callback(system_registry.register(button_pressed)), + Triggered, + )); + // This entity does not have a Triggered component, so its callback won't run. + commands.spawn(Callback(system_registry.register(slider_toggled))); + commands.run_system(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() { + // Because we don't have access to the type information of the callbacks + // we have to use the layer of indirection provided by system labels. + // Note that if we had registered multiple systems with the same label, + // they would all be evaluated here. + commands.run_system_by_id(callback.0); + } +}