bevyengine / bevy

A refreshingly simple data-driven game engine built in Rust
https://bevyengine.org
Apache License 2.0
36.38k stars 3.59k forks source link

Make `SystemId`s easier to create and manage using `SystemHandle`s #10426

Open asafigan opened 1 year ago

asafigan commented 1 year ago

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

Currently it isn't simple to create and manage SystemIds. For them to be widely used, there needs to be a simple way to work with them.

Reasons they are hard to create:

Reasons they are hard to manage:

What solution would you like?

Manage SystemId lifetimes using a new type. I propose SystemHandle. It would act like an asset Handle where it keeps track of references and removes the system when it is no longer being used.

Also an easy way to create SystemHandles. I would suggest a new type that acts like one of AssetServer, Assets, or Commands.

In the future SystemHandle could be used by scenes to reference systems. Maybe the TypeId of the system could be used in a SystemPath. This would require the systems to be registered ahead of time. This can be done in Plugins like so: app.world.register_system(system).

What alternative(s) have you considered?

We could add a methods to Commands and ChildBuilder for creating handles instead of creating a new type to create SystemHandles. This has the drawback that you can't inline creating a SystemHandle in the arguments of spawn.

Is it possible to use the asset system for this and just use Handle<SystemId>? Would this be a good idea even if it were possible?

Additional context

I quickly wrote a proof of concept plugin:

Plugin ```rust use crossbeam_channel::{Receiver, Sender}; use std::sync::{Arc, OnceLock}; use bevy::{ ecs::system::{CommandQueue, SystemId, SystemParam}, prelude::*, }; pub struct OneShotSystemsPlugin; impl Plugin for OneShotSystemsPlugin { fn build(&self, app: &mut App) { let (drop_sender, drop_receiver) = crossbeam_channel::unbounded(); app.insert_resource(SystemHandleProvider { drop_sender, drop_receiver, }) .add_systems(Last, clean_up_system_ids); } } #[derive(SystemParam)] pub struct Systems<'w, 's> { queue: Deferred<'s, CommandQueue>, systems_server: Res<'w, SystemHandleProvider>, } #[derive(Resource)] struct SystemHandleProvider { drop_sender: Sender, drop_receiver: Receiver, } impl<'w, 's> Systems<'w, 's> { pub fn add + Send + 'static>(&mut self, system: S) -> SystemHandle { let handle = SystemHandle::new(self.systems_server.drop_sender.clone()); let clone = handle.inner.clone(); self.queue.push(move |world: &mut World| { clone.id.set(world.register_system(system)).unwrap(); }); handle } } #[derive(Clone)] pub struct SystemHandle { inner: StrongHandle, } impl SystemHandle { fn new(drop_sender: Sender) -> Self { SystemHandle { inner: StrongHandle { id: default(), drop_sender, }, } } } #[derive(Clone)] struct StrongHandle { id: Arc>, drop_sender: Sender, } impl Drop for StrongHandle { fn drop(&mut self) { let _ = self.drop_sender.send(DropEvent { id: self.id.clone(), }); } } struct DropEvent { id: Arc>, } impl SystemHandle { pub fn id(&self) -> Option { self.inner.id.get().cloned() } } fn clean_up_system_ids(world: &mut World) { let provider = world.resource::(); let ids: Vec<_> = provider .drop_receiver .try_iter() .filter_map(|e| Arc::into_inner(e.id)) .filter_map(|id| id.into_inner()) .collect(); for id in ids { world.remove_system(id).unwrap(); } } ```

Here is an example of how I use it to spawn a button:

Example usage ```rust // system for setting up a main menu fn setup_main_menu(mut commands: Commands, mut systems: Systems, asset_server: Res) { commands .spawn( // center button NodeBundle { style: Style { width: Val::Percent(100.0), height: Val::Percent(100.0), justify_content: JustifyContent::Center, align_items: AlignItems::Center, ..default() }, ..default() } ) .with_children(|builder| { spawn_button( builder, "Start", // use system the transitions to a new state systems.add(|mut next_state: ResMut>| { next_state.set(GameStates::InGame); }), ); }); } // helper function for spawning a button fn spawn_button( commands: &mut ChildBuilder, text: impl Into, on_press_down: SystemHandle, ) { commands .spawn(( ButtonBundle { style: Style { align_items: AlignItems::Center, justify_items: JustifyItems::Center, align_content: AlignContent::Center, justify_content: JustifyContent::Center, padding: UiRect::all(Val::Px(10.0)), ..default() }, background_color: Color::BLACK, ..default() }, OnPressDown(on_press_down), )) .with_children(|builder| { builder.spawn(TextBundle { text: Text { sections: vec![TextSection::new( text, TextStyle { font_size: 20.0, color: Color::WHITE, ..default() }, )], alignment: TextAlignment::Center, ..default() }, ..default() }); }); } #[derive(Event)] pub struct PressDown { pub entity: Entity, } // system to send event when button is pressed fn ui_press_down( interactions: Query<(Entity, &Interaction), Changed>, mut events: EventWriter, ) { events.send_batch( interactions .iter() .filter(|(_, x)| **x == Interaction::Pressed) .map(|(entity, _)| PressDown { entity }), ); } #[derive(Component)] pub struct OnPressDown(pub SystemHandle); // system to trigger systems when `PressDown` events are fired fn handle_on_press_down( mut events: EventReader, on_pressed: Query<&OnPressDown>, mut commands: Commands, ) { for on_pressed in on_pressed.iter_many(events.read().map(|x| x.entity)) { if let Some(id) = on_pressed.0.id() { commands.run_system(id); } } } ```
alice-i-cecile commented 1 year ago

Related to #10382.

asafigan commented 1 year ago

Probably should favor not using SystemIds at all. For further information see my comment in #10582.