diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index 7d2f1ab617..4259b6d1f2 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -71,6 +71,57 @@ pub type BoxedCondition = Box>; /// # app.run(&mut world); /// # assert!(world.resource::().0); pub trait Condition: sealed::Condition { + /// Returns a new run condition that only returns `true` + /// if both this one and the passed `and` return `true`. + /// + /// The returned run condition is short-circuiting, meaning + /// `and` will only be invoked if `self` returns `true`. + /// + /// # Examples + /// + /// ```should_panic + /// use bevy_ecs::prelude::*; + /// + /// #[derive(Resource, PartialEq)] + /// struct R(u32); + /// + /// # let mut app = Schedule::default(); + /// # let mut world = World::new(); + /// # fn my_system() {} + /// app.add_systems( + /// // The `resource_equals` run condition will panic since we don't initialize `R`, + /// // just like if we used `Res` in a system. + /// my_system.run_if(resource_equals(R(0))), + /// ); + /// # app.run(&mut world); + /// ``` + /// + /// Use `.and()` to avoid checking the condition. + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Resource, PartialEq)] + /// # struct R(u32); + /// # let mut app = Schedule::default(); + /// # let mut world = World::new(); + /// # fn my_system() {} + /// app.add_systems( + /// // `resource_equals` will only get run if the resource `R` exists. + /// my_system.run_if(resource_exists::.and(resource_equals(R(0)))), + /// ); + /// # app.run(&mut world); + /// ``` + /// + /// Note that in this case, it's better to just use the run condition [`resource_exists_and_equals`]. + /// + /// [`resource_exists_and_equals`]: common_conditions::resource_exists_and_equals + fn and>(self, and: C) -> And { + let a = IntoSystem::into_system(self); + let b = IntoSystem::into_system(and); + let name = format!("{} && {}", a.name(), b.name()); + CombinatorSystem::new(a, b, Cow::Owned(name)) + } + /// Returns a new run condition that only returns `true` /// if both this one and the passed `and_then` return `true`. /// @@ -115,18 +166,122 @@ pub trait Condition: sealed::Condition { /// Note that in this case, it's better to just use the run condition [`resource_exists_and_equals`]. /// /// [`resource_exists_and_equals`]: common_conditions::resource_exists_and_equals - fn and_then>(self, and_then: C) -> AndThen { + #[deprecated( + note = "Users should use the `.and(condition)` method in lieu of `.and_then(condition)`" + )] + fn and_then>(self, and_then: C) -> And { + self.and(and_then) + } + + /// Returns a new run condition that only returns `false` + /// if both this one and the passed `nand` return `true`. + /// + /// The returned run condition is short-circuiting, meaning + /// `nand` will only be invoked if `self` returns `true`. + /// + /// # Examples + /// + /// ```compile_fail + /// use bevy::prelude::*; + /// + /// #[derive(States, Debug, Clone, PartialEq, Eq, Hash)] + /// pub enum PlayerState { + /// Alive, + /// Dead, + /// } + /// + /// #[derive(States, Debug, Clone, PartialEq, Eq, Hash)] + /// pub enum EnemyState { + /// Alive, + /// Dead, + /// } + /// + /// # let mut app = Schedule::default(); + /// # let mut world = World::new(); + /// # fn game_over_credits() {} + /// app.add_systems( + /// // The game_over_credits system will only execute if either the `in_state(PlayerState::Alive)` + /// // run condition or `in_state(EnemyState::Alive)` run condition evaluates to `false`. + /// game_over_credits.run_if( + /// in_state(PlayerState::Alive).nand(in_state(EnemyState::Alive)) + /// ), + /// ); + /// # app.run(&mut world); + /// ``` + /// + /// Equivalent logic can be achieved by using `not` in concert with `and`: + /// + /// ```compile_fail + /// app.add_systems( + /// game_over_credits.run_if( + /// not(in_state(PlayerState::Alive).and(in_state(EnemyState::Alive))) + /// ), + /// ); + /// ``` + fn nand>(self, nand: C) -> Nand { let a = IntoSystem::into_system(self); - let b = IntoSystem::into_system(and_then); - let name = format!("{} && {}", a.name(), b.name()); + let b = IntoSystem::into_system(nand); + let name = format!("!({} && {})", a.name(), b.name()); + CombinatorSystem::new(a, b, Cow::Owned(name)) + } + + /// Returns a new run condition that only returns `true` + /// if both this one and the passed `nor` return `false`. + /// + /// The returned run condition is short-circuiting, meaning + /// `nor` will only be invoked if `self` returns `false`. + /// + /// # Examples + /// + /// ```compile_fail + /// use bevy::prelude::*; + /// + /// #[derive(States, Debug, Clone, PartialEq, Eq, Hash)] + /// pub enum WeatherState { + /// Sunny, + /// Cloudy, + /// } + /// + /// #[derive(States, Debug, Clone, PartialEq, Eq, Hash)] + /// pub enum SoilState { + /// Fertilized, + /// NotFertilized, + /// } + /// + /// # let mut app = Schedule::default(); + /// # let mut world = World::new(); + /// # fn slow_plant_growth() {} + /// app.add_systems( + /// // The slow_plant_growth system will only execute if both the `in_state(WeatherState::Sunny)` + /// // run condition and `in_state(SoilState::Fertilized)` run condition evaluate to `false`. + /// slow_plant_growth.run_if( + /// in_state(WeatherState::Sunny).nor(in_state(SoilState::Fertilized)) + /// ), + /// ); + /// # app.run(&mut world); + /// ``` + /// + /// Equivalent logic can be achieved by using `not` in concert with `or`: + /// + /// ```compile_fail + /// app.add_systems( + /// slow_plant_growth.run_if( + /// not(in_state(WeatherState::Sunny).or(in_state(SoilState::Fertilized))) + /// ), + /// ); + /// ``` + fn nor>(self, nor: C) -> Nor { + let a = IntoSystem::into_system(self); + let b = IntoSystem::into_system(nor); + let name = format!("!({} || {})", a.name(), b.name()); CombinatorSystem::new(a, b, Cow::Owned(name)) } /// Returns a new run condition that returns `true` - /// if either this one or the passed `or_else` return `true`. + /// if either this one or the passed `or` return `true`. /// /// The returned run condition is short-circuiting, meaning - /// `or_else` will only be invoked if `self` returns `false`. + /// `or` will only be invoked if `self` returns `false`. /// /// # Examples /// @@ -145,7 +300,7 @@ pub trait Condition: sealed::Condition { /// # fn my_system(mut c: ResMut) { c.0 = true; } /// app.add_systems( /// // Only run the system if either `A` or `B` exist. - /// my_system.run_if(resource_exists::.or_else(resource_exists::)), + /// my_system.run_if(resource_exists::.or(resource_exists::)), /// ); /// # /// # world.insert_resource(C(false)); @@ -162,12 +317,153 @@ pub trait Condition: sealed::Condition { /// # app.run(&mut world); /// # assert!(world.resource::().0); /// ``` - fn or_else>(self, or_else: C) -> OrElse { + fn or>(self, or: C) -> Or { let a = IntoSystem::into_system(self); - let b = IntoSystem::into_system(or_else); + let b = IntoSystem::into_system(or); let name = format!("{} || {}", a.name(), b.name()); CombinatorSystem::new(a, b, Cow::Owned(name)) } + + /// Returns a new run condition that returns `true` + /// if either this one or the passed `or` return `true`. + /// + /// The returned run condition is short-circuiting, meaning + /// `or` will only be invoked if `self` returns `false`. + /// + /// # Examples + /// + /// ``` + /// use bevy_ecs::prelude::*; + /// + /// #[derive(Resource, PartialEq)] + /// struct A(u32); + /// + /// #[derive(Resource, PartialEq)] + /// struct B(u32); + /// + /// # let mut app = Schedule::default(); + /// # let mut world = World::new(); + /// # #[derive(Resource)] struct C(bool); + /// # fn my_system(mut c: ResMut) { c.0 = true; } + /// app.add_systems( + /// // Only run the system if either `A` or `B` exist. + /// my_system.run_if(resource_exists::.or(resource_exists::)), + /// ); + /// # + /// # world.insert_resource(C(false)); + /// # app.run(&mut world); + /// # assert!(!world.resource::().0); + /// # + /// # world.insert_resource(A(0)); + /// # app.run(&mut world); + /// # assert!(world.resource::().0); + /// # + /// # world.remove_resource::(); + /// # world.insert_resource(B(0)); + /// # world.insert_resource(C(false)); + /// # app.run(&mut world); + /// # assert!(world.resource::().0); + /// ``` + #[deprecated( + note = "Users should use the `.or(condition)` method in lieu of `.or_else(condition)`" + )] + fn or_else>(self, or_else: C) -> Or { + self.or(or_else) + } + + /// Returns a new run condition that only returns `true` + /// if `self` and `xnor` **both** return `false` or **both** return `true`. + /// + /// # Examples + /// + /// ```compile_fail + /// use bevy::prelude::*; + /// + /// #[derive(States, Debug, Clone, PartialEq, Eq, Hash)] + /// pub enum CoffeeMachineState { + /// Heating, + /// Brewing, + /// Inactive, + /// } + /// + /// #[derive(States, Debug, Clone, PartialEq, Eq, Hash)] + /// pub enum TeaKettleState { + /// Heating, + /// Steeping, + /// Inactive, + /// } + /// + /// # let mut app = Schedule::default(); + /// # let mut world = World::new(); + /// # fn take_drink_orders() {} + /// app.add_systems( + /// // The take_drink_orders system will only execute if the `in_state(CoffeeMachineState::Inactive)` + /// // run condition and `in_state(TeaKettleState::Inactive)` run conditions both evaluate to `false`, + /// // or both evaluate to `true`. + /// take_drink_orders.run_if( + /// in_state(CoffeeMachineState::Inactive).xnor(in_state(TeaKettleState::Inactive)) + /// ), + /// ); + /// # app.run(&mut world); + /// ``` + /// + /// Equivalent logic can be achieved by using `not` in concert with `xor`: + /// + /// ```compile_fail + /// app.add_systems( + /// take_drink_orders.run_if( + /// not(in_state(CoffeeMachineState::Inactive).xor(in_state(TeaKettleState::Inactive))) + /// ), + /// ); + /// ``` + fn xnor>(self, xnor: C) -> Xnor { + let a = IntoSystem::into_system(self); + let b = IntoSystem::into_system(xnor); + let name = format!("!({} ^ {})", a.name(), b.name()); + CombinatorSystem::new(a, b, Cow::Owned(name)) + } + + /// Returns a new run condition that only returns `true` + /// if either `self` or `xor` return `true`, but not both. + /// + /// # Examples + /// + /// ```compile_fail + /// use bevy::prelude::*; + /// + /// #[derive(States, Debug, Clone, PartialEq, Eq, Hash)] + /// pub enum CoffeeMachineState { + /// Heating, + /// Brewing, + /// Inactive, + /// } + /// + /// #[derive(States, Debug, Clone, PartialEq, Eq, Hash)] + /// pub enum TeaKettleState { + /// Heating, + /// Steeping, + /// Inactive, + /// } + /// + /// # let mut app = Schedule::default(); + /// # let mut world = World::new(); + /// # fn prepare_beverage() {} + /// app.add_systems( + /// // The prepare_beverage system will only execute if either the `in_state(CoffeeMachineState::Inactive)` + /// // run condition or `in_state(TeaKettleState::Inactive)` run condition evaluates to `true`, + /// // but not both. + /// prepare_beverage.run_if( + /// in_state(CoffeeMachineState::Inactive).xor(in_state(TeaKettleState::Inactive)) + /// ), + /// ); + /// # app.run(&mut world); + /// ``` + fn xor>(self, xor: C) -> Xor { + let a = IntoSystem::into_system(self); + let b = IntoSystem::into_system(xor); + let name = format!("({} ^ {})", a.name(), b.name()); + CombinatorSystem::new(a, b, Cow::Owned(name)) + } } impl Condition for F where F: sealed::Condition {} @@ -434,7 +730,7 @@ pub mod common_conditions { /// // By default detecting changes will also trigger if the resource was /// // just added, this won't work with my example so I will add a second /// // condition to make sure the resource wasn't just added - /// .and_then(not(resource_added::)) + /// .and(not(resource_added::)) /// ), /// ); /// @@ -487,7 +783,7 @@ pub mod common_conditions { /// // By default detecting changes will also trigger if the resource was /// // just added, this won't work with my example so I will add a second /// // condition to make sure the resource wasn't just added - /// .and_then(not(resource_added::)) + /// .and(not(resource_added::)) /// ), /// ); /// @@ -549,7 +845,7 @@ pub mod common_conditions { /// // By default detecting changes will also trigger if the resource was /// // just added, this won't work with my example so I will add a second /// // condition to make sure the resource wasn't just added - /// .and_then(not(resource_added::)) + /// .and(not(resource_added::)) /// ), /// ); /// @@ -809,15 +1105,27 @@ where } /// Combines the outputs of two systems using the `&&` operator. -pub type AndThen = CombinatorSystem; +pub type And = CombinatorSystem; + +/// Combines and inverts the outputs of two systems using the `&&` and `!` operators. +pub type Nand = CombinatorSystem; + +/// Combines and inverts the outputs of two systems using the `&&` and `!` operators. +pub type Nor = CombinatorSystem; /// Combines the outputs of two systems using the `||` operator. -pub type OrElse = CombinatorSystem; +pub type Or = CombinatorSystem; + +/// Combines and inverts the outputs of two systems using the `^` and `!` operators. +pub type Xnor = CombinatorSystem; + +/// Combines the outputs of two systems using the `^` operator. +pub type Xor = CombinatorSystem; #[doc(hidden)] -pub struct AndThenMarker; +pub struct AndMarker; -impl Combine for AndThenMarker +impl Combine for AndMarker where In: Copy, A: System, @@ -836,9 +1144,51 @@ where } #[doc(hidden)] -pub struct OrElseMarker; +pub struct NandMarker; -impl Combine for OrElseMarker +impl Combine for NandMarker +where + In: Copy, + A: System, + B: System, +{ + type In = In; + type Out = bool; + + fn combine( + input: Self::In, + a: impl FnOnce(::In) -> ::Out, + b: impl FnOnce(::In) -> ::Out, + ) -> Self::Out { + !(a(input) && b(input)) + } +} + +#[doc(hidden)] +pub struct NorMarker; + +impl Combine for NorMarker +where + In: Copy, + A: System, + B: System, +{ + type In = In; + type Out = bool; + + fn combine( + input: Self::In, + a: impl FnOnce(::In) -> ::Out, + b: impl FnOnce(::In) -> ::Out, + ) -> Self::Out { + !(a(input) || b(input)) + } +} + +#[doc(hidden)] +pub struct OrMarker; + +impl Combine for OrMarker where In: Copy, A: System, @@ -856,6 +1206,48 @@ where } } +#[doc(hidden)] +pub struct XnorMarker; + +impl Combine for XnorMarker +where + In: Copy, + A: System, + B: System, +{ + type In = In; + type Out = bool; + + fn combine( + input: Self::In, + a: impl FnOnce(::In) -> ::Out, + b: impl FnOnce(::In) -> ::Out, + ) -> Self::Out { + !(a(input) ^ b(input)) + } +} + +#[doc(hidden)] +pub struct XorMarker; + +impl Combine for XorMarker +where + In: Copy, + A: System, + B: System, +{ + type In = In; + type Out = bool; + + fn combine( + input: Self::In, + a: impl FnOnce(::In) -> ::Out, + b: impl FnOnce(::In) -> ::Out, + ) -> Self::Out { + a(input) ^ b(input) + } +} + #[cfg(test)] mod tests { use super::{common_conditions::*, Condition}; @@ -874,6 +1266,10 @@ mod tests { counter.0 += 1; } + fn double_counter(mut counter: ResMut) { + counter.0 *= 2; + } + fn every_other_time(mut has_ran: Local) -> bool { *has_ran = !*has_ran; *has_ran @@ -907,20 +1303,32 @@ mod tests { } #[test] + #[allow(deprecated)] fn run_condition_combinators() { let mut world = World::new(); world.init_resource::(); let mut schedule = Schedule::default(); - // Always run - schedule.add_systems(increment_counter.run_if(every_other_time.or_else(|| true))); - // Run every other cycle - schedule.add_systems(increment_counter.run_if(every_other_time.and_then(|| true))); + schedule.add_systems( + ( + increment_counter.run_if(every_other_time.and(|| true)), // Run every odd cycle. + increment_counter.run_if(every_other_time.and_then(|| true)), // Run every odd cycle. + increment_counter.run_if(every_other_time.nand(|| false)), // Always run. + double_counter.run_if(every_other_time.nor(|| false)), // Run every even cycle. + increment_counter.run_if(every_other_time.or(|| true)), // Always run. + increment_counter.run_if(every_other_time.or_else(|| true)), // Always run. + increment_counter.run_if(every_other_time.xnor(|| true)), // Run every odd cycle. + double_counter.run_if(every_other_time.xnor(|| false)), // Run every even cycle. + increment_counter.run_if(every_other_time.xor(|| false)), // Run every odd cycle. + double_counter.run_if(every_other_time.xor(|| true)), // Run every even cycle. + ) + .chain(), + ); schedule.run(&mut world); - assert_eq!(world.resource::().0, 2); + assert_eq!(world.resource::().0, 7); schedule.run(&mut world); - assert_eq!(world.resource::().0, 3); + assert_eq!(world.resource::().0, 72); } #[test] diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index c06e71e159..e0a9d6715e 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -1682,7 +1682,7 @@ mod tests { res.0 += 2; }, ) - .distributive_run_if(resource_exists::.or_else(resource_exists::)), + .distributive_run_if(resource_exists::.or(resource_exists::)), ); sched.initialize(&mut world).unwrap(); sched.run(&mut world); diff --git a/crates/bevy_state/src/condition.rs b/crates/bevy_state/src/condition.rs index c0ff5abe49..ddca7b3a54 100644 --- a/crates/bevy_state/src/condition.rs +++ b/crates/bevy_state/src/condition.rs @@ -197,7 +197,7 @@ mod tests { Schedule::default().add_systems( (test_system, test_system) .distributive_run_if(state_exists::) - .distributive_run_if(in_state(TestState::A).or_else(in_state(TestState::B))) + .distributive_run_if(in_state(TestState::A).or(in_state(TestState::B))) .distributive_run_if(state_changed::), ); } diff --git a/examples/ecs/run_conditions.rs b/examples/ecs/run_conditions.rs index b2fbea9176..42d828e81c 100644 --- a/examples/ecs/run_conditions.rs +++ b/examples/ecs/run_conditions.rs @@ -18,11 +18,11 @@ fn main() { // The common_conditions module has a few useful run conditions // for checking resources and states. These are included in the prelude. .run_if(resource_exists::) - // `.or_else()` is a run condition combinator that only evaluates the second condition + // `.or()` is a run condition combinator that only evaluates the second condition // if the first condition returns `false`. This behavior is known as "short-circuiting", // and is how the `||` operator works in Rust (as well as most C-family languages). // In this case, the `has_user_input` run condition will be evaluated since the `Unused` resource has not been initialized. - .run_if(resource_exists::.or_else( + .run_if(resource_exists::.or( // This is a custom run condition, defined using a system that returns // a `bool` and which has read-only `SystemParam`s. // Both run conditions must return `true` in order for the system to run. @@ -30,11 +30,11 @@ fn main() { has_user_input, )), print_input_counter - // `.and_then()` is a run condition combinator that only evaluates the second condition + // `.and()` is a run condition combinator that only evaluates the second condition // if the first condition returns `true`, analogous to the `&&` operator. // In this case, the short-circuiting behavior prevents the second run condition from // panicking if the `InputCounter` resource has not been initialized. - .run_if(resource_exists::.and_then( + .run_if(resource_exists::.and( // This is a custom run condition in the form of a closure. // This is useful for small, simple run conditions you don't need to reuse. // All the normal rules still apply: all parameters must be read only except for local parameters. diff --git a/examples/math/render_primitives.rs b/examples/math/render_primitives.rs index 285c71ef9a..0d34d4ad7c 100644 --- a/examples/math/render_primitives.rs +++ b/examples/math/render_primitives.rs @@ -44,9 +44,8 @@ fn main() { switch_to_previous_primitive.run_if(input_just_pressed(KeyCode::ArrowDown)), draw_gizmos_2d.run_if(in_mode(CameraActive::Dim2)), draw_gizmos_3d.run_if(in_mode(CameraActive::Dim3)), - update_primitive_meshes.run_if( - state_changed::.or_else(state_changed::), - ), + update_primitive_meshes + .run_if(state_changed::.or(state_changed::)), rotate_primitive_2d_meshes, rotate_primitive_3d_meshes, ),