diff --git a/Cargo.toml b/Cargo.toml index cc9c26056daa9..ab846aa70be4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1102,6 +1102,17 @@ description = "Demonstrates loading a compressed asset" category = "Assets" wasm = false +[[example]] +name = "callbacks" +path = "examples/asset/callbacks.rs" +doc-scrape-examples = true + +[package.metadata.example.callbacks] +name = "Callbacks" +description = "Demonstrates using Callbacks" +category = "Assets" +wasm = true + [[example]] name = "custom_asset" path = "examples/asset/custom_asset.rs" diff --git a/crates/bevy_asset/src/callback.rs b/crates/bevy_asset/src/callback.rs new file mode 100644 index 0000000000000..0164ecc8c38b0 --- /dev/null +++ b/crates/bevy_asset/src/callback.rs @@ -0,0 +1,379 @@ +use bevy_ecs::{prelude::*, system::Command}; +use bevy_reflect::prelude::*; +use bevy_utils::tracing; +use thiserror::Error; + +use crate::{Asset, Assets, Handle, VisitAssetDependencies}; + +impl VisitAssetDependencies for Callback { + fn visit_dependencies(&self, _visit: &mut impl FnMut(crate::UntypedAssetId)) { + // TODO: Would there be a way to get this info from the IntoSystem used to contruct the callback? + // TODO: Should there be a way to pass this info through a different construct function? + } +} + +impl Asset for Callback {} + +// TODO: Add docs +#[derive(Error, PartialEq, Eq)] +pub enum CallbackError { + // TODO: Add docs + #[error("Callback {0:?} was not found")] + HandleNotFound(Handle>), + // TODO: Add docs + #[error("Callback {0:?} tried to run itself recursively")] + Recursive(Handle>), +} + +impl std::fmt::Debug for CallbackError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::HandleNotFound(arg0) => f.debug_tuple("HandleNotFound").field(arg0).finish(), + Self::Recursive(arg0) => f.debug_tuple("Recursive").field(arg0).finish(), + } + } +} + +// TODO: Add docs +pub trait RunCallbackWorld { + // TODO: Add docs + fn run_callback_with_input( + &mut self, + handle: Handle>, + input: In, + ) -> Result>; + + // TODO: Add docs + fn run_callback( + &mut self, + handle: Handle>, + ) -> Result> { + self.run_callback_with_input(handle, ()) + } +} + +impl RunCallbackWorld for World { + fn run_callback_with_input( + &mut self, + handle: Handle>, + input: In, + ) -> Result> { + let mut assets = self.resource_mut::>>(); + let mut callback = assets + .remove_untracked(&handle) + .ok_or_else(|| CallbackError::HandleNotFound(handle.clone()))?; + + let result = callback.run_with_input(self, input); + let mut assets = self.resource_mut::>>(); + assets.insert(&handle, callback); + + Ok(result) + } +} + +// TODO: add docs +#[derive(Debug, Clone)] +pub struct RunCallbackWithInput { + handle: Handle>, + input: I, +} + +// TODO: add docs +pub type RunCallback = RunCallbackWithInput<()>; + +impl RunCallback { + // TODO: add docs + pub fn new(handle: Handle) -> Self { + Self::new_with_input(handle, ()) + } +} + +impl RunCallbackWithInput { + // TODO: add docs + pub fn new_with_input(handle: Handle>, input: I) -> Self { + Self { handle, input } + } +} + +impl Command for RunCallbackWithInput { + #[inline] + fn apply(self, world: &mut World) { + if let Err(error) = world.run_callback_with_input(self.handle, self.input) { + tracing::error!("{error}"); + } + } +} + +// TODO: Add docs +pub trait RunCallbackCommands { + // TODO: Add docs + fn run_callback_with_input( + &mut self, + handle: Handle>, + input: I, + ); + + // TODO: Add docs + fn run_callback(&mut self, handle: Handle) { + self.run_callback_with_input(handle, ()); + } +} + +impl<'w, 's> RunCallbackCommands for Commands<'w, 's> { + fn run_callback_with_input( + &mut self, + handle: Handle>, + input: I, + ) { + self.add(RunCallbackWithInput::new_with_input(handle, input)); + } +} + +#[cfg(test)] +mod tests { + use crate::*; + use bevy_ecs::prelude::*; + + #[derive(Resource, Default, PartialEq, Debug)] + struct Counter(u8); + + #[test] + fn change_detection() { + #[derive(Resource, Default)] + struct ChangeDetector; + + fn count_up_iff_changed( + mut counter: ResMut, + change_detector: ResMut, + ) { + if change_detector.is_changed() { + counter.0 += 1; + } + } + + let mut app = App::new(); + app.add_plugins(AssetPlugin::default()); + app.init_resource::(); + app.init_resource::(); + assert_eq!(*app.world.resource::(), Counter(0)); + + // Resources are changed when they are first added. + let mut callbacks = app.world.resource_mut::>(); + let handle = callbacks.add(Callback::from_system(count_up_iff_changed)); + app.world + .run_callback(handle.clone()) + .expect("system runs successfully"); + assert_eq!(*app.world.resource::(), Counter(1)); + // Nothing changed + app.world + .run_callback(handle.clone()) + .expect("system runs successfully"); + assert_eq!(*app.world.resource::(), Counter(1)); + // Making a change + app.world.resource_mut::().set_changed(); + app.world + .run_callback(handle) + .expect("system runs successfully"); + assert_eq!(*app.world.resource::(), Counter(2)); + } + + #[test] + fn local_variables() { + // The `Local` begins at the default value of 0 + fn doubling(mut last_counter: Local, mut counter: ResMut) { + counter.0 += last_counter.0; + last_counter.0 = counter.0; + } + + let mut app = App::new(); + app.add_plugins(AssetPlugin::default()); + app.insert_resource(Counter(1)); + assert_eq!(*app.world.resource::(), Counter(1)); + + let mut callbacks = app.world.resource_mut::>(); + let handle = callbacks.add(Callback::from_system(doubling)); + app.world + .run_callback(handle.clone()) + .expect("system runs successfully"); + assert_eq!(*app.world.resource::(), Counter(1)); + app.world + .run_callback(handle.clone()) + .expect("system runs successfully"); + assert_eq!(*app.world.resource::(), Counter(2)); + app.world + .run_callback(handle.clone()) + .expect("system runs successfully"); + assert_eq!(*app.world.resource::(), Counter(4)); + app.world + .run_callback(handle) + .expect("system runs successfully"); + assert_eq!(*app.world.resource::(), Counter(8)); + } + + #[test] + fn input_values() { + // Verify that a non-Copy, non-Clone type can be passed in. + #[derive(TypePath)] + struct NonCopy(u8); + + fn increment_sys(In(NonCopy(increment_by)): In, mut counter: ResMut) { + counter.0 += increment_by; + } + + let mut app = App::new(); + app.add_plugins(AssetPlugin::default()); + app.init_asset::>(); + + let mut callbacks = app.world.resource_mut::>>(); + let handle = callbacks.add(Callback::from_system(increment_sys)); + + // Insert the resource after registering the system. + app.insert_resource(Counter(1)); + assert_eq!(*app.world.resource::(), Counter(1)); + + app.world + .run_callback_with_input(handle.clone(), NonCopy(1)) + .expect("system runs successfully"); + assert_eq!(*app.world.resource::(), Counter(2)); + + app.world + .run_callback_with_input(handle.clone(), NonCopy(1)) + .expect("system runs successfully"); + assert_eq!(*app.world.resource::(), Counter(3)); + + app.world + .run_callback_with_input(handle.clone(), NonCopy(20)) + .expect("system runs successfully"); + assert_eq!(*app.world.resource::(), Counter(23)); + + app.world + .run_callback_with_input(handle, NonCopy(1)) + .expect("system runs successfully"); + assert_eq!(*app.world.resource::(), Counter(24)); + } + + #[test] + fn output_values() { + // Verify that a non-Copy, non-Clone type can be returned. + #[derive(TypePath, Eq, PartialEq, Debug)] + struct NonCopy(u8); + + fn increment_sys(mut counter: ResMut) -> NonCopy { + counter.0 += 1; + NonCopy(counter.0) + } + + let mut app = App::new(); + app.add_plugins(AssetPlugin::default()); + app.init_asset::>(); + + let mut callbacks = app.world.resource_mut::>>(); + let handle = callbacks.add(Callback::from_system(increment_sys)); + + // Insert the resource after registering the system. + app.insert_resource(Counter(1)); + assert_eq!(*app.world.resource::(), Counter(1)); + + let output = app + .world + .run_callback(handle.clone()) + .expect("system runs successfully"); + assert_eq!(*app.world.resource::(), Counter(2)); + assert_eq!(output, NonCopy(2)); + + let output = app + .world + .run_callback(handle) + .expect("system runs successfully"); + assert_eq!(*app.world.resource::(), Counter(3)); + assert_eq!(output, NonCopy(3)); + } + + #[test] + fn nested_systems() { + #[derive(Component)] + struct Call(Handle); + + fn nested(query: Query<&Call>, mut commands: Commands) { + for call in query.iter() { + commands.run_callback(call.0.clone()); + } + } + + let mut app = App::new(); + app.add_plugins(AssetPlugin::default()); + app.insert_resource(Counter(0)); + + let mut callbacks = app.world.resource_mut::>(); + + let increment_two = callbacks.add(Callback::from_system(|mut counter: ResMut| { + counter.0 += 2; + })); + let increment_three = + callbacks.add(Callback::from_system(|mut counter: ResMut| { + counter.0 += 3; + })); + let nested_handle = callbacks.add(Callback::from_system(nested)); + + app.world.spawn(Call(increment_two)); + app.world.spawn(Call(increment_three)); + let _ = app.world.run_callback(nested_handle); + assert_eq!(*app.world.resource::(), Counter(5)); + } + + #[test] + fn nested_systems_with_inputs() { + #[derive(Component)] + struct Call(Handle>, u8); + + fn nested(query: Query<&Call>, mut commands: Commands) { + for callback in query.iter() { + commands.run_callback_with_input(callback.0.clone(), callback.1); + } + } + + let mut app = App::new(); + app.add_plugins(AssetPlugin::default()); + app.init_asset::>(); + app.insert_resource(Counter(0)); + + let mut callbacks = app.world.resource_mut::>>(); + + let increment_by = callbacks.add(Callback::from_system( + |In(amt): In, mut counter: ResMut| { + counter.0 += amt; + }, + )); + let mut callbacks = app.world.resource_mut::>(); + let nested_id = callbacks.add(Callback::from_system(nested)); + + app.world.spawn(Call(increment_by.clone(), 2)); + app.world.spawn(Call(increment_by, 3)); + let _ = app.world.run_callback(nested_id); + assert_eq!(*app.world.resource::(), Counter(5)); + } + + #[test] + fn error_on_recursive_call() { + #[derive(Component)] + struct Call(Handle); + + let mut app = App::new(); + app.add_plugins(AssetPlugin::default()); + app.insert_resource(Counter(0)); + + let mut callbacks = app.world.resource_mut::>(); + + let call_self = callbacks.add(Callback::from_system(|world: &mut World| { + let callbacks = world.resource::>(); + // this is this callback's id because it is the only callback. + let own_id = callbacks.iter().next().unwrap().0.clone(); + let own_handle = Handle::Weak(own_id); + let result = world.run_callback(own_handle.clone()); + + assert_eq!(result, Err(CallbackError::Recursive(own_handle))); + })); + + let _ = app.world.run_callback(call_self); + } +} diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 61eeb58f8a453..42aec331b2038 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -7,11 +7,12 @@ pub mod prelude { #[doc(hidden)] pub use crate::{ Asset, AssetApp, AssetEvent, AssetId, AssetMode, AssetPlugin, AssetServer, Assets, Handle, - UntypedHandle, + RunCallbackCommands, RunCallbackWorld, UntypedHandle, }; } mod assets; +mod callback; mod event; mod folder; mod handle; @@ -23,6 +24,7 @@ mod server; pub use assets::*; pub use bevy_asset_macros::Asset; +pub use callback::*; pub use event::*; pub use folder::*; pub use futures_lite::{AsyncReadExt, AsyncWriteExt}; @@ -46,7 +48,7 @@ use bevy_app::{App, First, MainScheduleOrder, Plugin, PostUpdate}; use bevy_ecs::{ reflect::AppTypeRegistry, schedule::{IntoSystemConfigs, IntoSystemSetConfigs, ScheduleLabel, SystemSet}, - system::Resource, + system::{Callback, Resource}, world::FromWorld, }; use bevy_log::error; @@ -213,6 +215,7 @@ impl Plugin for AssetPlugin { .init_asset::() .init_asset::() .init_asset::<()>() + .init_asset::() .configure_sets( UpdateAssets, TrackAssets.after(handle_internal_asset_events), diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 7372d92c25e0a..e9539729e48a7 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -43,8 +43,9 @@ pub mod prelude { OnTransition, Schedule, Schedules, State, States, SystemSet, }, system::{ - Commands, Deferred, In, IntoSystem, Local, NonSend, NonSendMut, ParallelCommands, - ParamSet, Query, ReadOnlySystem, Res, ResMut, Resource, System, SystemParamFunction, + Callback, Commands, Deferred, In, IntoSystem, Local, NonSend, NonSendMut, + ParallelCommands, ParamSet, Query, ReadOnlySystem, Res, ResMut, Resource, System, + SystemParamFunction, }, world::{EntityMut, EntityRef, EntityWorldMut, FromWorld, World}, }; diff --git a/crates/bevy_ecs/src/system/system_registry.rs b/crates/bevy_ecs/src/system/system_registry.rs index 5a9d7269d1032..046f26d109a94 100644 --- a/crates/bevy_ecs/src/system/system_registry.rs +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -3,15 +3,45 @@ use crate::system::{BoxedSystem, Command, IntoSystem}; use crate::world::World; use crate::{self as bevy_ecs}; use bevy_ecs_macros::Component; +use bevy_reflect::TypePath; use thiserror::Error; /// A small wrapper for [`BoxedSystem`] that also keeps track whether or not the system has been initialized. #[derive(Component)] -struct RegisteredSystem { +struct RegisteredSystem(Callback); + +#[derive(TypePath)] +pub struct Callback { initialized: bool, system: BoxedSystem, } +impl Callback { + pub fn new(system: BoxedSystem) -> Self { + Self { + initialized: false, + system, + } + } + + pub fn from_system + 'static>(system: S) -> Self { + Self::new(Box::new(IntoSystem::into_system(system))) + } +} + +impl Callback { + pub fn run_with_input(&mut self, world: &mut World, input: I) -> O { + if !self.initialized { + self.system.initialize(world); + self.initialized = true; + } + let result = self.system.run(input, world); + self.system.apply_deferred(world); + + result + } +} + /// A system that has been removed from the registry. /// It contains the system and whether or not it has been initialized. /// @@ -100,11 +130,7 @@ impl World { system: BoxedSystem, ) -> SystemId { SystemId( - self.spawn(RegisteredSystem { - initialized: false, - system, - }) - .id(), + self.spawn(RegisteredSystem(Callback::new(system))).id(), std::marker::PhantomData, ) } @@ -126,8 +152,8 @@ impl World { .ok_or(RegisteredSystemError::SelfRemove(id))?; entity.despawn(); Ok(RemovedSystem { - initialized: registered_system.initialized, - system: registered_system.system, + initialized: registered_system.0.initialized, + system: registered_system.0.system, }) } None => Err(RegisteredSystemError::SystemIdNotRegistered(id)), @@ -273,27 +299,16 @@ impl World { .ok_or(RegisteredSystemError::SystemIdNotRegistered(id))?; // take ownership of system trait object - let RegisteredSystem { - mut initialized, - mut system, - } = entity + let RegisteredSystem(mut drive_system) = entity .take::>() .ok_or(RegisteredSystemError::Recursive(id))?; // run the system - if !initialized { - system.initialize(self); - initialized = true; - } - let result = system.run(input, self); - system.apply_deferred(self); + let result = drive_system.run_with_input(self, input); // return ownership of system trait object (if entity still exists) if let Some(mut entity) = self.get_entity_mut(id.0) { - entity.insert::>(RegisteredSystem { - initialized, - system, - }); + entity.insert::>(RegisteredSystem(drive_system)); } Ok(result) } diff --git a/examples/README.md b/examples/README.md index 0450ef88159b8..f1ca5d67dda1b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -184,6 +184,7 @@ Example | Description [Asset Decompression](../examples/asset/asset_decompression.rs) | Demonstrates loading a compressed asset [Asset Loading](../examples/asset/asset_loading.rs) | Demonstrates various methods to load assets [Asset Processing](../examples/asset/processing/asset_processing.rs) | Demonstrates how to process and load custom assets +[Callbacks](../examples/asset/callbacks.rs) | Demonstrates using Callbacks [Custom Asset](../examples/asset/custom_asset.rs) | Implements a custom asset loader [Custom Asset IO](../examples/asset/custom_asset_reader.rs) | Implements a custom AssetReader [Hot Reloading of Assets](../examples/asset/hot_asset_reloading.rs) | Demonstrates automatic reloading of assets when modified on disk diff --git a/examples/asset/callbacks.rs b/examples/asset/callbacks.rs new file mode 100644 index 0000000000000..70b05309447f1 --- /dev/null +++ b/examples/asset/callbacks.rs @@ -0,0 +1,108 @@ +//! Demonstrates the use of [`Callback`]s, which run once when triggered. +//! +//! These can be useful to help structure your logic in a push-based fashion, +//! reducing the overhead of running extremely rarely run systems +//! and improving schedule flexibility. Their advantage over [`SystemId`]s is +//! that they are [`Asset`]s. +//! +//! See the [`RunCallbackWorld::run_callback`](bevy::prelude::RunCallbackWorld::run_callback) +//! docs for more details. + +use bevy::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + // `init_asset` must be called for new types of callbacks. + // By default, only `Callback` (no input or output) is inited. + .init_asset::>() + .add_systems(Startup, setup) + .add_systems(Update, evaluate_callbacks) + .run(); +} + +// Need to store the `Handle` to the callback +#[derive(Component)] +struct OnPressedDown(Handle>); + +// Input and output of `Callback`s must implement `TypePath` +#[derive(TypePath)] +struct PressedDown(Entity); + +// Need mutible access to `Assets` to add a callback +fn setup(mut commands: Commands, mut callbacks: ResMut>>) { + // Camera + commands.spawn(Camera2dBundle::default()); + + // root node + commands + .spawn(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(|parent| { + // button + parent + .spawn(( + ButtonBundle { + style: Style { + width: Val::Px(250.0), + height: Val::Px(65.0), + margin: UiRect::all(Val::Px(20.0)), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + background_color: Color::TEAL.into(), + ..default() + }, + // Add callback + OnPressedDown(callbacks.add(Callback::from_system(toggle))), + )) + .with_children(|parent| { + // text + parent.spawn(TextBundle::from_section( + "false", + TextStyle { + font_size: 40.0, + color: Color::BLACK, + ..default() + }, + )); + }); + }); +} + +// `Callback`s can be created from any system. +fn toggle( + In(pressed_down): In, + mut text: Query<&mut Text>, + children: Query<&Children>, + mut value: Local, +) { + *value = !*value; + let children = children.get(pressed_down.0).unwrap(); + let mut text = text.iter_many_mut(children); + let mut text = text.fetch_next().unwrap(); + text.sections[0].value = value.to_string(); +} + +/// Runs the systems associated with each `OnPressedDown` component if button is pressed. +/// +/// This could be done in an exclusive system rather than using `Commands` if preferred. +fn evaluate_callbacks( + on_pressed_down: Query<(Entity, &OnPressedDown, &Interaction), Changed>, + mut commands: Commands, +) { + for (entity, on_button_pressed, interaction) in &on_pressed_down { + if *interaction == Interaction::Pressed { + commands.run_callback_with_input(on_button_pressed.0.clone(), PressedDown(entity)); + } + } +}