Files
bevy/examples/ecs/run_conditions.rs
Joseph 12952486ef fix: improve semantic clarity for run condition combinators (#22690)
# 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>
2026-02-17 00:13:33 +00:00

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");
}