
There are two related problems here: 1. Users should be able to change the fallback behavior of *all* ECS-based errors in their application by setting the `GLOBAL_ERROR_HANDLER`. See #18351 for earlier work in this vein. 2. The existing solution (#15500) for customizing this behavior is high on boilerplate, not global and adds a great deal of complexity. The consensus is that the default behavior when a parameter fails validation should be set based on the kind of system parameter in question: `Single` / `Populated` should silently skip the system, but `Res` should panic. Setting this behavior at the system level is a bandaid that makes getting to that ideal behavior more painful, and can mask real failures (if a resource is missing but you've ignored a system to make the Single stop panicking you're going to have a bad day). I've removed the existing `ParamWarnPolicy`-based configuration, and wired up the `GLOBAL_ERROR_HANDLER`/`default_error_handler` to the various schedule executors to properly plumb through errors . Additionally, I've done a small cleanup pass on the corresponding example. I've run the `fallible_params` example, with both the default and a custom global error handler. The former panics (as expected), and the latter spams the error console with warnings 🥲 1. Currently, failed system param validation will result in endless console spam. Do you want me to implement a solution for warn_once-style debouncing somehow? 2. Currently, the error reporting for failed system param validation is very limited: all we get is that a system param failed validation and the name of the system. Do you want me to implement improved error reporting by bubbling up errors in this PR? 3. There is broad consensus that the default behavior for failed system param validation should be set on a per-system param basis. Would you like me to implement that in this PR? My gut instinct is that we absolutely want to solve 2 and 3, but it will be much easier to do that work (and review it) if we split the PRs apart. `ParamWarnPolicy` and the `WithParamWarnPolicy` have been removed completely. Failures during system param validation are now handled via the `GLOBAL_ERROR_HANDLER`: please see the `bevy_ecs::error` module docs for more information. --------- Co-authored-by: MiniaczQ <xnetroidpl@gmail.com>
158 lines
5.8 KiB
Rust
158 lines
5.8 KiB
Rust
//! This example demonstrates how fallible parameters can prevent their systems
|
|
//! from running if their acquiry conditions aren't met.
|
|
//!
|
|
//! Fallible parameters include:
|
|
//! - [`Res<R>`], [`ResMut<R>`] - Resource has to exist.
|
|
//! - [`Single<D, F>`] - There must be exactly one matching entity.
|
|
//! - [`Option<Single<D, F>>`] - There must be zero or one matching entity.
|
|
//! - [`Populated<D, F>`] - There must be at least one matching entity.
|
|
//!
|
|
//! To learn more about setting the fallback behavior for when a parameter fails to be fetched,
|
|
//! please see the `error_handling.rs` example.
|
|
|
|
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.
|
|
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>)>) {}
|