Skip to content

One-shot systems through Commands and Schedules #7707

Closed as not planned
Closed as not planned
@Pascualex

Description

@Pascualex

What problem does this solve or what need does it fill?

I've found other attempts at solving this problem, it's well explained in #4090. Basically I want to be able to use the command pattern with SystemParams.

Commands should also keep their ability to trigger behavior associated with concrete data.

// behavior not associated with concrete command data
commands.add(write_hello_world); // function command
commands.add(WriteHelloWorld); // struct command

// behavior associated with concrete command data
commands.add(WriteNumber(42)); // static function command doesn't work

And IMO the command pattern should also support adding commands to the command queue within commands. This is one of their main advantages compared to events. I believe this is currently completely supported so we just need to keep the behavior as is.

What solution would you like?

In an ideal world you would be able to simply add your SystemParams to the write function.

struct WriteNumber(i32);
impl Command for WriteNumber {
    fn write(self, mut numbers: ResMut<Numbers>) {
        numbers.push(self.0);
    }
}

This is obviously not possible because it doesn't comply with the write signature defined in the Command trait. I'm not sure if it would be possible to do it with some tricks and magic and then register the command so that its SystemState gets cached.

Anyhow, the next best solution I can think off is to allow systems to take a command as an input.

app.register_command(write_number);
struct WriteNumber(i32);
fn write_number(command: In<WriteNumber>, mut numbers: ResMut<Numbers>) {
    numbers.push(command.0);
}

The closest thing I've been able to do to implement this solution is through schedules and resources.

First, the code that supports this hacky solution.

struct SystemCommand<T: 'static + Send + Sync>(T);

impl<T: 'static + Send + Sync> Command for SystemCommand<T> {
    fn write(self, world: &mut World) {
        world.insert_resource(CommandIn(self.0));
        world.run_schedule(CommandScheduleLabel::<T>::default());
        world.remove_resource::<CommandIn<T>>();
    }
}

#[derive(Resource)]
struct CommandIn<T: 'static + Send + Sync>(T);

#[derive(ScheduleLabel)]
struct CommandScheduleLabel<T: 'static + Send + Sync> {
    phantom_data: PhantomData<T>,
}

// Default, Clone, PartialEq, Eq, Hash and Debug implementations for CommandScheduleLabel

trait RegisterCommand {
    fn register_command<T: 'static + Send + Sync, P>(
        &mut self,
        system: impl IntoSystemConfig<P>,
    ) -> &mut Self;
}

impl RegisterCommand for App {
    fn register_command<T: 'static + Send + Sync, P>(
        &mut self,
        system: impl IntoSystemConfig<P>,
    ) -> &mut Self {
        self.init_schedule(CommandScheduleLabel::<T>::default())
            .add_system_to_schedule(CommandScheduleLabel::<T>::default(), system)
    }
}

And a simple example using this solution.

fn main() {
    App::new()
        .init_resource::<Numbers>()
        // ideally: register_command(count_to)
        .register_command::<CountTo, _>(count_to)
        .register_command::<WriteNumber, _>(write_number)
        .add_startup_systems((add_commands, apply_system_buffers, read_numbers).chain())
        .run();
}

#[derive(Resource, Default, Deref, DerefMut)]
struct Numbers(Vec<i32>);

struct CountTo(i32);

struct WriteNumber(i32);

fn add_commands(mut commands: Commands) {
    // ideally: commands.add(CountTo(3));
    commands.add(SystemCommand(CountTo(3)));
    commands.add(SystemCommand(WriteNumber(100)));
    commands.add(SystemCommand(CountTo(2)));
}

// ideally: command: In<CountTo>
fn count_to(command: Res<CommandIn<CountTo>>, mut commands: Commands) {
    let CountTo(number) = command.0;
    for i in 1..=number {
        commands.add(SystemCommand(WriteNumber(i)));
    }
}

fn write_number(command: Res<CommandIn<WriteNumber>>, mut numbers: ResMut<Numbers>) {
    let WriteNumber(number) = command.0;
    numbers.push(number);
}

fn read_numbers(numbers: Res<Numbers>) {
    for number in &**numbers {
        println!("{number}");
    }
}

The output of the program is consistent with the behavior commands should have when inserting other commands.

1 2 3 100 1 2

As you can probably tell, I'm not super familiar with the Bevy internals and my solution is only interacting with its surface. I would love to read your suggestions on how to improve this and your overall feedback on this type of solution.

The most obvious concerns right now are getting the "ideal" syntax working and analyzing the performance impact of running all these different schedules. To avoid regressions we could have ExclusiveCommands to reproduce the current behavior of taking the whole world with no need for an schedule.

What alternative(s) have you considered?

There are many other alternatives, but none of them fit these particular requirements.

I apologize if I've misrepresented any of these solutions out of ignorance.

Additional context

If you want to experiment with this I've set up a small repo you can fork:
https://github.com/pascualex/bevy_system_commands

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-ECSEntities, components, systems, and eventsC-FeatureA new feature, making something new possibleD-ComplexQuite challenging from either a design or technical perspective. Ask for help!X-ControversialThere is active debate or serious implications around merging this PR

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions