
# Objective When introduced, `Single` was intended to simply be silently skipped, allowing for graceful and efficient handling of systems during invalid game states (such as when the player is dead). However, this also caused missing resources to *also* be silently skipped, leading to confusing and very hard to debug failures. In 0.15.1, this behavior was reverted to a panic, making missing resources easier to debug, but largely making `Single` (and `Populated`) worthless, as they would panic during expected game states. Ultimately, the consensus is that this behavior should differ on a per-system-param basis. However, there was no sensible way to *do* that before this PR. ## Solution Swap `SystemParam::validate_param` from a `bool` to: ```rust /// The outcome of system / system param validation, /// used by system executors to determine what to do with a system. pub enum ValidationOutcome { /// All system parameters were validated successfully and the system can be run. Valid, /// At least one system parameter failed validation, and an error must be handled. /// By default, this will result in1 a panic. See [crate::error] for more information. /// /// This is the default behavior, and is suitable for system params that should *always* be valid, /// either because sensible fallback behavior exists (like [`Query`] or because /// failures in validation should be considered a bug in the user's logic that must be immediately addressed (like [`Res`]). Invalid, /// At least one system parameter failed validation, but the system should be skipped due to [`ValidationBehavior::Skip`]. /// This is suitable for system params that are intended to only operate in certain application states, such as [`Single`]. Skipped, } ``` Then, inside of each `SystemParam` implementation, return either Valid, Invalid or Skipped. Currently, only `Single`, `Option<Single>` and `Populated` use the `Skipped` behavior. Other params (like resources) retain their current failing ## Testing Messed around with the fallible_params example. Added a pair of tests: one for panicking when resources are missing, and another for properly skipping `Single` and `Populated` system params. ## To do - [x] get https://github.com/bevyengine/bevy/pull/18454 merged - [x] fix the todo!() in the macro-powered tuple implementation (please help 🥺) - [x] test - [x] write a migration guide - [x] update the example comments ## Migration Guide Various system and system parameter validation methods (`SystemParam::validate_param`, `System::validate_param` and `System::validate_param_unsafe`) now return and accept a `ValidationOutcome` enum, rather than a `bool`. The previous `true` values map to `ValidationOutcome::Valid`, while `false` maps to `ValidationOutcome::Invalid`. However, if you wrote a custom schedule executor, you should now respect the new `ValidationOutcome::Skipped` parameter, skipping any systems whose validation was skipped. By contrast, `ValidationOutcome::Invalid` systems should also be skipped, but you should call the `default_error_handler` on them first, which by default will result in a panic. If you are implementing a custom `SystemParam`, you should consider whether failing system param validation is an error or an expected state, and choose between `Invalid` and `Skipped` accordingly. In Bevy itself, `Single` and `Populated` now once again skip the system when their conditions are not met. This is the 0.15.0 behavior, but stands in contrast to the 0.15.1 behavior, where they would panic. --------- Co-authored-by: MiniaczQ <xnetroidpl@gmail.com> Co-authored-by: Dmytro Banin <banind@cs.washington.edu> Co-authored-by: Chris Russell <8494645+chescock@users.noreply.github.com>
169 lines
6.8 KiB
Rust
169 lines
6.8 KiB
Rust
//! This example demonstrates how fallible parameters can prevent their systems
|
|
//! from running if their acquiry conditions aren't met.
|
|
//!
|
|
//! Fallible system parameters include:
|
|
//! - [`Res<R>`], [`ResMut<R>`] - Resource has to exist, and the [`GLOBAL_ERROR_HANDLER`] will be called if it doesn't.
|
|
//! - [`Single<D, F>`] - There must be exactly one matching entity, but the system will be silently skipped otherwise.
|
|
//! - [`Option<Single<D, F>>`] - There must be zero or one matching entity. The system will be silently skipped if there are more.
|
|
//! - [`Populated<D, F>`] - There must be at least one matching entity, but the system will be silently skipped otherwise.
|
|
//!
|
|
//! Other system parameters, such as [`Query`], will never fail validation: returning a query with no matching entities is valid.
|
|
//!
|
|
//! The result of failed system parameter validation is determined by the [`ValidationOutcome`] returned
|
|
//! by [`SystemParam::validate_param`] for each system parameter.
|
|
//! Each system will pass, fail, or skip based on the joint outcome of all its parameters,
|
|
//! according to the rules defined in [`ValidationOutcome::combine`].
|
|
//!
|
|
//! To learn more about setting the fallback behavior for [`ValidationOutcome`] failures,
|
|
//! please see the `error_handling.rs` example.
|
|
//!
|
|
//! [`ValidationOutcome`]: bevy::ecs::system::ValidationOutcome
|
|
//! [`ValidationOutcome::combine`]: bevy::ecs::system::ValidationOutcome::combine
|
|
//! [`SystemParam::validate_param`]: bevy::ecs::system::SystemParam::validate_param
|
|
|
|
use bevy::ecs::error::{warn, GLOBAL_ERROR_HANDLER};
|
|
use bevy::prelude::*;
|
|
use rand::Rng;
|
|
|
|
fn main() {
|
|
// By default, if a parameter fail to be fetched,
|
|
// the `GLOBAL_ERROR_HANDLER` will be used to handle the error,
|
|
// which by default is set to panic.
|
|
GLOBAL_ERROR_HANDLER
|
|
.set(warn)
|
|
.expect("The error handler can only be set once, globally.");
|
|
|
|
println!();
|
|
println!("Press 'A' to add enemy ships and 'R' to remove them.");
|
|
println!("Player ship will wait for enemy ships and track one if it exists,");
|
|
println!("but will stop tracking if there are more than one.");
|
|
println!();
|
|
|
|
App::new()
|
|
.add_plugins(DefaultPlugins)
|
|
.add_systems(Startup, setup)
|
|
.add_systems(Update, (user_input, move_targets, track_targets).chain())
|
|
// This system will always fail validation, because we never create an entity with both `Player` and `Enemy` components.
|
|
.add_systems(Update, do_nothing_fail_validation)
|
|
.run();
|
|
}
|
|
|
|
/// Enemy component stores data for movement in a circle.
|
|
#[derive(Component, Default)]
|
|
struct Enemy {
|
|
origin: Vec2,
|
|
radius: f32,
|
|
rotation: f32,
|
|
rotation_speed: f32,
|
|
}
|
|
|
|
/// Player component stores data for going after enemies.
|
|
#[derive(Component, Default)]
|
|
struct Player {
|
|
speed: f32,
|
|
rotation_speed: f32,
|
|
min_follow_radius: f32,
|
|
}
|
|
|
|
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|
// Spawn 2D camera.
|
|
commands.spawn(Camera2d);
|
|
|
|
// Spawn player.
|
|
let texture = asset_server.load("textures/simplespace/ship_C.png");
|
|
commands.spawn((
|
|
Player {
|
|
speed: 100.0,
|
|
rotation_speed: 2.0,
|
|
min_follow_radius: 50.0,
|
|
},
|
|
Sprite {
|
|
image: texture,
|
|
color: bevy::color::palettes::tailwind::BLUE_800.into(),
|
|
..Default::default()
|
|
},
|
|
Transform::from_translation(Vec3::ZERO),
|
|
));
|
|
}
|
|
|
|
/// System that reads user input.
|
|
/// If user presses 'A' we spawn a new random enemy.
|
|
/// If user presses 'R' we remove a random enemy (if any exist).
|
|
fn user_input(
|
|
mut commands: Commands,
|
|
enemies: Query<Entity, With<Enemy>>,
|
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
|
asset_server: Res<AssetServer>,
|
|
) {
|
|
let mut rng = rand::thread_rng();
|
|
if keyboard_input.just_pressed(KeyCode::KeyA) {
|
|
let texture = asset_server.load("textures/simplespace/enemy_A.png");
|
|
commands.spawn((
|
|
Enemy {
|
|
origin: Vec2::new(rng.gen_range(-200.0..200.0), rng.gen_range(-200.0..200.0)),
|
|
radius: rng.gen_range(50.0..150.0),
|
|
rotation: rng.gen_range(0.0..std::f32::consts::TAU),
|
|
rotation_speed: rng.gen_range(0.5..1.5),
|
|
},
|
|
Sprite {
|
|
image: texture,
|
|
color: bevy::color::palettes::tailwind::RED_800.into(),
|
|
..default()
|
|
},
|
|
Transform::from_translation(Vec3::ZERO),
|
|
));
|
|
}
|
|
|
|
if keyboard_input.just_pressed(KeyCode::KeyR) {
|
|
if let Some(entity) = enemies.iter().next() {
|
|
commands.entity(entity).despawn();
|
|
}
|
|
}
|
|
}
|
|
|
|
// System that moves the enemies in a circle.
|
|
// Only runs if there are enemies, due to the `Populated` parameter.
|
|
fn move_targets(mut enemies: Populated<(&mut Transform, &mut Enemy)>, time: Res<Time>) {
|
|
for (mut transform, mut target) in &mut *enemies {
|
|
target.rotation += target.rotation_speed * time.delta_secs();
|
|
transform.rotation = Quat::from_rotation_z(target.rotation);
|
|
let offset = transform.right() * target.radius;
|
|
transform.translation = target.origin.extend(0.0) + offset;
|
|
}
|
|
}
|
|
|
|
/// System that moves the player, causing them to track a single enemy.
|
|
/// The player will search for enemies if there are none.
|
|
/// If there is one, player will track it.
|
|
/// If there are too many enemies, the player will cease all action (the system will not run).
|
|
fn track_targets(
|
|
// `Single` ensures the system runs ONLY when exactly one matching entity exists.
|
|
mut player: Single<(&mut Transform, &Player)>,
|
|
// `Option<Single>` ensures that the system runs ONLY when zero or one matching entity exists.
|
|
enemy: Option<Single<&Transform, (With<Enemy>, Without<Player>)>>,
|
|
time: Res<Time>,
|
|
) {
|
|
let (player_transform, player) = &mut *player;
|
|
if let Some(enemy_transform) = enemy {
|
|
// Enemy found, rotate and move towards it.
|
|
let delta = enemy_transform.translation - player_transform.translation;
|
|
let distance = delta.length();
|
|
let front = delta / distance;
|
|
let up = Vec3::Z;
|
|
let side = front.cross(up);
|
|
player_transform.rotation = Quat::from_mat3(&Mat3::from_cols(side, front, up));
|
|
let max_step = distance - player.min_follow_radius;
|
|
if 0.0 < max_step {
|
|
let velocity = (player.speed * time.delta_secs()).min(max_step);
|
|
player_transform.translation += front * velocity;
|
|
}
|
|
} else {
|
|
// 0 or multiple enemies found, keep searching.
|
|
player_transform.rotate_axis(Dir3::Z, player.rotation_speed * time.delta_secs());
|
|
}
|
|
}
|
|
|
|
/// This system always fails param validation, because we never
|
|
/// create an entity with both [`Player`] and [`Enemy`] components.
|
|
fn do_nothing_fail_validation(_: Single<(), (With<Player>, With<Enemy>)>) {}
|