Unify picking backends (#17348)

# Objective

Currently, our picking backends are inconsistent:

- Mesh picking and sprite picking both have configurable opt in/out
behavior. UI picking does not.
- Sprite picking uses `SpritePickingCamera` and `Pickable` for control,
but mesh picking uses `RayCastPickable`.
- `MeshPickingPlugin` is not a part of `DefaultPlugins`.
`SpritePickingPlugin` and `UiPickingPlugin` are.

## Solution

- Add configurable opt in/out behavior to UI picking (defaults to opt
out).
- Replace `RayCastPickable` with `MeshPickingCamera` and `Pickable`.
- Remove `SpritePickingPlugin` and `UiPickingPlugin` from
`DefaultPlugins`.

## Testing

Ran some examples.

## Migration Guide

`UiPickingPlugin` and `SpritePickingPlugin` are no longer included in
`DefaultPlugins`. They must be explicitly added.

`RayCastPickable` has been replaced in favor of the `MeshPickingCamera`
and `Pickable` components. You should add them to cameras and entities,
respectively, if you have `MeshPickingSettings::require_markers` set to
`true`.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
Antony 2025-03-18 19:24:43 +00:00 committed by GitHub
parent b3ccc623fa
commit 65e289f5bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 107 additions and 65 deletions

View File

@ -179,7 +179,7 @@ pub mod prelude {
#[doc(hidden)]
pub use crate::mesh_picking::{
ray_cast::{MeshRayCast, MeshRayCastSettings, RayCastBackfaces, RayCastVisibility},
MeshPickingPlugin, MeshPickingSettings, RayCastPickable,
MeshPickingCamera, MeshPickingPlugin, MeshPickingSettings,
};
#[doc(hidden)]
pub use crate::{

View File

@ -4,7 +4,8 @@
//! by adding [`Pickable::IGNORE`].
//!
//! To make mesh picking entirely opt-in, set [`MeshPickingSettings::require_markers`]
//! to `true` and add a [`RayCastPickable`] component to the desired camera and target entities.
//! to `true` and add [`MeshPickingCamera`] and [`Pickable`] components to the desired camera and
//! target entities.
//!
//! To manually perform mesh ray casts independent of picking, use the [`MeshRayCast`] system parameter.
//!
@ -26,12 +27,19 @@ use bevy_reflect::prelude::*;
use bevy_render::{prelude::*, view::RenderLayers};
use ray_cast::{MeshRayCast, MeshRayCastSettings, RayCastVisibility, SimplifiedMesh};
/// An optional component that marks cameras that should be used in the [`MeshPickingPlugin`].
///
/// Only needed if [`MeshPickingSettings::require_markers`] is set to `true`, and ignored otherwise.
#[derive(Debug, Clone, Default, Component, Reflect)]
#[reflect(Debug, Default, Component)]
pub struct MeshPickingCamera;
/// Runtime settings for the [`MeshPickingPlugin`].
#[derive(Resource, Reflect)]
#[reflect(Resource, Default)]
pub struct MeshPickingSettings {
/// When set to `true` ray casting will only happen between cameras and entities marked with
/// [`RayCastPickable`]. `false` by default.
/// When set to `true` ray casting will only consider cameras marked with
/// [`MeshPickingCamera`] and entities marked with [`Pickable`]. `false` by default.
///
/// This setting is provided to give you fine-grained control over which cameras and entities
/// should be used by the mesh picking backend at runtime.
@ -54,12 +62,6 @@ impl Default for MeshPickingSettings {
}
}
/// An optional component that marks cameras and target entities that should be used in the [`MeshPickingPlugin`].
/// Only needed if [`MeshPickingSettings::require_markers`] is set to `true`, and ignored otherwise.
#[derive(Debug, Clone, Default, Component, Reflect)]
#[reflect(Component, Default, Clone)]
pub struct RayCastPickable;
/// Adds the mesh picking backend to your app.
#[derive(Clone, Default)]
pub struct MeshPickingPlugin;
@ -67,7 +69,6 @@ pub struct MeshPickingPlugin;
impl Plugin for MeshPickingPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<MeshPickingSettings>()
.register_type::<RayCastPickable>()
.register_type::<MeshPickingSettings>()
.register_type::<SimplifiedMesh>()
.add_systems(PreUpdate, update_hits.in_set(PickSet::Backend));
@ -78,18 +79,18 @@ impl Plugin for MeshPickingPlugin {
pub fn update_hits(
backend_settings: Res<MeshPickingSettings>,
ray_map: Res<RayMap>,
picking_cameras: Query<(&Camera, Option<&RayCastPickable>, Option<&RenderLayers>)>,
picking_cameras: Query<(&Camera, Has<MeshPickingCamera>, Option<&RenderLayers>)>,
pickables: Query<&Pickable>,
marked_targets: Query<&RayCastPickable>,
marked_targets: Query<&Pickable>,
layers: Query<&RenderLayers>,
mut ray_cast: MeshRayCast,
mut output: EventWriter<PointerHits>,
) {
for (&ray_id, &ray) in ray_map.map().iter() {
let Ok((camera, cam_pickable, cam_layers)) = picking_cameras.get(ray_id.camera) else {
let Ok((camera, cam_can_pick, cam_layers)) = picking_cameras.get(ray_id.camera) else {
continue;
};
if backend_settings.require_markers && cam_pickable.is_none() {
if backend_settings.require_markers && !cam_can_pick {
continue;
}

View File

@ -21,6 +21,11 @@ mod texture_slice;
///
/// This includes the most common types in this crate, re-exported for your convenience.
pub mod prelude {
#[cfg(feature = "bevy_sprite_picking_backend")]
#[doc(hidden)]
pub use crate::picking_backend::{
SpritePickingCamera, SpritePickingMode, SpritePickingPlugin, SpritePickingSettings,
};
#[doc(hidden)]
pub use crate::{
sprite::{Sprite, SpriteImageMode},
@ -52,28 +57,8 @@ use bevy_render::{
};
/// Adds support for 2D sprite rendering.
pub struct SpritePlugin {
/// Whether to add the sprite picking backend to the app.
#[cfg(feature = "bevy_sprite_picking_backend")]
pub add_picking: bool,
}
#[expect(
clippy::allow_attributes,
reason = "clippy::derivable_impls is not always linted"
)]
#[allow(
clippy::derivable_impls,
reason = "Known false positive with clippy: <https://github.com/rust-lang/rust-clippy/issues/13160>"
)]
impl Default for SpritePlugin {
fn default() -> Self {
Self {
#[cfg(feature = "bevy_sprite_picking_backend")]
add_picking: true,
}
}
}
#[derive(Default)]
pub struct SpritePlugin;
pub const SPRITE_SHADER_HANDLE: Handle<Shader> =
weak_handle!("ed996613-54c0-49bd-81be-1c2d1a0d03c2");
@ -125,9 +110,7 @@ impl Plugin for SpritePlugin {
);
#[cfg(feature = "bevy_sprite_picking_backend")]
if self.add_picking {
app.add_plugins(SpritePickingPlugin);
}
app.add_plugins(SpritePickingPlugin);
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app

View File

@ -20,7 +20,10 @@ use bevy_render::prelude::*;
use bevy_transform::prelude::*;
use bevy_window::PrimaryWindow;
/// A component that marks cameras that should be used in the [`SpritePickingPlugin`].
/// An optional component that marks cameras that should be used in the [`SpritePickingPlugin`].
///
/// Only needed if [`SpritePickingSettings::require_markers`] is set to `true`, and ignored
/// otherwise.
#[derive(Debug, Clone, Default, Component, Reflect)]
#[reflect(Debug, Default, Component, Clone)]
pub struct SpritePickingCamera;
@ -62,6 +65,7 @@ impl Default for SpritePickingSettings {
}
}
/// Enables the sprite picking backend, allowing you to click on, hover over and drag sprites.
#[derive(Clone)]
pub struct SpritePickingPlugin;

View File

@ -35,6 +35,7 @@ pub use focus::*;
pub use geometry::*;
pub use layout::*;
pub use measurement::*;
use prelude::UiPickingPlugin;
pub use render::*;
pub use ui_material::*;
pub use ui_node::*;
@ -45,6 +46,9 @@ use widget::{ImageNode, ImageNodeSize};
///
/// This includes the most common types in this crate, re-exported for your convenience.
pub mod prelude {
#[cfg(feature = "bevy_ui_picking_backend")]
#[doc(hidden)]
pub use crate::picking_backend::{UiPickingCamera, UiPickingPlugin, UiPickingSettings};
#[doc(hidden)]
#[cfg(feature = "bevy_ui_debug")]
pub use crate::render::UiDebugOptions;
@ -79,17 +83,12 @@ pub struct UiPlugin {
/// If set to false, the UI's rendering systems won't be added to the `RenderApp` and no UI elements will be drawn.
/// The layout and interaction components will still be updated as normal.
pub enable_rendering: bool,
/// Whether to add the UI picking backend to the app.
#[cfg(feature = "bevy_ui_picking_backend")]
pub add_picking: bool,
}
impl Default for UiPlugin {
fn default() -> Self {
Self {
enable_rendering: true,
#[cfg(feature = "bevy_ui_picking_backend")]
add_picking: true,
}
}
}
@ -181,6 +180,7 @@ impl Plugin for UiPlugin {
)
.chain(),
)
.add_plugins(UiPickingPlugin)
.add_systems(
PreUpdate,
ui_focus_system.in_set(UiSystem::Focus).after(InputSystem),
@ -219,11 +219,6 @@ impl Plugin for UiPlugin {
);
build_text_interop(app);
#[cfg(feature = "bevy_ui_picking_backend")]
if self.add_picking {
app.add_plugins(picking_backend::UiPickingPlugin);
}
if !self.enable_rendering {
return;
}

View File

@ -29,18 +29,59 @@ use bevy_app::prelude::*;
use bevy_ecs::{prelude::*, query::QueryData};
use bevy_math::{Rect, Vec2};
use bevy_platform_support::collections::HashMap;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::prelude::*;
use bevy_transform::prelude::*;
use bevy_window::PrimaryWindow;
use bevy_picking::backend::prelude::*;
/// An optional component that marks cameras that should be used in the [`UiPickingPlugin`].
///
/// Only needed if [`UiPickingSettings::require_markers`] is set to `true`, and ignored
/// otherwise.
#[derive(Debug, Clone, Default, Component, Reflect)]
#[reflect(Debug, Default, Component)]
pub struct UiPickingCamera;
/// Runtime settings for the [`UiPickingPlugin`].
#[derive(Resource, Reflect)]
#[reflect(Resource, Default)]
pub struct UiPickingSettings {
/// When set to `true` UI picking will only consider cameras marked with
/// [`UiPickingCamera`] and entities marked with [`Pickable`]. `false` by default.
///
/// This setting is provided to give you fine-grained control over which cameras and entities
/// should be used by the UI picking backend at runtime.
pub require_markers: bool,
}
#[expect(
clippy::allow_attributes,
reason = "clippy::derivable_impls is not always linted"
)]
#[allow(
clippy::derivable_impls,
reason = "Known false positive with clippy: <https://github.com/rust-lang/rust-clippy/issues/13160>"
)]
impl Default for UiPickingSettings {
fn default() -> Self {
Self {
require_markers: false,
}
}
}
/// A plugin that adds picking support for UI nodes.
///
/// This is included by default in [`UiPlugin`](crate::UiPlugin).
#[derive(Clone)]
pub struct UiPickingPlugin;
impl Plugin for UiPickingPlugin {
fn build(&self, app: &mut App) {
app.add_systems(PreUpdate, ui_picking.in_set(PickSet::Backend));
app.init_resource::<UiPickingSettings>()
.register_type::<(UiPickingCamera, UiPickingSettings)>()
.add_systems(PreUpdate, ui_picking.in_set(PickSet::Backend));
}
}
@ -63,8 +104,14 @@ pub struct NodeQuery {
/// we need for determining picking.
pub fn ui_picking(
pointers: Query<(&PointerId, &PointerLocation)>,
camera_query: Query<(Entity, &Camera, Has<IsDefaultUiCamera>)>,
camera_query: Query<(
Entity,
&Camera,
Has<IsDefaultUiCamera>,
Has<UiPickingCamera>,
)>,
primary_window: Query<Entity, With<PrimaryWindow>>,
settings: Res<UiPickingSettings>,
ui_stack: Res<UiStack>,
node_query: Query<NodeQuery>,
mut output: EventWriter<PointerHits>,
@ -81,7 +128,8 @@ pub fn ui_picking(
// cameras. We want to ensure we return all cameras with a matching target.
for camera in camera_query
.iter()
.map(|(entity, camera, _)| {
.filter(|(_, _, _, cam_can_pick)| !settings.require_markers || *cam_can_pick)
.map(|(entity, camera, _, _)| {
(
entity,
camera.target.normalize(primary_window.single().ok()),
@ -91,7 +139,7 @@ pub fn ui_picking(
.filter(|(_entity, target)| target == &pointer_location.target)
.map(|(cam_entity, _target)| cam_entity)
{
let Ok((_, camera_data, _)) = camera_query.get(camera) else {
let Ok((_, camera_data, _, _)) = camera_query.get(camera) else {
continue;
};
let mut pointer_pos =
@ -122,6 +170,10 @@ pub fn ui_picking(
continue;
};
if settings.require_markers && node.pickable.is_none() {
continue;
}
// Nodes that are not rendered should not be interactable
if node
.inherited_visibility
@ -208,7 +260,7 @@ pub fn ui_picking(
let order = camera_query
.get(*camera)
.map(|(_, cam, _)| cam.order)
.map(|(_, cam, _, _)| cam.order)
.unwrap_or_default() as f32
+ 0.5; // bevy ui can run on any camera, it's a special case

View File

@ -10,8 +10,7 @@ fn main() {
filter: "bevy_dev_tools=trace".into(), // Show picking logs trace level and up
..default()
}))
// Unlike UiPickingPlugin, MeshPickingPlugin is not a default plugin
.add_plugins((MeshPickingPlugin, DebugPickingPlugin))
.add_plugins((MeshPickingPlugin, DebugPickingPlugin, UiPickingPlugin))
.add_systems(Startup, setup_scene)
.insert_resource(DebugPickingMode::Normal)
// A system that cycles the debugging state when you press F3:

View File

@ -16,8 +16,8 @@
//!
//! 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 [`RayCastPickable`] component to
//! the desired camera and target entities.
//! [`MeshPickingSettings::require_markers`] to `true` and add a [`Pickable`] component to the
//! desired camera and target entities.
use std::f32::consts::PI;

View File

@ -4,8 +4,7 @@ use bevy::prelude::*;
fn main() {
App::new()
// Unlike UiPickingPlugin, MeshPickingPlugin is not a default plugin
.add_plugins((DefaultPlugins, MeshPickingPlugin))
.add_plugins((DefaultPlugins, MeshPickingPlugin, UiPickingPlugin))
.add_systems(Startup, setup_scene)
.run();
}

View File

@ -6,7 +6,10 @@ use std::fmt::Debug;
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()))
.add_plugins((
DefaultPlugins.set(ImagePlugin::default_nearest()),
SpritePickingPlugin,
))
.add_systems(Startup, (setup, setup_atlas))
.add_systems(Update, (move_sprite, animate_sprite))
.run();

View File

@ -31,6 +31,7 @@ fn main() {
DefaultPlugins,
InputDispatchPlugin,
DirectionalNavigationPlugin,
UiPickingPlugin,
))
// This resource is canonically used to track whether or not to render a focus indicator
// It starts as false, but we set it to true here as we would like to see the focus indicator

View File

@ -11,7 +11,7 @@ use bevy::{
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins)
app.add_plugins((DefaultPlugins, UiPickingPlugin))
.insert_resource(WinitSettings::desktop_app())
.add_systems(Startup, setup)
.add_systems(Update, update_scroll_position);

View File

@ -12,7 +12,12 @@ use bevy::{
fn main() {
App::new()
.add_plugins((DefaultPlugins, InputDispatchPlugin, TabNavigationPlugin))
.add_plugins((
DefaultPlugins,
InputDispatchPlugin,
TabNavigationPlugin,
UiPickingPlugin,
))
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
.insert_resource(WinitSettings::desktop_app())
.add_systems(Startup, setup)