Skip to content

Add runtime system insertion #2507

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
141 changes: 141 additions & 0 deletions crates/bevy_ecs/src/schedule/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
use super::{Schedule, StageLabel, SystemDescriptor};
use crate::system::Command;
use crate::world::World;

#[derive(Default)]
pub struct SchedulerCommandQueue {
items: Vec<Box<dyn SchedulerCommand>>,
}

impl SchedulerCommandQueue {
pub fn push<C>(&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<Self>, schedule: &mut Schedule);
}

impl<T> Command for T
where
T: SchedulerCommand,
{
fn write(self, world: &mut World) {
world.scheduler_commands.push(self);
}
}

pub struct InsertSystem<S>
where
S: StageLabel,
{
pub system: SystemDescriptor,
pub stage_label: S,
}

impl<S> SchedulerCommand for InsertSystem<S>
where
S: StageLabel,
{
fn write(self: Box<Self>, 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::<SystemStage>(&"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<TestResource>| {
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::<SystemStage>(&"test").unwrap();
assert_eq!(stage.parallel_systems().len(), 2);

schedule.run_once(&mut world);
assert_eq!(
world.get_resource::<TestResource>(),
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::<SystemStage>(&"test").unwrap();
assert_eq!(stage.parallel_systems().len(), 2);
}
}
6 changes: 6 additions & 0 deletions crates/bevy_ecs/src/schedule/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod commands;
mod executor;
mod executor_parallel;
pub mod graph_utils;
Expand All @@ -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;
Expand Down Expand Up @@ -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 =
Expand All @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions crates/bevy_ecs/src/schedule/system_descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ pub trait IntoSystemDescriptor<Params> {

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)
Expand Down
38 changes: 38 additions & 0 deletions crates/bevy_ecs/src/system/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::{
bundle::Bundle,
component::Component,
entity::{Entities, Entity},
schedule::{InsertSystem, IntoSystemDescriptor, StageLabel},
world::World,
};
use bevy_utils::tracing::debug;
Expand Down Expand Up @@ -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::<SystemStage>(&"some stage").unwrap();
/// assert_eq!(stage.parallel_systems().len(), 2);
/// ```
pub fn insert_system<T, S, Params>(&mut self, system: T, stage_label: S)
where
T: IntoSystemDescriptor<Params>,
S: StageLabel,
{
self.queue.push(InsertSystem {
system: system.into_descriptor(),
stage_label,
});
}

/// Adds a command directly to the command list.
pub fn add<C: Command>(&mut self, command: C) {
self.queue.push(command);
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_ecs/src/world/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::{
},
entity::{Entities, Entity},
query::{FilterFetch, QueryState, WorldQuery},
schedule::SchedulerCommandQueue,
storage::{Column, SparseSet, Storages},
};
use std::{
Expand Down Expand Up @@ -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 {
Expand All @@ -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(),
}
}
}
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
53 changes: 53 additions & 0 deletions examples/ecs/dynamic_schedule.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use bevy::{core::FixedTimestep, prelude::*};
use rand::Rng;

#[derive(Default)]
struct BevyMaterial(Option<Handle<ColorMaterial>>);

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::<BevyMaterial>()
.run();
}

fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut materials: ResMut<Assets<ColorMaterial>>,
mut bevy_material: ResMut<BevyMaterial>,
) {
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<u64>) {
let count = *system_counter;
*system_counter += 1;
let closure = move |mut commands: Commands, bevy_material: Res<BevyMaterial>| {
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");
}