diff --git a/Cargo.toml b/Cargo.toml index 7feb450cc82be..6c635312bb456 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -317,6 +317,10 @@ path = "examples/ecs/timers.rs" name = "query_bundle" path = "examples/ecs/query_bundle.rs" +[[example]] +name = "dynamic_schedule" +path = "examples/ecs/dynamic_schedule.rs" + # Games [[example]] name = "alien_cake_addict" diff --git a/crates/bevy_ecs/src/schedule/commands.rs b/crates/bevy_ecs/src/schedule/commands.rs new file mode 100644 index 0000000000000..29d574d972eb5 --- /dev/null +++ b/crates/bevy_ecs/src/schedule/commands.rs @@ -0,0 +1,141 @@ +use super::{Schedule, StageLabel, SystemDescriptor}; +use crate::system::Command; +use crate::world::World; + +#[derive(Default)] +pub struct SchedulerCommandQueue { + items: Vec>, +} + +impl SchedulerCommandQueue { + pub fn push(&mut self, command: C) + where + C: SchedulerCommand, + { + self.items.push(Box::new(command)); + } + + pub fn apply(&mut self, schedule: &mut Schedule, offset: usize) { + for command in self.items.drain(offset..) { + command.write(schedule); + } + } + + pub fn len(&self) -> usize { + self.items.len() + } + + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } +} + +/// A [`Schedule`] mutation. +pub trait SchedulerCommand: Send + Sync + 'static { + fn write(self: Box, schedule: &mut Schedule); +} + +impl Command for T +where + T: SchedulerCommand, +{ + fn write(self, world: &mut World) { + world.scheduler_commands.push(self); + } +} + +pub struct InsertSystem +where + S: StageLabel, +{ + pub system: SystemDescriptor, + pub stage_label: S, +} + +impl SchedulerCommand for InsertSystem +where + S: StageLabel, +{ + fn write(self: Box, schedule: &mut Schedule) { + schedule.add_system_to_stage(self.stage_label, self.system); + } +} + +#[cfg(test)] +mod tests { + use crate::{ + prelude::ResMut, + schedule::{ + InsertSystem, IntoSystemDescriptor, Schedule, SchedulerCommandQueue, SystemStage, + }, + system::Commands, + world::World, + }; + + #[test] + fn insert_system() { + fn sample_system(mut _commands: Commands) {} + let mut schedule = Schedule::default(); + schedule.add_stage("test", SystemStage::parallel()); + let mut queue = SchedulerCommandQueue::default(); + queue.push(InsertSystem { + system: sample_system.into_descriptor(), + stage_label: "test", + }); + queue.apply(&mut schedule, 0); + + let stage = schedule.get_stage::(&"test").unwrap(); + assert_eq!(stage.parallel_systems().len(), 1); + } + + #[derive(Debug, Default, PartialEq)] + struct TestResource(Option<()>); + + #[test] + fn insert_system_from_system() { + fn sample_system(mut commands: Commands) { + commands.insert_system( + |mut res: ResMut| { + res.0 = Some(()); + }, + "test", + ); + } + + let mut world = World::default(); + world.insert_resource(TestResource(None)); + + let mut schedule = Schedule::default(); + schedule.add_stage("test", SystemStage::parallel()); + schedule.add_system_to_stage("test", sample_system); + schedule.run_once(&mut world); + + let stage = schedule.get_stage::(&"test").unwrap(); + assert_eq!(stage.parallel_systems().len(), 2); + + schedule.run_once(&mut world); + assert_eq!( + world.get_resource::(), + Some(&TestResource(Some(()))) + ); + } + + #[test] + fn insert_with_nested_schedule() { + fn sample_system(mut commands: Commands) { + commands.insert_system(|| {}, "test"); + } + + let mut world = World::default(); + + let nested_schedule = Schedule::default(); + let mut schedule = Schedule::default(); + schedule.add_stage("test", SystemStage::parallel()); + schedule.add_system_to_stage("test", sample_system); + schedule.add_stage("nested", nested_schedule); + schedule.run_once(&mut world); + + let stage = schedule.get_stage::(&"test").unwrap(); + assert_eq!(stage.parallel_systems().len(), 2); + } +} diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index 67f21025cf0bf..739eb170f2d27 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -1,3 +1,4 @@ +mod commands; mod executor; mod executor_parallel; pub mod graph_utils; @@ -9,6 +10,7 @@ mod system_container; mod system_descriptor; mod system_set; +pub use commands::*; pub use executor::*; pub use executor_parallel::*; pub use graph_utils::GraphNode; @@ -198,6 +200,8 @@ impl Schedule { } pub fn run_once(&mut self, world: &mut World) { + let existing_commands = world.scheduler_commands.len(); + for label in self.stage_order.iter() { #[cfg(feature = "trace")] let stage_span = @@ -207,6 +211,8 @@ impl Schedule { let stage = self.stages.get_mut(label).unwrap(); stage.run(world); } + + world.scheduler_commands.apply(self, existing_commands); } /// Iterates over all of schedule's stages and their labels, in execution order. diff --git a/crates/bevy_ecs/src/schedule/system_descriptor.rs b/crates/bevy_ecs/src/schedule/system_descriptor.rs index b75fbcb89f3db..f20e57d4908df 100644 --- a/crates/bevy_ecs/src/schedule/system_descriptor.rs +++ b/crates/bevy_ecs/src/schedule/system_descriptor.rs @@ -45,6 +45,12 @@ pub trait IntoSystemDescriptor { pub struct SystemLabelMarker; +impl IntoSystemDescriptor<()> for SystemDescriptor { + fn into_descriptor(self) -> SystemDescriptor { + self + } +} + impl IntoSystemDescriptor<()> for ParallelSystemDescriptor { fn into_descriptor(self) -> SystemDescriptor { SystemDescriptor::Parallel(self) diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 0488b09e4f770..711e2578d0ed6 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -4,6 +4,7 @@ use crate::{ bundle::Bundle, component::Component, entity::{Entities, Entity}, + schedule::{InsertSystem, IntoSystemDescriptor, StageLabel}, world::World, }; use bevy_utils::tracing::debug; @@ -152,6 +153,43 @@ impl<'a> Commands<'a> { }); } + /// Inserts a [`crate::system::System`] into a [`crate::schedule::Stage`] + /// + /// # Example + /// + /// ``` + /// use bevy_ecs::prelude::*; + /// + /// // A system like any other + /// fn another_system(mut commands: Commands) { + /// let entity = commands.spawn().id(); + /// } + /// + /// fn example_system(mut commands: Commands) { + /// commands.insert_system(another_system, "some stage"); + /// } + /// + /// let mut world = World::default(); + /// let mut schedule = Schedule::default(); + /// schedule.add_stage("some stage", SystemStage::parallel()); + /// schedule.add_system_to_stage("some stage", example_system); + /// // When we run the schedule + /// schedule.run_once(&mut world); + /// // We should now have 2 systems in "test", the initial system and the inserted system + /// let stage = schedule.get_stage::(&"some stage").unwrap(); + /// assert_eq!(stage.parallel_systems().len(), 2); + /// ``` + pub fn insert_system(&mut self, system: T, stage_label: S) + where + T: IntoSystemDescriptor, + S: StageLabel, + { + self.queue.push(InsertSystem { + system: system.into_descriptor(), + stage_label, + }); + } + /// Adds a command directly to the command list. pub fn add(&mut self, command: C) { self.queue.push(command); diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 5068fdc4e5074..c0a579a854ae4 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -18,6 +18,7 @@ use crate::{ }, entity::{Entities, Entity}, query::{FilterFetch, QueryState, WorldQuery}, + schedule::SchedulerCommandQueue, storage::{Column, SparseSet, Storages}, }; use std::{ @@ -53,6 +54,7 @@ pub struct World { main_thread_validator: MainThreadValidator, pub(crate) change_tick: AtomicU32, pub(crate) last_change_tick: u32, + pub(crate) scheduler_commands: SchedulerCommandQueue, } impl Default for World { @@ -71,6 +73,7 @@ impl Default for World { // are detected on first system runs and for direct world queries. change_tick: AtomicU32::new(1), last_change_tick: 0, + scheduler_commands: Default::default(), } } } diff --git a/examples/README.md b/examples/README.md index 37b76a3a34830..222073d3644f7 100644 --- a/examples/README.md +++ b/examples/README.md @@ -170,6 +170,7 @@ Example | File | Description `system_param` | [`ecs/system_param.rs`](./ecs/system_param.rs) | Illustrates creating custom system parameters with `SystemParam` `system_sets` | [`ecs/system_sets.rs`](./ecs/system_sets.rs) | Shows `SystemSet` use along with run criterion `timers` | [`ecs/timers.rs`](./ecs/timers.rs) | Illustrates ticking `Timer` resources inside systems and handling their state +`dynamic_schedule` | [`ecs/dynamic_schedule.rs`](./ecs/dynamic_schedule.rs) | Shows how to modify the schedule from systems ## Games diff --git a/examples/ecs/dynamic_schedule.rs b/examples/ecs/dynamic_schedule.rs new file mode 100644 index 0000000000000..732cfe853135a --- /dev/null +++ b/examples/ecs/dynamic_schedule.rs @@ -0,0 +1,53 @@ +use bevy::{core::FixedTimestep, prelude::*}; +use rand::Rng; + +#[derive(Default)] +struct BevyMaterial(Option>); + +fn main() { + App::build() + .add_plugins(DefaultPlugins) + .add_stage_after( + CoreStage::Update, + "slow", + SystemStage::parallel().with_run_criteria(FixedTimestep::step(1.0)), + ) + .add_startup_system(setup) + .add_system(dynamic.with_run_criteria(FixedTimestep::step(1.0))) + .init_resource::() + .run(); +} + +fn setup( + mut commands: Commands, + asset_server: Res, + mut materials: ResMut>, + mut bevy_material: ResMut, +) { + commands.spawn_bundle(OrthographicCameraBundle::new_2d()); + + let texture_handle = asset_server.load("branding/icon.png"); + let material = materials.add(texture_handle.into()); + bevy_material.0 = Some(material); +} + +fn dynamic(mut commands: Commands, mut system_counter: Local) { + let count = *system_counter; + *system_counter += 1; + let closure = move |mut commands: Commands, bevy_material: Res| { + info!("Hello from system {}", count); + + let mut rng = rand::thread_rng(); + + commands.spawn_bundle(SpriteBundle { + material: bevy_material.0.clone().unwrap(), + transform: Transform::from_xyz( + rng.gen_range(-400f32..400f32), + rng.gen_range(-400f32..400f32), + 0.0, + ), + ..Default::default() + }); + }; + commands.insert_system(closure, "slow"); +}