From 65e289f5bc25def281babdc53b7e31e87e83d9c2 Mon Sep 17 00:00:00 2001 From: Antony Date: Tue, 18 Mar 2025 19:24:43 +0000 Subject: [PATCH] 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 --- crates/bevy_picking/src/lib.rs | 2 +- crates/bevy_picking/src/mesh_picking/mod.rs | 29 +++++----- crates/bevy_sprite/src/lib.rs | 33 +++-------- crates/bevy_sprite/src/picking_backend.rs | 6 +- crates/bevy_ui/src/lib.rs | 15 ++--- crates/bevy_ui/src/picking_backend.rs | 62 +++++++++++++++++++-- examples/picking/debug_picking.rs | 3 +- examples/picking/mesh_picking.rs | 4 +- examples/picking/simple_picking.rs | 3 +- examples/picking/sprite_picking.rs | 5 +- examples/ui/directional_navigation.rs | 1 + examples/ui/scroll.rs | 2 +- examples/ui/tab_navigation.rs | 7 ++- 13 files changed, 107 insertions(+), 65 deletions(-) diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 17e8c994b1..6afe86b0d6 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -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::{ diff --git a/crates/bevy_picking/src/mesh_picking/mod.rs b/crates/bevy_picking/src/mesh_picking/mod.rs index ceccc12764..42d704e772 100644 --- a/crates/bevy_picking/src/mesh_picking/mod.rs +++ b/crates/bevy_picking/src/mesh_picking/mod.rs @@ -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::() - .register_type::() .register_type::() .register_type::() .add_systems(PreUpdate, update_hits.in_set(PickSet::Backend)); @@ -78,18 +79,18 @@ impl Plugin for MeshPickingPlugin { pub fn update_hits( backend_settings: Res, ray_map: Res, - picking_cameras: Query<(&Camera, Option<&RayCastPickable>, Option<&RenderLayers>)>, + picking_cameras: Query<(&Camera, Has, Option<&RenderLayers>)>, pickables: Query<&Pickable>, - marked_targets: Query<&RayCastPickable>, + marked_targets: Query<&Pickable>, layers: Query<&RenderLayers>, mut ray_cast: MeshRayCast, mut output: EventWriter, ) { 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; } diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 1107e6c38e..ae204dcfd0 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -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: " -)] -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 = 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 diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 5877b116e0..a029838147 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -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; diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 5dfb4feeeb..82dac8d975 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -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; } diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index 09acc000ea..f0b8412854 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -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: " +)] +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::() + .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)>, + camera_query: Query<( + Entity, + &Camera, + Has, + Has, + )>, primary_window: Query>, + settings: Res, ui_stack: Res, node_query: Query, mut output: EventWriter, @@ -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 diff --git a/examples/picking/debug_picking.rs b/examples/picking/debug_picking.rs index ea42702032..28859c52e3 100644 --- a/examples/picking/debug_picking.rs +++ b/examples/picking/debug_picking.rs @@ -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: diff --git a/examples/picking/mesh_picking.rs b/examples/picking/mesh_picking.rs index ae9a68980e..3c0d3bf09f 100644 --- a/examples/picking/mesh_picking.rs +++ b/examples/picking/mesh_picking.rs @@ -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; diff --git a/examples/picking/simple_picking.rs b/examples/picking/simple_picking.rs index 4501e192b5..8a8b7cc982 100644 --- a/examples/picking/simple_picking.rs +++ b/examples/picking/simple_picking.rs @@ -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(); } diff --git a/examples/picking/sprite_picking.rs b/examples/picking/sprite_picking.rs index 7b6b2d1582..3ed01cff57 100644 --- a/examples/picking/sprite_picking.rs +++ b/examples/picking/sprite_picking.rs @@ -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(); diff --git a/examples/ui/directional_navigation.rs b/examples/ui/directional_navigation.rs index 0512c7498d..e1505bcef2 100644 --- a/examples/ui/directional_navigation.rs +++ b/examples/ui/directional_navigation.rs @@ -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 diff --git a/examples/ui/scroll.rs b/examples/ui/scroll.rs index 5cf44df553..fe040dda84 100644 --- a/examples/ui/scroll.rs +++ b/examples/ui/scroll.rs @@ -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); diff --git a/examples/ui/tab_navigation.rs b/examples/ui/tab_navigation.rs index c6060bd848..d1e54e8a72 100644 --- a/examples/ui/tab_navigation.rs +++ b/examples/ui/tab_navigation.rs @@ -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)