mirror of
https://github.com/bevyengine/bevy.git
synced 2026-05-06 06:06:42 -04:00
12952486ef
# Objective We include several run condition combinators, such as `and`, `or`, etc., which short-circuit depending on the output of the first condition in the combinator. This is incredibly error-prone due to the subtle way that short-circuiting interacts with change detection -- rather than reacting to changes frame-by-frame, the second condition in short-circuiting combinator will react to _the last time that the first condition did not short circuit_. This can easily lead to confusing bugs if the user does not expect this, and I suspect that most users will not expect this. For this reason, when combining multiple run conditions added via `.run_if()`, all run conditions are intentionally eagerly evaluated. ## Solution Add new run condition combinators `and_then`, `and_eager`, `or_else`, `or_eager`, etc., for clarity, and deprecate the previous methods, pointing users to the new ones. After the previous combinators have been removed for a few release cycles, we should consider renaming combinators such as `and_eager` to simply `and`. # Migration Guide Bevy supports run condition combinators (`and`, `or`, `nan`, `nor`), which have historically short-circuited. While familiar, short-circuiting interacts with Bevy’s change detection in a subtle way: when the left-hand condition short-circuits, the right-hand condition is not evaluated and therefore does not observe changes on that frame. Instead, it reacts based on the last frame it ran, which can lead to confusing and non-local bugs. By contrast, Bevy's scheduler combines multiple .run_if(...) conditions using eager evaluation, which avoids this known pitfall. To make intent explicit and reduce footguns, short-circuiting combinators have been renamed and eagerly-evaluated variants have been added. ## Examples Most users should use eager evaluation, which ensures all conditions participate in change detection every frame: ```rust // Before (deprecated) cond_a.and(cond_b) cond_a.or(cond_b) cond_a.nand(cond_b) cond_a.nor(cond_b) // After (recommended default) cond_a.and_eager(cond_b) cond_a.or_eager(cond_b) cond_a.nand_eager(cond_b) cond_a.nor_eager(cond_b) ``` If you *intentionally rely on short-circuiting* for correctness, use the explicit short-circuiting variants: ```rust // Explicit short-circuiting cond_a.and_then(cond_b) cond_a.or_else(cond_b) cond_a.nand_then(cond_b) cond_a.nor_else(cond_b) ``` `xor` and `xnor` are unchanged, as they cannot short-circuit by nature. ## Future naming note The `_eager` suffix exists to ease migration without changing the behavior of existing code that relied on short-circuiting. After the deprecated combinators have been removed for a few release cycles, we expect to revisit naming and likely remove the _eager suffix, keeping `and_then` / `or_else` as the explicit short-circuiting forms. --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Mike <mike.hsu@gmail.com>
108 lines
5.1 KiB
Rust
108 lines
5.1 KiB
Rust
//! This example demonstrates how to use run conditions to control when systems run.
|
|
|
|
use bevy::prelude::*;
|
|
|
|
fn main() {
|
|
println!();
|
|
println!("For the first 2 seconds you will not be able to increment the counter");
|
|
println!("Once that time has passed you can press space, enter, left mouse, right mouse or touch the screen to increment the counter");
|
|
println!();
|
|
|
|
App::new()
|
|
.add_plugins(DefaultPlugins)
|
|
.init_resource::<InputCounter>()
|
|
.add_systems(
|
|
Update,
|
|
(
|
|
increment_input_counter
|
|
// 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::<InputCounter>)
|
|
// `.or_else()` 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::<Unused>.or_else(
|
|
// This is a custom run condition, defined using a system that returns
|
|
// a `bool` and which has read-only `SystemParam`s.
|
|
// Only a single run condition must return `true` in order for the system to run.
|
|
has_user_input,
|
|
)),
|
|
print_input_counter
|
|
// `.and_then()` 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::<InputCounter>.and_then(
|
|
// 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.
|
|
|counter: Res<InputCounter>| counter.is_changed() && !counter.is_added(),
|
|
)),
|
|
print_time_message
|
|
// This function returns a custom run condition, much like the common conditions module.
|
|
// It will only return true once 2 seconds have passed.
|
|
.run_if(time_passed(2.0))
|
|
// You can use the `not` condition from the common_conditions module
|
|
// to inverse a run condition. In this case it will return true if
|
|
// less than 2.5 seconds have elapsed since the app started.
|
|
.run_if(not(time_passed(2.5))),
|
|
),
|
|
)
|
|
.run();
|
|
}
|
|
|
|
#[derive(Resource, Default)]
|
|
struct InputCounter(usize);
|
|
|
|
#[derive(Resource)]
|
|
struct Unused;
|
|
|
|
/// Return true if any of the defined inputs were just pressed.
|
|
///
|
|
/// This is a custom run condition, it can take any normal system parameters as long as
|
|
/// they are read only (except for local parameters which can be mutable).
|
|
/// It returns a bool which determines if the system should run.
|
|
fn has_user_input(
|
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
|
mouse_button_input: Res<ButtonInput<MouseButton>>,
|
|
touch_input: Res<Touches>,
|
|
) -> bool {
|
|
keyboard_input.just_pressed(KeyCode::Space)
|
|
|| keyboard_input.just_pressed(KeyCode::Enter)
|
|
|| mouse_button_input.just_pressed(MouseButton::Left)
|
|
|| mouse_button_input.just_pressed(MouseButton::Right)
|
|
|| touch_input.any_just_pressed()
|
|
}
|
|
|
|
/// This is a function that returns a closure which can be used as a run condition.
|
|
///
|
|
/// This is useful because you can reuse the same run condition but with different variables.
|
|
/// This is how the common conditions module works.
|
|
fn time_passed(t: f32) -> impl FnMut(Local<f32>, Res<Time>) -> bool {
|
|
move |mut timer: Local<f32>, time: Res<Time>| {
|
|
// Tick the timer
|
|
*timer += time.delta_secs();
|
|
// Return true if the timer has passed the time
|
|
*timer >= t
|
|
}
|
|
}
|
|
|
|
/// SYSTEM: Increment the input counter
|
|
/// Notice how we can take just the `ResMut` and not have to wrap
|
|
/// it in an option in case it hasn't been initialized, this is because
|
|
/// it has a run condition that checks if the `InputCounter` resource exists
|
|
fn increment_input_counter(mut counter: ResMut<InputCounter>) {
|
|
counter.0 += 1;
|
|
}
|
|
|
|
/// SYSTEM: Print the input counter
|
|
fn print_input_counter(counter: Res<InputCounter>) {
|
|
println!("Input counter: {}", counter.0);
|
|
}
|
|
|
|
/// SYSTEM: Adds the input counter resource
|
|
fn print_time_message() {
|
|
println!("It has been more than 2 seconds since the program started and less than 2.5 seconds");
|
|
}
|