From 1cf6ebd57422693c0d614a633e4795a9d7ae050e Mon Sep 17 00:00:00 2001 From: NathanW Date: Thu, 20 May 2021 09:12:13 -0600 Subject: [PATCH] [ecs] command error handling --- Cargo.toml | 4 + crates/bevy_ecs/src/lib.rs | 5 +- crates/bevy_ecs/src/system/commands/config.rs | 201 ++++++++ .../system/{commands.rs => commands/mod.rs} | 451 +++++++++++++++--- examples/README.md | 1 + examples/ecs/command_error_handling.rs | 77 +++ 6 files changed, 682 insertions(+), 57 deletions(-) create mode 100644 crates/bevy_ecs/src/system/commands/config.rs rename crates/bevy_ecs/src/system/{commands.rs => commands/mod.rs} (51%) create mode 100644 examples/ecs/command_error_handling.rs diff --git a/Cargo.toml b/Cargo.toml index 9e8551ffc3daf..b81785e5aeabf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -262,6 +262,10 @@ path = "examples/ecs/ecs_guide.rs" name = "change_detection" path = "examples/ecs/change_detection.rs" +[[example]] +name = "command_error_handling" +path = "examples/ecs/command_error_handling.rs" + [[example]] name = "event" path = "examples/ecs/event.rs" diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index a6fdbced0894d..78051a0bd9e8b 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -29,8 +29,9 @@ pub mod prelude { Schedule, Stage, StageLabel, State, SystemLabel, SystemSet, SystemStage, }, system::{ - Commands, In, IntoChainSystem, IntoExclusiveSystem, IntoSystem, Local, NonSend, - NonSendMut, Query, QuerySet, RemovedComponents, Res, ResMut, System, + CommandErrorHandler, Commands, FallibleCommand, In, IntoChainSystem, + IntoExclusiveSystem, IntoSystem, Local, NonSend, NonSendMut, Query, QuerySet, + RemovedComponents, Res, ResMut, System, }, world::{FromWorld, Mut, World}, }; diff --git a/crates/bevy_ecs/src/system/commands/config.rs b/crates/bevy_ecs/src/system/commands/config.rs new file mode 100644 index 0000000000000..a73c7b9a506b1 --- /dev/null +++ b/crates/bevy_ecs/src/system/commands/config.rs @@ -0,0 +1,201 @@ +use crate::{ + prelude::{FallibleCommand, World}, + system::Command, +}; +use bevy_utils::tracing::error; +use std::{ + fmt::Debug, + ops::{Deref, DerefMut}, +}; + +#[doc(hidden)] +pub trait AddCommand { + fn add_command(&mut self, command: impl Command); +} + +/// Provides configuration mechanisms in case a command errors. +/// You can specify a custom handler via [`FallibleCommandConfig::on_failure`] or +/// use one of the provided implementations. +/// +/// ## Note +/// The default error handler logs the error (via [`error!`]), but does not panic. +pub struct FallibleCommandConfig<'a, C, T> +where + C: FallibleCommand, + T: AddCommand, +{ + command: Option, + inner: &'a mut T, +} + +impl<'a, C, T> Deref for FallibleCommandConfig<'a, C, T> +where + C: FallibleCommand, + T: AddCommand, +{ + type Target = T; + + #[inline] + fn deref(&self) -> &Self::Target { + self.inner + } +} + +impl<'a, C, T> DerefMut for FallibleCommandConfig<'a, C, T> +where + C: FallibleCommand, + T: AddCommand, +{ + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + self.inner + } +} + +/// Builtin command error handlers. +pub struct CommandErrorHandler; + +impl CommandErrorHandler { + /// If the command failed, log the error. + /// + /// ## Note + /// This is the default behavior if no error handler is specified. + pub fn log(error: E, _ctx: CommandContext) { + error!("Commands failed with error: {:?}", error) + } + + /// If the command failed, [`panic!`] with the error. + pub fn panic(error: E, _ctx: CommandContext) { + panic!("Commands failed with error: {:?}", error) + } + + /// If the command failed, ignore the error and silently succeed. + pub fn ignore(_error: E, _ctx: CommandContext) {} +} + +pub(crate) struct HandledErrorCommand +where + C: FallibleCommand, + F: FnOnce(C::Error, CommandContext) + Send + Sync + 'static, +{ + pub(crate) command: C, + pub(crate) error_handler: F, +} + +impl Command for HandledErrorCommand +where + C: FallibleCommand, + F: FnOnce(C::Error, CommandContext) + Send + Sync + 'static, +{ + fn write(self: Box, world: &mut World) { + let HandledErrorCommand { + command, + error_handler, + } = *self; + + if let Err(error) = command.try_write(world) { + error_handler(error, CommandContext { world }); + } + } +} + +#[non_exhaustive] +pub struct CommandContext<'a> { + pub world: &'a mut World, +} + +/// Similar to [`FallibleCommandConfig`] however does not +/// implement [`DerefMut`] nor return `&mut T` of the underlying +/// Commands type. +pub struct FinalFallibleCommandConfig<'a, C, T> +where + C: FallibleCommand, + T: AddCommand, +{ + command: Option, + inner: &'a mut T, +} + +macro_rules! impl_fallible_commands { + ($name:ident, $returnty:ty, $returnfunc:ident) => { + impl<'a, C, T> $name<'a, C, T> + where + C: FallibleCommand, + C::Error: Debug, + T: AddCommand, + { + #[inline] + pub(crate) fn new(command: C, inner: &'a mut T) -> Self { + Self { + command: Some(command), + inner, + } + } + + #[inline] + #[allow(dead_code)] + fn return_inner(&mut self) -> &mut T { + self.inner + } + + #[inline] + #[allow(dead_code)] + fn return_unit(&self) {} + } + + impl<'a, C, T> $name<'a, C, T> + where + C: FallibleCommand, + C::Error: Debug, + T: AddCommand, + { + /// If the command failed, run the provided `error_handler`. + /// + /// ## Note + /// This is normally used in conjunction with [`CommandErrorHandler`]. + /// However, this can also be used with custom error handlers (e.g. closures). + /// + /// # Examples + /// ``` + /// use bevy_ecs::prelude::*; + /// + /// fn system(mut commands: Commands) { + /// // built-in error handler + /// commands.spawn().insert(42).on_err(CommandErrorHandler::ignore); + /// + /// // custom error handler + /// commands.spawn().insert(42).on_err(|error, ctx| {}); + /// } + /// ``` + pub fn on_err( + &mut self, + error_handler: impl FnOnce(C::Error, CommandContext) + Send + Sync + 'static, + ) -> $returnty { + let command = self + .command + .take() + .expect("Cannot call `on_err` multiple times for a command error handler."); + self.inner.add_command(HandledErrorCommand { + command, + error_handler, + }); + self.$returnfunc() + } + } + + impl<'a, C, T> Drop for $name<'a, C, T> + where + C: FallibleCommand, + T: AddCommand, + { + fn drop(&mut self) { + if self.command.is_some() { + self.on_err(CommandErrorHandler::log); + } + } + } + }; +} + +impl_fallible_commands!(FinalFallibleCommandConfig, (), return_unit); +impl_fallible_commands!(FallibleCommandConfig, &mut T, return_inner); diff --git a/crates/bevy_ecs/src/system/commands.rs b/crates/bevy_ecs/src/system/commands/mod.rs similarity index 51% rename from crates/bevy_ecs/src/system/commands.rs rename to crates/bevy_ecs/src/system/commands/mod.rs index 90adfb8dc510c..373e2b867b5d5 100644 --- a/crates/bevy_ecs/src/system/commands.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -1,17 +1,28 @@ +mod config; +pub use config::*; + use crate::{ bundle::Bundle, component::Component, entity::{Entities, Entity}, world::World, }; -use bevy_utils::tracing::debug; -use std::marker::PhantomData; +use std::{fmt::Debug, marker::PhantomData}; /// A [`World`] mutation. +/// If this could potentially fail, use [`FallibleCommand`]. pub trait Command: Send + Sync + 'static { fn write(self: Box, world: &mut World); } +/// A [`World`] mutation that can potentially fail. +/// For an infallible variant, use [`Command`]. +pub trait FallibleCommand: Send + Sync + 'static { + type Error: Debug; + + fn try_write(self, world: &mut World) -> Result<(), Self::Error>; +} + /// A queue of [`Command`]s. #[derive(Default)] pub struct CommandQueue { @@ -76,7 +87,7 @@ impl<'a> Commands<'a> { /// } /// # example_system.system(); /// ``` - pub fn spawn(&mut self) -> EntityCommands<'a, '_> { + pub fn spawn<'this>(&'this mut self) -> EntityCommands<'a, 'this> { let entity = self.entities.reserve_entity(); EntityCommands { entity, @@ -124,7 +135,7 @@ impl<'a> Commands<'a> { /// } /// # example_system.system(); /// ``` - pub fn spawn_bundle<'b, T: Bundle>(&'b mut self, bundle: T) -> EntityCommands<'a, 'b> { + pub fn spawn_bundle<'this, T: Bundle>(&'this mut self, bundle: T) -> EntityCommands<'a, 'this> { let mut e = self.spawn(); e.insert_bundle(bundle); e @@ -149,7 +160,7 @@ impl<'a> Commands<'a> { /// } /// # example_system.system(); /// ``` - pub fn entity(&mut self, entity: Entity) -> EntityCommands<'a, '_> { + pub fn entity<'this>(&'this mut self, entity: Entity) -> EntityCommands<'a, 'this> { EntityCommands { entity, commands: self, @@ -172,16 +183,35 @@ impl<'a> Commands<'a> { } /// Queue a resource removal. - pub fn remove_resource(&mut self) { - self.queue.push(RemoveResource:: { - phantom: PhantomData, - }); + pub fn remove_resource( + &mut self, + ) -> FinalFallibleCommandConfig<'_, RemoveResource, Self> { + FinalFallibleCommandConfig::new( + RemoveResource { + phantom: PhantomData, + }, + self, + ) } /// Adds a command directly to the command list. pub fn add(&mut self, command: C) { self.queue.push(command); } + + /// Adds a fallible command to the command list. + pub fn add_fallible(&mut self, command: C) -> FinalFallibleCommandConfig<'_, C, Self> + where + C: FallibleCommand, + { + FinalFallibleCommandConfig::new(command, self) + } +} + +impl<'a> AddCommand for Commands<'a> { + fn add_command(&mut self, command: impl Command) { + self.add(command); + } } /// A list of commands that will be run to modify an [`Entity`]. @@ -198,12 +228,17 @@ impl<'a, 'b> EntityCommands<'a, 'b> { } /// Adds a [`Bundle`] of components to the current entity. - pub fn insert_bundle(&mut self, bundle: impl Bundle) -> &mut Self { - self.commands.add(InsertBundle { - entity: self.entity, - bundle, - }); - self + pub fn insert_bundle( + &mut self, + bundle: T, + ) -> FallibleCommandConfig<'_, InsertBundle, Self> { + FallibleCommandConfig::new( + InsertBundle { + entity: self.entity, + bundle, + }, + self, + ) } /// Adds a single [`Component`] to the current entity. @@ -238,43 +273,55 @@ impl<'a, 'b> EntityCommands<'a, 'b> { /// } /// # example_system.system(); /// ``` - pub fn insert(&mut self, component: impl Component) -> &mut Self { - self.commands.add(Insert { - entity: self.entity, - component, - }); - self + pub fn insert( + &mut self, + component: T, + ) -> FallibleCommandConfig<'_, Insert, Self> { + FallibleCommandConfig::new( + Insert { + entity: self.entity, + component, + }, + self, + ) } /// See [`EntityMut::remove_bundle`](crate::world::EntityMut::remove_bundle). - pub fn remove_bundle(&mut self) -> &mut Self + pub fn remove_bundle(&mut self) -> FallibleCommandConfig<'_, RemoveBundle, Self> where T: Bundle, { - self.commands.add(RemoveBundle:: { - entity: self.entity, - phantom: PhantomData, - }); - self + FallibleCommandConfig::new( + RemoveBundle { + entity: self.entity, + phantom: PhantomData, + }, + self, + ) } /// See [`EntityMut::remove`](crate::world::EntityMut::remove). - pub fn remove(&mut self) -> &mut Self + pub fn remove(&mut self) -> FallibleCommandConfig<'_, Remove, Self> where T: Component, { - self.commands.add(Remove:: { - entity: self.entity, - phantom: PhantomData, - }); - self + FallibleCommandConfig::new( + Remove { + entity: self.entity, + phantom: PhantomData, + }, + self, + ) } /// Despawns only the specified entity, not including its children. - pub fn despawn(&mut self) { - self.commands.add(Despawn { - entity: self.entity, - }) + pub fn despawn(&mut self) -> FinalFallibleCommandConfig<'_, Despawn, Self> { + FinalFallibleCommandConfig::new( + Despawn { + entity: self.entity, + }, + self, + ) } /// Returns the underlying `[Commands]`. @@ -283,6 +330,12 @@ impl<'a, 'b> EntityCommands<'a, 'b> { } } +impl<'a, 'b> AddCommand for EntityCommands<'a, 'b> { + fn add_command(&mut self, command: impl Command) { + self.commands.add_command(command); + } +} + #[derive(Debug)] pub struct Spawn { pub bundle: T, @@ -320,10 +373,22 @@ pub struct Despawn { pub entity: Entity, } -impl Command for Despawn { - fn write(self: Box, world: &mut World) { - if !world.despawn(self.entity) { - debug!("Failed to despawn non-existent entity {:?}", self.entity); +/// The error resulting from [`EntityCommands::despawn`] +#[derive(Debug)] +pub struct DespawnError { + pub entity: Entity, +} + +impl FallibleCommand for Despawn { + type Error = DespawnError; + + fn try_write(self, world: &mut World) -> Result<(), Self::Error> { + if world.despawn(self.entity) { + Ok(()) + } else { + Err(DespawnError { + entity: self.entity, + }) } } } @@ -333,12 +398,38 @@ pub struct InsertBundle { pub bundle: T, } -impl Command for InsertBundle +/// The error resulting from [`EntityCommands::insert_bundle`] +/// Contains both the failed to insert bundle and the relative entity. +pub struct InsertBundleError { + pub entity: Entity, + pub bundle: T, +} + +impl Debug for InsertBundleError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("InsertBundleError") + .field("entity", &self.entity) + .field("bundle_type", &std::any::type_name::()) + .finish() + } +} + +impl FallibleCommand for InsertBundle where T: Bundle + 'static, { - fn write(self: Box, world: &mut World) { - world.entity_mut(self.entity).insert_bundle(self.bundle); + type Error = InsertBundleError; + + fn try_write(self, world: &mut World) -> Result<(), Self::Error> { + if let Some(mut entity_mut) = world.get_entity_mut(self.entity) { + entity_mut.insert_bundle(self.bundle); + Ok(()) + } else { + Err(InsertBundleError { + entity: self.entity, + bundle: self.bundle, + }) + } } } @@ -348,12 +439,39 @@ pub struct Insert { pub component: T, } -impl Command for Insert +/// The error resulting from [`EntityCommands::insert`] +/// Contains both the failed to insert component and the relative entity. +pub struct InsertError { + pub entity: Entity, + pub component: T, +} + +impl Debug for InsertError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("InsertError") + .field("entity", &self.entity) + .field("component_type", &std::any::type_name::()) + .finish() + } +} + +impl FallibleCommand for Insert where T: Component, { - fn write(self: Box, world: &mut World) { - world.entity_mut(self.entity).insert(self.component); + type Error = InsertError; + + fn try_write(self, world: &mut World) -> Result<(), Self::Error> { + match world.get_entity_mut(self.entity) { + Some(mut entity) => { + entity.insert(self.component); + Ok(()) + } + None => Err(InsertError { + entity: self.entity, + component: self.component, + }), + } } } @@ -363,13 +481,36 @@ pub struct Remove { phantom: PhantomData, } -impl Command for Remove +/// The error resulting from [`EntityCommands::remove`] +pub struct RemoveError { + pub entity: Entity, + phantom: PhantomData, +} + +impl Debug for RemoveError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RemoveError") + .field("entity", &self.entity) + .field("component_type", &std::any::type_name::()) + .finish() + } +} + +impl FallibleCommand for Remove where T: Component, { - fn write(self: Box, world: &mut World) { + type Error = RemoveError; + + fn try_write(self, world: &mut World) -> Result<(), Self::Error> { if let Some(mut entity_mut) = world.get_entity_mut(self.entity) { entity_mut.remove::(); + Ok(()) + } else { + Err(RemoveError { + entity: self.entity, + phantom: PhantomData, + }) } } } @@ -380,15 +521,38 @@ pub struct RemoveBundle { pub phantom: PhantomData, } -impl Command for RemoveBundle +/// The error resulting from [`EntityCommands::remove_bundle`] +pub struct RemoveBundleError { + pub entity: Entity, + phantom: PhantomData, +} + +impl Debug for RemoveBundleError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RemoveBundleError") + .field("entity", &self.entity) + .field("bundle_type", &std::any::type_name::()) + .finish() + } +} + +impl FallibleCommand for RemoveBundle where T: Bundle, { - fn write(self: Box, world: &mut World) { + type Error = RemoveBundleError; + + fn try_write(self, world: &mut World) -> Result<(), Self::Error> { if let Some(mut entity_mut) = world.get_entity_mut(self.entity) { // remove intersection to gracefully handle components that were removed before running // this command entity_mut.remove_bundle_intersection::(); + Ok(()) + } else { + Err(RemoveBundleError { + entity: self.entity, + phantom: PhantomData, + }) } } } @@ -407,18 +571,42 @@ pub struct RemoveResource { pub phantom: PhantomData, } -impl Command for RemoveResource { - fn write(self: Box, world: &mut World) { - world.remove_resource::(); +/// The error resulting from [`Commands::remove_resource`] +pub struct RemoveResourceError { + phantom: PhantomData, +} + +impl Debug for RemoveResourceError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RemoveResourceError") + .field("resource_type", &std::any::type_name::()) + .finish() + } +} + +impl FallibleCommand for RemoveResource { + type Error = RemoveResourceError; + + fn try_write(self, world: &mut World) -> Result<(), Self::Error> { + if world.remove_resource::().is_some() { + Ok(()) + } else { + Err(RemoveResourceError { + phantom: PhantomData, + }) + } } } #[cfg(test)] #[allow(clippy::float_cmp, clippy::approx_constant)] mod tests { + use crate as bevy_ecs; use crate::{ + bundle::Bundle, component::{ComponentDescriptor, StorageType}, - system::{CommandQueue, Commands}, + entity::Entity, + system::{CommandErrorHandler, CommandQueue, Commands, FallibleCommand}, world::World, }; use std::sync::{ @@ -546,4 +734,157 @@ mod tests { assert!(!world.contains_resource::()); assert!(world.contains_resource::()); } + + struct FailingCommand; + impl FallibleCommand for FailingCommand { + type Error = (); + + fn try_write(self, _: &mut World) -> Result<(), Self::Error> { + Err(()) + } + } + + struct SuccessfulCommand; + impl FallibleCommand for SuccessfulCommand { + type Error = (); + + fn try_write(self, _: &mut World) -> Result<(), Self::Error> { + Ok(()) + } + } + + #[test] + fn test_commands_error_handler() { + let invoked = Arc::new(AtomicUsize::new(0)); + let mut world = World::default(); + let mut queue = CommandQueue::default(); + { + let mut commands = Commands::new(&mut queue, &world); + + commands.insert_resource(42u32); + let invoked_clone = invoked.clone(); + // should succeed + commands.remove_resource::().on_err(move |_, _| { + invoked_clone.fetch_add(1, Ordering::Relaxed); + }); + + let invoked_clone = invoked.clone(); + // should fail + commands.remove_resource::().on_err(move |_, _| { + invoked_clone.fetch_add(1, Ordering::Relaxed); + }); + + let invoked_clone = invoked.clone(); + // should fail + commands.add_fallible(FailingCommand).on_err(move |_, _| { + invoked_clone.fetch_add(1, Ordering::Relaxed); + }); + + let invoked_clone = invoked.clone(); + // should succeed + commands + .add_fallible(SuccessfulCommand) + .on_err(move |_, _| { + invoked_clone.fetch_add(1, Ordering::Relaxed); + }); + } + queue.apply(&mut world); + + assert_eq!(invoked.load(Ordering::Relaxed), 2); + } + + #[test] + fn test_entity_commands_error_handler() { + #[derive(Bundle)] + struct TestBundle { + value: u32, + } + + let invoked = Arc::new(AtomicUsize::new(0)); + + let mut world = World::default(); + + let valid_entity = world.spawn().id(); + let invalid_entity = Entity::new(42); + + let mut queue = CommandQueue::default(); + { + let mut commands = Commands::new(&mut queue, &world); + + // EntityCommands::despawn + let mut try_despawn = |e| { + let invoked_clone = invoked.clone(); + commands.entity(e).despawn().on_err(move |error, _| { + assert_eq!(error.entity, e); + invoked_clone.fetch_add(1, Ordering::Relaxed); + }); + }; + + try_despawn(invalid_entity); + try_despawn(valid_entity); + + // EntityCommands::insert + let invoked_clone = invoked.clone(); + commands + .entity(invalid_entity) + .insert(42) + .on_err(move |error, _| { + assert_eq!(error.entity, invalid_entity); + assert_eq!(error.component, 42); + invoked_clone.fetch_add(1, Ordering::Relaxed); + }); + + // EntityCommands::insert_bundle + let invoked_clone = invoked.clone(); + commands + .entity(invalid_entity) + .insert_bundle(TestBundle { value: 42 }) + .on_err(move |error, _| { + assert_eq!(error.entity, invalid_entity); + assert_eq!(error.bundle.value, 42); + invoked_clone.fetch_add(1, Ordering::Relaxed); + }); + + // EntityCommands::remove + let invoked_clone = invoked.clone(); + commands + .entity(invalid_entity) + .remove::() + .on_err(move |error, _| { + assert_eq!(error.entity, invalid_entity); + invoked_clone.fetch_add(1, Ordering::Relaxed); + }); + + // EntityCommands::remove_resource + let invoked_clone = invoked.clone(); + commands + .entity(invalid_entity) + .remove_bundle::() + .on_err(move |error, _| { + assert_eq!(error.entity, invalid_entity); + invoked_clone.fetch_add(1, Ordering::Relaxed); + }); + } + queue.apply(&mut world); + + assert_eq!(invoked.load(Ordering::Relaxed), 5); + } + + #[test] + #[should_panic] + fn test_panicking_error_handler() { + std::panic::set_hook(Box::new(|_| {})); // prevents printing of stack trace. + + let mut world = World::default(); + let mut queue = CommandQueue::default(); + { + let mut commands = Commands::new(&mut queue, &world); + let invalid_entity = Entity::new(42); + commands + .entity(invalid_entity) + .despawn() + .on_err(CommandErrorHandler::panic); + } + queue.apply(&mut world); + } } diff --git a/examples/README.md b/examples/README.md index e3b8878c136f0..3d0bb113807c2 100644 --- a/examples/README.md +++ b/examples/README.md @@ -157,6 +157,7 @@ Example | File | Description --- | --- | --- `ecs_guide` | [`ecs/ecs_guide.rs`](./ecs/ecs_guide.rs) | Full guide to Bevy's ECS `change_detection` | [`ecs/change_detection.rs`](./ecs/change_detection.rs) | Change detection on components +`command_error_handling` | [`ecs/command_error_handling.rs`](./ecs/command_error_handling.rs) | Error handling fallible commands `event` | [`ecs/event.rs`](./ecs/event.rs) | Illustrates event creation, activation, and reception `fixed_timestep` | [`ecs/fixed_timestep.rs`](./ecs/fixed_timestep.rs) | Shows how to create systems that run every fixed timestep, rather than every tick `hierarchy` | [`ecs/hierarchy.rs`](./ecs/hierarchy.rs) | Creates a hierarchy of parents and children entities diff --git a/examples/ecs/command_error_handling.rs b/examples/ecs/command_error_handling.rs new file mode 100644 index 0000000000000..5459379b158e0 --- /dev/null +++ b/examples/ecs/command_error_handling.rs @@ -0,0 +1,77 @@ +use bevy::{core::FixedTimestep, prelude::*}; + +fn main() { + App::build() + .insert_resource(FailedDespawnAttempts(0)) + .add_startup_system(setup.system()) + .add_system( + remove_components + .system() + .with_run_criteria(FixedTimestep::step(0.5)), + ) + .add_system_set( + SystemSet::new() + .with_run_criteria(FixedTimestep::step(2.0)) + .with_system(despawn_all_entities.system()) + .with_system(log_failed_despawn_attempts.system()), + ) + .run(); +} + +struct A(usize); + +#[derive(Bundle, Default)] +struct B { + value: usize, +} + +struct FailedDespawnAttempts(usize); + +fn setup(mut commands: Commands) { + for i in 0..3 { + // Note that `insert` and `insert_bundle` are fallible functions. + // If no error handler is specified, the default behavior is to log the error, and continue. + // However, these calls to `insert` and `insert_bundle` will not fail, since the entity is valid. + commands.spawn().insert(A(i)).insert_bundle(B::default()); + } +} + +fn log_failed_despawn_attempts(attempts: Res) { + info!("There have been {} failed despawn attempts!", attempts.0); +} + +fn despawn_all_entities(mut commands: Commands, query: Query) { + for e in query.iter() { + // `on_err` also allows you to provide a custom error handler! + commands.entity(e).despawn().on_err(|error, ctx| { + // You'll notice that the `error` will also give you back the entity + // you tried to despawn. + let entity = error.entity; + + warn!("Sadly our entity '{:?}' didn't despawn :(", entity); + + // error handlers have mutable access to `World` + if let Some(mut failed_despawns) = ctx.world.get_resource_mut::() + { + failed_despawns.0 += 1; + } + }); + } +} + +fn remove_components(mut commands: Commands, query: Query) { + for e in query.iter() { + // Some nice things: + // - You can still chain commands! + // - There are a slew of built-in error handlers + commands + .entity(e) + .remove::() + // `CommandErrorHandler::ignore` will neither log nor panic the error + .on_err(CommandErrorHandler::ignore) + .remove_bundle::() + // `CommandErrorHandler::log` is the default behavior, and will log the error. + // `CommandErrorHandler::panic` is another alternative which will panic on the error. + .on_err(CommandErrorHandler::log); + } +}