
# Objective Currently, the observer API looks like this: ```rust app.add_observer(|trigger: Trigger<Explode>| { info!("Entity {} exploded!", trigger.target()); }); ``` Future plans for observers also include "multi-event observers" with a trigger that looks like this (see [Cart's example](https://github.com/bevyengine/bevy/issues/14649#issuecomment-2960402508)): ```rust trigger: Trigger<( OnAdd<Pressed>, OnRemove<Pressed>, OnAdd<InteractionDisabled>, OnRemove<InteractionDisabled>, OnInsert<Hovered>, )>, ``` In scenarios like this, there is a lot of repetition of `On`. These are expected to be very high-traffic APIs especially in UI contexts, so ergonomics and readability are critical. By renaming `Trigger` to `On`, we can make these APIs read more cleanly and get rid of the repetition: ```rust app.add_observer(|trigger: On<Explode>| { info!("Entity {} exploded!", trigger.target()); }); ``` ```rust trigger: On<( Add<Pressed>, Remove<Pressed>, Add<InteractionDisabled>, Remove<InteractionDisabled>, Insert<Hovered>, )>, ``` Names like `On<Add<Pressed>>` emphasize the actual event listener nature more than `Trigger<OnAdd<Pressed>>`, and look cleaner. This *also* frees up the `Trigger` name if we want to use it for the observer event type, splitting them out from buffered events (bikeshedding this is out of scope for this PR though). For prior art: [`bevy_eventlistener`](https://github.com/aevyrie/bevy_eventlistener) used [`On`](https://docs.rs/bevy_eventlistener/latest/bevy_eventlistener/event_listener/struct.On.html) for its event listener type. Though in our case, the observer is the event listener, and `On` is just a type containing information about the triggered event. ## Solution Steal from `bevy_event_listener` by @aevyrie and use `On`. - Rename `Trigger` to `On` - Rename `OnAdd` to `Add` - Rename `OnInsert` to `Insert` - Rename `OnReplace` to `Replace` - Rename `OnRemove` to `Remove` - Rename `OnDespawn` to `Despawn` ## Discussion ### Naming Conflicts?? Using a name like `Add` might initially feel like a very bad idea, since it risks conflict with `core::ops::Add`. However, I don't expect this to be a big problem in practice. - You rarely need to actually implement the `Add` trait, especially in modules that would use the Bevy ECS. - In the rare cases where you *do* get a conflict, it is very easy to fix by just disambiguating, for example using `ops::Add`. - The `Add` event is a struct while the `Add` trait is a trait (duh), so the compiler error should be very obvious. For the record, renaming `OnAdd` to `Add`, I got exactly *zero* errors or conflicts within Bevy itself. But this is of course not entirely representative of actual projects *using* Bevy. You might then wonder, why not use `Added`? This would conflict with the `Added` query filter, so it wouldn't work. Additionally, the current naming convention for observer events does not use past tense. ### Documentation This does make documentation slightly more awkward when referring to `On` or its methods. Previous docs often referred to `Trigger::target` or "sends a `Trigger`" (which is... a bit strange anyway), which would now be `On::target` and "sends an observer `Event`". You can see the diff in this PR to see some of the effects. I think it should be fine though, we may just need to reword more documentation to read better.
185 lines
5.9 KiB
Rust
185 lines
5.9 KiB
Rust
//! Showcases how fallible systems and observers can make use of Rust's powerful result handling
|
|
//! syntax.
|
|
|
|
use bevy::ecs::{error::warn, world::DeferredWorld};
|
|
use bevy::math::sampling::UniformMeshSampler;
|
|
use bevy::prelude::*;
|
|
|
|
use rand::distributions::Distribution;
|
|
use rand::SeedableRng;
|
|
use rand_chacha::ChaCha8Rng;
|
|
|
|
fn main() {
|
|
let mut app = App::new();
|
|
// By default, fallible systems that return an error will panic.
|
|
//
|
|
// We can change this by setting a custom error handler, which applies to the entire app
|
|
// (you can also set it for specific `World`s).
|
|
// Here we it 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 = "bevy_mesh_picking_backend")]
|
|
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 global 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 {
|
|
shadows_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(
|
|
trigger: On<Pointer<Move>>,
|
|
mut world: DeferredWorld,
|
|
mut step: Local<f32>,
|
|
) -> Result {
|
|
let mut transform = world
|
|
.get_mut::<Transform>(trigger.target)
|
|
.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")?;
|
|
|
|
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 global 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}");
|
|
},
|
|
);
|
|
}
|