Description
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 SystemParam
s.
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 SystemParam
s 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 ExclusiveCommand
s 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.
- Some have been discarded because the systems aren't cached:
- Fast and correct one-shot systems with a system registry resource #4090 could replace schedules but doesn't seem to be concerned with command data.
- Event Stages #1041 is built over events and stages. So it's outdated and wouldn't support events generating other events in a single update.
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