
# 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.
198 lines
7.6 KiB
Rust
198 lines
7.6 KiB
Rust
//! A simple 3D scene to demonstrate mesh picking.
|
|
//!
|
|
//! [`bevy::picking::backend`] provides an API for adding picking hit tests to any entity. To get
|
|
//! started with picking 3d meshes, the [`MeshPickingPlugin`] is provided as a simple starting
|
|
//! point, especially useful for debugging. For your game, you may want to use a 3d picking backend
|
|
//! provided by your physics engine, or a picking shader, depending on your specific use case.
|
|
//!
|
|
//! [`bevy::picking`] allows you to compose backends together to make any entity on screen pickable
|
|
//! with pointers, regardless of how that entity is rendered. For example, `bevy_ui` and
|
|
//! `bevy_sprite` provide their own picking backends that can be enabled at the same time as this
|
|
//! mesh picking backend. This makes it painless to deal with cases like the UI or sprites blocking
|
|
//! meshes underneath them, or vice versa.
|
|
//!
|
|
//! If you want to build more complex interactions than afforded by the provided pointer events, you
|
|
//! may want to use [`MeshRayCast`] or a full physics engine with raycasting capabilities.
|
|
//!
|
|
//! By default, the mesh picking plugin will raycast against all entities, which is especially
|
|
//! useful for debugging. If you want mesh picking to be opt-in, you can set
|
|
//! [`MeshPickingSettings::require_markers`] to `true` and add a [`Pickable`] component to the
|
|
//! desired camera and target entities.
|
|
|
|
use std::f32::consts::PI;
|
|
|
|
use bevy::{color::palettes::tailwind::*, picking::pointer::PointerInteraction, prelude::*};
|
|
|
|
fn main() {
|
|
App::new()
|
|
// MeshPickingPlugin is not a default plugin
|
|
.add_plugins((DefaultPlugins, MeshPickingPlugin))
|
|
.add_systems(Startup, setup_scene)
|
|
.add_systems(Update, (draw_mesh_intersections, rotate))
|
|
.run();
|
|
}
|
|
|
|
/// A marker component for our shapes so we can query them separately from the ground plane.
|
|
#[derive(Component)]
|
|
struct Shape;
|
|
|
|
const SHAPES_X_EXTENT: f32 = 14.0;
|
|
const EXTRUSION_X_EXTENT: f32 = 16.0;
|
|
const Z_EXTENT: f32 = 5.0;
|
|
|
|
fn setup_scene(
|
|
mut commands: Commands,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
) {
|
|
// Set up the materials.
|
|
let white_matl = materials.add(Color::WHITE);
|
|
let ground_matl = materials.add(Color::from(GRAY_300));
|
|
let hover_matl = materials.add(Color::from(CYAN_300));
|
|
let pressed_matl = materials.add(Color::from(YELLOW_300));
|
|
|
|
let shapes = [
|
|
meshes.add(Cuboid::default()),
|
|
meshes.add(Tetrahedron::default()),
|
|
meshes.add(Capsule3d::default()),
|
|
meshes.add(Torus::default()),
|
|
meshes.add(Cylinder::default()),
|
|
meshes.add(Cone::default()),
|
|
meshes.add(ConicalFrustum::default()),
|
|
meshes.add(Sphere::default().mesh().ico(5).unwrap()),
|
|
meshes.add(Sphere::default().mesh().uv(32, 18)),
|
|
];
|
|
|
|
let extrusions = [
|
|
meshes.add(Extrusion::new(Rectangle::default(), 1.)),
|
|
meshes.add(Extrusion::new(Capsule2d::default(), 1.)),
|
|
meshes.add(Extrusion::new(Annulus::default(), 1.)),
|
|
meshes.add(Extrusion::new(Circle::default(), 1.)),
|
|
meshes.add(Extrusion::new(Ellipse::default(), 1.)),
|
|
meshes.add(Extrusion::new(RegularPolygon::default(), 1.)),
|
|
meshes.add(Extrusion::new(Triangle2d::default(), 1.)),
|
|
];
|
|
|
|
let num_shapes = shapes.len();
|
|
|
|
// Spawn the shapes. The meshes will be pickable by default.
|
|
for (i, shape) in shapes.into_iter().enumerate() {
|
|
commands
|
|
.spawn((
|
|
Mesh3d(shape),
|
|
MeshMaterial3d(white_matl.clone()),
|
|
Transform::from_xyz(
|
|
-SHAPES_X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * SHAPES_X_EXTENT,
|
|
2.0,
|
|
Z_EXTENT / 2.,
|
|
)
|
|
.with_rotation(Quat::from_rotation_x(-PI / 4.)),
|
|
Shape,
|
|
))
|
|
.observe(update_material_on::<Pointer<Over>>(hover_matl.clone()))
|
|
.observe(update_material_on::<Pointer<Out>>(white_matl.clone()))
|
|
.observe(update_material_on::<Pointer<Press>>(pressed_matl.clone()))
|
|
.observe(update_material_on::<Pointer<Release>>(hover_matl.clone()))
|
|
.observe(rotate_on_drag);
|
|
}
|
|
|
|
let num_extrusions = extrusions.len();
|
|
|
|
for (i, shape) in extrusions.into_iter().enumerate() {
|
|
commands
|
|
.spawn((
|
|
Mesh3d(shape),
|
|
MeshMaterial3d(white_matl.clone()),
|
|
Transform::from_xyz(
|
|
-EXTRUSION_X_EXTENT / 2.
|
|
+ i as f32 / (num_extrusions - 1) as f32 * EXTRUSION_X_EXTENT,
|
|
2.0,
|
|
-Z_EXTENT / 2.,
|
|
)
|
|
.with_rotation(Quat::from_rotation_x(-PI / 4.)),
|
|
Shape,
|
|
))
|
|
.observe(update_material_on::<Pointer<Over>>(hover_matl.clone()))
|
|
.observe(update_material_on::<Pointer<Out>>(white_matl.clone()))
|
|
.observe(update_material_on::<Pointer<Press>>(pressed_matl.clone()))
|
|
.observe(update_material_on::<Pointer<Release>>(hover_matl.clone()))
|
|
.observe(rotate_on_drag);
|
|
}
|
|
|
|
// Ground
|
|
commands.spawn((
|
|
Mesh3d(meshes.add(Plane3d::default().mesh().size(50.0, 50.0).subdivisions(10))),
|
|
MeshMaterial3d(ground_matl.clone()),
|
|
Pickable::IGNORE, // Disable picking for the ground plane.
|
|
));
|
|
|
|
// Light
|
|
commands.spawn((
|
|
PointLight {
|
|
shadows_enabled: true,
|
|
intensity: 10_000_000.,
|
|
range: 100.0,
|
|
shadow_depth_bias: 0.2,
|
|
..default()
|
|
},
|
|
Transform::from_xyz(8.0, 16.0, 8.0),
|
|
));
|
|
|
|
// Camera
|
|
commands.spawn((
|
|
Camera3d::default(),
|
|
Transform::from_xyz(0.0, 7., 14.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y),
|
|
));
|
|
|
|
// Instructions
|
|
commands.spawn((
|
|
Text::new("Hover over the shapes to pick them\nDrag to rotate"),
|
|
Node {
|
|
position_type: PositionType::Absolute,
|
|
top: Val::Px(12.0),
|
|
left: Val::Px(12.0),
|
|
..default()
|
|
},
|
|
));
|
|
}
|
|
|
|
/// Returns an observer that updates the entity's material to the one specified.
|
|
fn update_material_on<E>(
|
|
new_material: Handle<StandardMaterial>,
|
|
) -> impl Fn(On<E>, Query<&mut MeshMaterial3d<StandardMaterial>>) {
|
|
// An observer closure that captures `new_material`. We do this to avoid needing to write four
|
|
// versions of this observer, each triggered by a different event and with a different hardcoded
|
|
// material. Instead, the event type is a generic, and the material is passed in.
|
|
move |trigger, mut query| {
|
|
if let Ok(mut material) = query.get_mut(trigger.target().unwrap()) {
|
|
material.0 = new_material.clone();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A system that draws hit indicators for every pointer.
|
|
fn draw_mesh_intersections(pointers: Query<&PointerInteraction>, mut gizmos: Gizmos) {
|
|
for (point, normal) in pointers
|
|
.iter()
|
|
.filter_map(|interaction| interaction.get_nearest_hit())
|
|
.filter_map(|(_entity, hit)| hit.position.zip(hit.normal))
|
|
{
|
|
gizmos.sphere(point, 0.05, RED_500);
|
|
gizmos.arrow(point, point + normal.normalize() * 0.5, PINK_100);
|
|
}
|
|
}
|
|
|
|
/// A system that rotates all shapes.
|
|
fn rotate(mut query: Query<&mut Transform, With<Shape>>, time: Res<Time>) {
|
|
for mut transform in &mut query {
|
|
transform.rotate_y(time.delta_secs() / 2.);
|
|
}
|
|
}
|
|
|
|
/// An observer to rotate an entity when it is dragged
|
|
fn rotate_on_drag(drag: On<Pointer<Drag>>, mut transforms: Query<&mut Transform>) {
|
|
let mut transform = transforms.get_mut(drag.target().unwrap()).unwrap();
|
|
transform.rotate_y(drag.delta.x * 0.02);
|
|
transform.rotate_x(drag.delta.y * 0.02);
|
|
}
|