mirror of
https://github.com/bevyengine/bevy.git
synced 2026-07-01 00:05:45 -04:00
18d106d26a
# Objective Currently, a panic (whether from engine or user code, as there is little distinction) takes down the entire app with it. Instead the user should be able to decide how the error is handled. This is currently not possible except by writing your own executor and setting it for all relevant schedules. See for comparison Godot's policy on exceptions: > ### [Why does Godot not use exceptions?](https://docs.godotengine.org/en/stable/about/faq.html#why-does-godot-not-use-exceptions) > > We believe games should not crash, no matter what. If an unexpected situation happens, Godot > will print an error (which can be traced even to script), but then it will try to recover as > gracefully as possible and keep going. Unity will also log an error and then continue if user code throws an exception. I believe Unreal does too for exceptions coming from Blueprints. Similarly, many web servers will respond with an error to a request that threw an exception, but will not crash the server itself. This PR does not enable this behavior by default, but makes it user-configurable. Also fixes #19109 Also (I think) fixes #7434 ## Solution Instead of rethrowing panics, hand them to the `FallbackErrorHandler`. If the panic was thrown by an error handler in the first place, we don't need to pass it back to a handler again. I've added a way for the error handler to signal that it's the source of the panic. The constructed error is created without a backtrace, as the default panic handler already prints it when instructed to via `RUST_LIB_BACKTRACE`/`RUST_BACKTRACE`. Panics will not be turned into errors on `no_std` projects. Potential work for a future PR: - if a error handler has been specified with e.g. `queue_handled`, use this error handler instead of the fallback error handler - if a command panics, still apply the remaining commands in the buffer? ## Testing See added `panic_to_error` test --------- Co-authored-by: Gonçalo Rica Pais da Silva <bluefinger@gmail.com>
199 lines
6.7 KiB
Rust
199 lines
6.7 KiB
Rust
//! Showcases how fallible systems and observers can make use of Rust's powerful result handling
|
|
//! syntax.
|
|
|
|
use bevy::ecs::{entity::SpawnError, error::warn, world::DeferredWorld};
|
|
use bevy::math::sampling::UniformMeshSampler;
|
|
use bevy::prelude::*;
|
|
|
|
use chacha20::ChaCha8Rng;
|
|
use rand::distr::Distribution;
|
|
use rand::SeedableRng;
|
|
|
|
fn main() {
|
|
let mut app = App::new();
|
|
// By default, fallible systems that return an error will respond according to the `Severity`` in the error.
|
|
// These will typically panic, unless `with_severity` is used to change the severity of the error.
|
|
//
|
|
// We can change this by configuring the fallback error handler, which applies to the entire app
|
|
// (you can also set it for specific `World`s).
|
|
// Here we are using one of the built-in error handlers.
|
|
// Bevy provides built-in handlers for `panic`, `error`, `warn`, `info`,
|
|
// `debug`, `trace` and `ignore`.
|
|
app.set_error_handler(warn);
|
|
|
|
app.add_plugins(DefaultPlugins);
|
|
|
|
#[cfg(feature = "mesh_picking")]
|
|
app.add_plugins(MeshPickingPlugin);
|
|
|
|
// Fallible systems can be used the same way as regular systems. The only difference is they
|
|
// return a `Result<(), BevyError>` instead of a `()` (unit) type. Bevy will handle both
|
|
// types of systems the same way, except for the error handling.
|
|
app.add_systems(Startup, setup);
|
|
|
|
// Commands can also return `Result`s, which are automatically handled by the fallback error handler
|
|
// if not explicitly handled by the user.
|
|
app.add_systems(Startup, failing_commands);
|
|
|
|
// Individual systems can also be handled by piping the output result:
|
|
app.add_systems(
|
|
PostStartup,
|
|
failing_system.pipe(|result: In<Result>| {
|
|
let _ = result.0.inspect_err(|err| info!("captured error: {err}"));
|
|
}),
|
|
);
|
|
|
|
// Fallible observers are also supported.
|
|
app.add_observer(fallible_observer);
|
|
|
|
// If we run the app, we'll see the following output at startup:
|
|
//
|
|
// WARN Encountered an error in system `fallible_systems::failing_system`: Resource not initialized
|
|
// ERROR fallible_systems::failing_system failed: Resource not initialized
|
|
// INFO captured error: Resource not initialized
|
|
app.run();
|
|
}
|
|
|
|
/// An example of a system that calls several fallible functions with the question mark operator.
|
|
///
|
|
/// See: <https://doc.rust-lang.org/reference/expressions/operator-expr.html#the-question-mark-operator>
|
|
fn setup(
|
|
mut commands: Commands,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
) -> Result {
|
|
let mut seeded_rng = ChaCha8Rng::seed_from_u64(19878367467712);
|
|
|
|
// Make a plane for establishing space.
|
|
commands.spawn((
|
|
Mesh3d(meshes.add(Plane3d::default().mesh().size(12.0, 12.0))),
|
|
MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
|
|
Transform::from_xyz(0.0, -2.5, 0.0),
|
|
));
|
|
|
|
// Spawn a light:
|
|
commands.spawn((
|
|
PointLight {
|
|
shadow_maps_enabled: true,
|
|
..default()
|
|
},
|
|
Transform::from_xyz(4.0, 8.0, 4.0),
|
|
));
|
|
|
|
// Spawn a camera:
|
|
commands.spawn((
|
|
Camera3d::default(),
|
|
Transform::from_xyz(-2.0, 3.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
|
|
));
|
|
|
|
// Create a new sphere mesh:
|
|
let mut sphere_mesh = Sphere::new(1.0).mesh().ico(7)?;
|
|
sphere_mesh.generate_tangents()?;
|
|
|
|
// Spawn the mesh into the scene:
|
|
let mut sphere = commands.spawn((
|
|
Mesh3d(meshes.add(sphere_mesh.clone())),
|
|
MeshMaterial3d(materials.add(StandardMaterial::default())),
|
|
Transform::from_xyz(-1.0, 1.0, 0.0),
|
|
));
|
|
|
|
// Generate random sample points:
|
|
let triangles = sphere_mesh.triangles()?;
|
|
let distribution = UniformMeshSampler::try_new(triangles)?;
|
|
|
|
// Setup sample points:
|
|
let point_mesh = meshes.add(Sphere::new(0.01).mesh().ico(3)?);
|
|
let point_material = materials.add(StandardMaterial {
|
|
base_color: Srgba::RED.into(),
|
|
emissive: LinearRgba::rgb(1.0, 0.0, 0.0),
|
|
..default()
|
|
});
|
|
|
|
// Add sample points as children of the sphere:
|
|
for point in distribution.sample_iter(&mut seeded_rng).take(10000) {
|
|
sphere.with_child((
|
|
Mesh3d(point_mesh.clone()),
|
|
MeshMaterial3d(point_material.clone()),
|
|
Transform::from_translation(point),
|
|
));
|
|
}
|
|
|
|
// Indicate the system completed successfully:
|
|
Ok(())
|
|
}
|
|
|
|
// Observer systems can also return a `Result`.
|
|
fn fallible_observer(
|
|
pointer_move: On<Pointer<Move>>,
|
|
mut world: DeferredWorld,
|
|
mut step: Local<f32>,
|
|
) -> Result {
|
|
let mut transform = world
|
|
.get_mut::<Transform>(pointer_move.entity)
|
|
.ok_or("No transform found.")?;
|
|
|
|
*step = if transform.translation.x > 3. {
|
|
-0.1
|
|
} else if transform.translation.x < -3. || *step == 0. {
|
|
0.1
|
|
} else {
|
|
*step
|
|
};
|
|
|
|
transform.translation.x += *step;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
struct UninitializedResource;
|
|
|
|
fn failing_system(world: &mut World) -> Result {
|
|
world
|
|
// `get_resource` returns an `Option<T>`, so we use `ok_or` to convert it to a `Result` on
|
|
// which we can call `?` to propagate the error.
|
|
.get_resource::<UninitializedResource>()
|
|
// We can provide a `str` here because `BevyError` implements `From<&str>`.
|
|
.ok_or("Resource not initialized")
|
|
// The default error severity is Severity::Panic.
|
|
// We can add a Severity level to any Result locally to downgrade it appropriately.
|
|
.with_severity(Severity::Warning)?;
|
|
|
|
world
|
|
// This entity doesn't exist!
|
|
.spawn_empty_at(Entity::from_raw_u32(12345678).unwrap())
|
|
.map_severity(|e| match e {
|
|
// Not that concerning, we just need to make sure to find a different entity
|
|
SpawnError::AlreadySpawned => Severity::Debug,
|
|
// Oh no
|
|
SpawnError::Invalid(_) => Severity::Error,
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn failing_commands(mut commands: Commands) {
|
|
commands
|
|
// This entity doesn't exist!
|
|
.entity(Entity::from_raw_u32(12345678).unwrap())
|
|
// Normally, this failed command would panic,
|
|
// but since we've set the fallback error handler to `warn`
|
|
// it will log a warning instead.
|
|
.insert(Transform::default());
|
|
|
|
// The error handlers for commands can be set individually as well,
|
|
// by using the queue_handled method.
|
|
commands.queue_handled(
|
|
|world: &mut World| -> Result {
|
|
world
|
|
.get_resource::<UninitializedResource>()
|
|
.ok_or("Resource not initialized when accessed in a command")?;
|
|
|
|
Ok(())
|
|
},
|
|
|error, context| {
|
|
error!("{error}, {context}");
|
|
},
|
|
);
|
|
}
|