Allow Commands to register systems (#11019)

# Objective

- Allow registering of systems from Commands with
`Commands::register_one_shot_system`
- Make registering of one shot systems more easy

## Solution

- Add the Command `RegisterSystem` for Commands use.
- Creation of SystemId based on lazy insertion of the System
- Changed the privacy of the fields in SystemId so Commands can return
the SystemId

---

## Changelog

### Added
- Added command `RegisterSystem`
- Added function `Commands::register_one_shot_system`
- Added function `App::register_one_shot_system`

### Changed
- Changed the privacy and the type of struct tuple to regular struct of
SystemId

## Migration Guide

- Changed SystemId fields from tuple struct to a normal struct
If you want to access the entity field, you should use
`SystemId::entity` instead of `SystemId::0`

## Showcase
> Before, if you wanted to register a system with `Commands`, you would
need to do:
```rust
commands.add(|world: &mut World| {
    let id = world.register_system(your_system);
    // You would need to insert the SystemId inside an entity or similar
})
```
> Now, you can:
```rust
let id = commands.register_one_shot_system(your_system);
// Do what you want with the Id

```

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Pablo Reinhardt <pabloreinhardt@gmail.com>
This commit is contained in:
Pablo Reinhardt 2024-03-22 14:31:40 -03:00 committed by GitHub
parent c9ec95d782
commit 78335a5ddc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 136 additions and 21 deletions

View File

@ -6,6 +6,7 @@ use bevy_ecs::{
common_conditions::run_once as run_once_condition, run_enter_schedule,
InternedScheduleLabel, ScheduleBuildSettings, ScheduleLabel,
},
system::SystemId,
};
use bevy_utils::{intern::Interned, tracing::debug, HashMap, HashSet};
use std::{
@ -453,6 +454,22 @@ impl App {
self
}
/// Registers a system and returns a [`SystemId`] so it can later be called by [`World::run_system`].
///
/// It's possible to register the same systems more than once, they'll be stored separately.
///
/// This is different from adding systems to a [`Schedule`] with [`App::add_systems`],
/// because the [`SystemId`] that is returned can be used anywhere in the [`World`] to run the associated system.
/// This allows for running systems in a push-based fashion.
/// Using a [`Schedule`] is still preferred for most cases
/// due to its better performance and ability to run non-conflicting systems simultaneously.
pub fn register_system<I: 'static, O: 'static, M, S: IntoSystem<I, O, M> + 'static>(
&mut self,
system: S,
) -> SystemId<I, O> {
self.world.register_system(system)
}
/// Configures a collection of system sets in the provided schedule, adding any sets that do not exist.
#[track_caller]
pub fn configure_sets(

View File

@ -1,6 +1,6 @@
mod parallel_scope;
use super::{Deferred, Resource};
use super::{Deferred, IntoSystem, RegisterSystem, Resource};
use crate::{
self as bevy_ecs,
bundle::Bundle,
@ -520,6 +520,72 @@ impl<'w, 's> Commands<'w, 's> {
.push(RunSystemWithInput::new_with_input(id, input));
}
/// Registers a system and returns a [`SystemId`] so it can later be called by [`World::run_system`].
///
/// It's possible to register the same systems more than once, they'll be stored separately.
///
/// This is different from adding systems to a [`Schedule`](crate::schedule::Schedule),
/// because the [`SystemId`] that is returned can be used anywhere in the [`World`] to run the associated system.
/// This allows for running systems in a push-based fashion.
/// Using a [`Schedule`](crate::schedule::Schedule) is still preferred for most cases
/// due to its better performance and ability to run non-conflicting systems simultaneously.
///
/// If you want to prevent Commands from registering the same system multiple times, consider using [`Local`](crate::system::Local)
///
/// # Example
///
/// ```
/// # use bevy_ecs::{prelude::*, world::CommandQueue, system::SystemId};
///
/// #[derive(Resource)]
/// struct Counter(i32);
///
/// fn register_system(mut local_system: Local<Option<SystemId>>, mut commands: Commands) {
/// if let Some(system) = *local_system {
/// commands.run_system(system);
/// } else {
/// *local_system = Some(commands.register_one_shot_system(increment_counter));
/// }
/// }
///
/// fn increment_counter(mut value: ResMut<Counter>) {
/// value.0 += 1;
/// }
///
/// # let mut world = World::default();
/// # world.insert_resource(Counter(0));
/// # let mut queue_1 = CommandQueue::default();
/// # let systemid = {
/// # let mut commands = Commands::new(&mut queue_1, &world);
/// # commands.register_one_shot_system(increment_counter)
/// # };
/// # let mut queue_2 = CommandQueue::default();
/// # {
/// # let mut commands = Commands::new(&mut queue_2, &world);
/// # commands.run_system(systemid);
/// # }
/// # queue_1.append(&mut queue_2);
/// # queue_1.apply(&mut world);
/// # assert_eq!(1, world.resource::<Counter>().0);
/// # bevy_ecs::system::assert_is_system(register_system);
/// ```
pub fn register_one_shot_system<
I: 'static + Send,
O: 'static + Send,
M,
S: IntoSystem<I, O, M> + 'static,
>(
&mut self,
system: S,
) -> SystemId<I, O> {
let entity = self.spawn_empty().id();
self.queue.push(RegisterSystem::new(system, entity));
SystemId {
entity,
marker: std::marker::PhantomData,
}
}
/// Pushes a generic [`Command`] to the command queue.
///
/// `command` can be a built-in command, custom struct that implements [`Command`] or a closure
@ -764,13 +830,13 @@ impl EntityCommands<'_> {
/// commands.entity(player.entity)
/// // You can try_insert individual components:
/// .try_insert(Defense(10))
///
///
/// // You can also insert tuples of components:
/// .try_insert(CombatBundle {
/// health: Health(100),
/// strength: Strength(40),
/// });
///
///
/// // Suppose this occurs in a parallel adjacent system or process
/// commands.entity(player.entity)
/// .despawn();

View File

@ -38,9 +38,11 @@ impl<I, O> RemovedSystem<I, O> {
///
/// These are opaque identifiers, keyed to a specific [`World`],
/// and are created via [`World::register_system`].
pub struct SystemId<I = (), O = ()>(Entity, std::marker::PhantomData<fn(I) -> O>);
pub struct SystemId<I = (), O = ()> {
pub(crate) entity: Entity,
pub(crate) marker: std::marker::PhantomData<fn(I) -> O>,
}
// A manual impl is used because the trait bounds should ignore the `I` and `O` phantom parameters.
impl<I, O> Eq for SystemId<I, O> {}
// A manual impl is used because the trait bounds should ignore the `I` and `O` phantom parameters.
@ -56,22 +58,22 @@ impl<I, O> Clone for SystemId<I, O> {
// A manual impl is used because the trait bounds should ignore the `I` and `O` phantom parameters.
impl<I, O> PartialEq for SystemId<I, O> {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0 && self.1 == other.1
self.entity == other.entity && self.marker == other.marker
}
}
// A manual impl is used because the trait bounds should ignore the `I` and `O` phantom parameters.
impl<I, O> std::hash::Hash for SystemId<I, O> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.hash(state);
self.entity.hash(state);
}
}
impl<I, O> std::fmt::Debug for SystemId<I, O> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("SystemId")
.field(&self.0)
.field(&self.1)
.field(&self.entity)
.field(&self.entity)
.finish()
}
}
@ -83,7 +85,7 @@ impl<I, O> From<SystemId<I, O>> for Entity {
/// is really an entity with associated handler function.
///
/// For example, this is useful if you want to assign a name label to a system.
fn from(SystemId(entity, _): SystemId<I, O>) -> Self {
fn from(SystemId { entity, .. }: SystemId<I, O>) -> Self {
entity
}
}
@ -113,14 +115,15 @@ impl World {
&mut self,
system: BoxedSystem<I, O>,
) -> SystemId<I, O> {
SystemId(
self.spawn(RegisteredSystem {
initialized: false,
system,
})
.id(),
std::marker::PhantomData,
)
SystemId {
entity: self
.spawn(RegisteredSystem {
initialized: false,
system,
})
.id(),
marker: std::marker::PhantomData,
}
}
/// Removes a registered system and returns the system, if it exists.
@ -133,7 +136,7 @@ impl World {
&mut self,
id: SystemId<I, O>,
) -> Result<RemovedSystem<I, O>, RegisteredSystemError<I, O>> {
match self.get_entity_mut(id.0) {
match self.get_entity_mut(id.entity) {
Some(mut entity) => {
let registered_system = entity
.take::<RegisteredSystem<I, O>>()
@ -275,7 +278,7 @@ impl World {
) -> Result<O, RegisteredSystemError<I, O>> {
// lookup
let mut entity = self
.get_entity_mut(id.0)
.get_entity_mut(id.entity)
.ok_or(RegisteredSystemError::SystemIdNotRegistered(id))?;
// take ownership of system trait object
@ -294,7 +297,7 @@ impl World {
let result = system.run(input, self);
// return ownership of system trait object (if entity still exists)
if let Some(mut entity) = self.get_entity_mut(id.0) {
if let Some(mut entity) = self.get_entity_mut(id.entity) {
entity.insert::<RegisteredSystem<I, O>>(RegisteredSystem {
initialized,
system,
@ -356,6 +359,35 @@ impl<I: 'static + Send> Command for RunSystemWithInput<I> {
}
}
/// The [`Command`] type for registering one shot systems from [Commands](crate::system::Commands).
///
/// This command needs an already boxed system to register, and an already spawned entity
pub struct RegisterSystem<I: 'static, O: 'static> {
system: BoxedSystem<I, O>,
entity: Entity,
}
impl<I: 'static, O: 'static> RegisterSystem<I, O> {
/// Creates a new [Command] struct, which can be added to [Commands](crate::system::Commands)
pub fn new<M, S: IntoSystem<I, O, M> + 'static>(system: S, entity: Entity) -> Self {
Self {
system: Box::new(IntoSystem::into_system(system)),
entity,
}
}
}
impl<I: 'static + Send, O: 'static + Send> Command for RegisterSystem<I, O> {
fn apply(self, world: &mut World) {
let _ = world.get_entity_mut(self.entity).map(|mut entity| {
entity.insert(RegisteredSystem {
initialized: false,
system: self.system,
});
});
}
}
/// An operation with stored systems failed.
#[derive(Error)]
pub enum RegisteredSystemError<I = (), O = ()> {