From 208ecb53dc5aed4baefc5ca5dc73bfdfee16a6e6 Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 21 Nov 2023 16:04:37 -0800 Subject: [PATCH] Append commands (#10400) # Objective - I've been experimenting with different patterns to try and make async tasks more convenient. One of the better ones I've found is to return a command queue to allow for deferred &mut World access. It can be convenient to check for task completion in a normal system, but it is hard to do something with the command queue after getting it back. This pr adds a `append` to Commands. This allows appending the returned command queue onto the system's commands. ## Solution - I edited the async compute example to use the new `append`, but not sure if I should keep the example changed as this might be too opinionated. ## Future Work - It would be very easy to pull the pattern used in the example out into a plugin or a external crate, so users wouldn't have to add the checking system. --- ## Changelog - add `append` to `Commands` and `CommandQueue` --- .../src/system/commands/command_queue.rs | 5 ++ crates/bevy_ecs/src/system/commands/mod.rs | 24 ++++++++ examples/async_tasks/async_compute.rs | 61 ++++++++++++------- 3 files changed, 69 insertions(+), 21 deletions(-) diff --git a/crates/bevy_ecs/src/system/commands/command_queue.rs b/crates/bevy_ecs/src/system/commands/command_queue.rs index 86a4e160e1..dab6a91666 100644 --- a/crates/bevy_ecs/src/system/commands/command_queue.rs +++ b/crates/bevy_ecs/src/system/commands/command_queue.rs @@ -136,6 +136,11 @@ impl CommandQueue { cursor = unsafe { cursor.add(size) }; } } + + /// Take all commands from `other` and append them to `self`, leaving `other` empty + pub fn append(&mut self, other: &mut CommandQueue) { + self.bytes.append(&mut other.bytes); + } } #[cfg(test)] diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 15dcafead3..1f9421474f 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -146,6 +146,11 @@ impl<'w, 's> Commands<'w, 's> { } } + /// Take all commands from `other` and append them to `self`, leaving `other` empty + pub fn append(&mut self, other: &mut CommandQueue) { + self.queue.append(other); + } + /// Pushes a [`Command`] to the queue for creating a new empty [`Entity`], /// and returns its corresponding [`EntityCommands`]. /// @@ -1321,4 +1326,23 @@ mod tests { assert!(!world.contains_resource::>()); assert!(world.contains_resource::>()); } + + #[test] + fn append() { + let mut world = World::default(); + let mut queue_1 = CommandQueue::default(); + { + let mut commands = Commands::new(&mut queue_1, &world); + commands.insert_resource(W(123i32)); + } + let mut queue_2 = CommandQueue::default(); + { + let mut commands = Commands::new(&mut queue_2, &world); + commands.insert_resource(W(456.0f64)); + } + queue_1.append(&mut queue_2); + queue_1.apply(&mut world); + assert!(world.contains_resource::>()); + assert!(world.contains_resource::>()); + } } diff --git a/examples/async_tasks/async_compute.rs b/examples/async_tasks/async_compute.rs index a6ffb37831..ebf4eaa986 100644 --- a/examples/async_tasks/async_compute.rs +++ b/examples/async_tasks/async_compute.rs @@ -2,6 +2,7 @@ //! to spawn, poll, and complete tasks across systems and system ticks. use bevy::{ + ecs::system::{CommandQueue, SystemState}, prelude::*, tasks::{block_on, futures_lite::future, AsyncComputeTaskPool, Task}, }; @@ -42,7 +43,7 @@ fn add_assets( } #[derive(Component)] -struct ComputeTransform(Task); +struct ComputeTransform(Task); /// This system generates tasks simulating computationally intensive /// work that potentially spans multiple frames/ticks. A separate @@ -56,6 +57,7 @@ fn spawn_tasks(mut commands: Commands) { // Spawn new task on the AsyncComputeTaskPool; the task will be // executed in the background, and the Task future returned by // spawn() can be used to poll for the result + let entity = commands.spawn_empty().id(); let task = thread_pool.spawn(async move { let mut rng = rand::thread_rng(); let start_time = Instant::now(); @@ -66,11 +68,41 @@ fn spawn_tasks(mut commands: Commands) { } // Such hard work, all done! - Transform::from_xyz(x as f32, y as f32, z as f32) + let transform = Transform::from_xyz(x as f32, y as f32, z as f32); + let mut command_queue = CommandQueue::default(); + + // we use a raw command queue to pass a FnOne(&mut World) back to be + // applied in a deferred manner. + command_queue.push(move |world: &mut World| { + let (box_mesh_handle, box_material_handle) = { + let mut system_state = SystemState::<( + Res, + Res, + )>::new(world); + let (box_mesh_handle, box_material_handle) = + system_state.get_mut(world); + + (box_mesh_handle.clone(), box_material_handle.clone()) + }; + + world + .entity_mut(entity) + // Add our new PbrBundle of components to our tagged entity + .insert(PbrBundle { + mesh: box_mesh_handle, + material: box_material_handle, + transform, + ..default() + }) + // Task is complete, so remove task component from entity + .remove::(); + }); + + command_queue }); // Spawn new entity and add our new task as a component - commands.spawn(ComputeTransform(task)); + commands.entity(entity).insert(ComputeTransform(task)); } } } @@ -80,24 +112,11 @@ fn spawn_tasks(mut commands: Commands) { /// tasks to see if they're complete. If the task is complete it takes the result, adds a /// new [`PbrBundle`] of components to the entity using the result from the task's work, and /// removes the task component from the entity. -fn handle_tasks( - mut commands: Commands, - mut transform_tasks: Query<(Entity, &mut ComputeTransform)>, - box_mesh_handle: Res, - box_material_handle: Res, -) { - for (entity, mut task) in &mut transform_tasks { - if let Some(transform) = block_on(future::poll_once(&mut task.0)) { - // Add our new PbrBundle of components to our tagged entity - commands.entity(entity).insert(PbrBundle { - mesh: box_mesh_handle.clone(), - material: box_material_handle.clone(), - transform, - ..default() - }); - - // Task is complete, so remove task component from entity - commands.entity(entity).remove::(); +fn handle_tasks(mut commands: Commands, mut transform_tasks: Query<&mut ComputeTransform>) { + for mut task in &mut transform_tasks { + if let Some(mut commands_queue) = block_on(future::poll_once(&mut task.0)) { + // append the returned command queue to have it execute later + commands.append(&mut commands_queue); } } }