Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unify picking backends #17348

Merged
merged 13 commits into from
Mar 18, 2025
2 changes: 1 addition & 1 deletion crates/bevy_picking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down
29 changes: 15 additions & 14 deletions crates/bevy_picking/src/mesh_picking/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//!
Expand All @@ -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.
Expand All @@ -54,20 +62,13 @@ 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;

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));
Expand All @@ -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;
}

Expand Down
33 changes: 8 additions & 25 deletions crates/bevy_sprite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion crates/bevy_sprite/src/picking_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down
15 changes: 5 additions & 10 deletions crates/bevy_ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand All @@ -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;
Expand Down Expand Up @@ -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,
}
}
}
Expand Down Expand Up @@ -181,6 +180,7 @@ impl Plugin for UiPlugin {
)
.chain(),
)
.add_plugins(UiPickingPlugin)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not gated on the feature flag, which is inconsistent with the sprite picking backend and also seems necessary for configuration.

.add_systems(
PreUpdate,
ui_focus_system.in_set(UiSystem::Focus).after(InputSystem),
Expand Down Expand Up @@ -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;
}
Expand Down
62 changes: 57 additions & 5 deletions crates/bevy_ui/src/picking_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}

Expand All @@ -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>,
Expand All @@ -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()),
Expand All @@ -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 =
Expand Down Expand Up @@ -122,6 +170,10 @@ pub fn ui_picking(
continue;
};

if settings.require_markers && node.pickable.is_none() {
continue;
Comment on lines +173 to +174
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should not make picking opt-out with require_markers. Maybe introduce a second flag or remove configurable opt-out. The require_markers should only use to config the camera, not the UI elements picking opt-in/out

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe introduce a second flag

To me, this would decrease readability for opt-in behavior. In my opinion, if you opt-in, you are opting to be explicit, so not having to add UiPickingCamera when all your UI needs Pickable seems off. Happy to hear your thoughts though :)

[...] or remove configurable opt-out

To the best of my knowledge UI picking is going to drive a lot of the interactions going forward, so I think it makes sense to be opt-out by default (and consequently have the option to opt-in since there's no (?) perf loss).

Copy link
Contributor

@notmd notmd Feb 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, sorry for not making it clear. I mean a common scenario is you start with picking opt-out (all elements are pickable without inserting Pickable) and 1 camera. Then you start adding more cameras and now want to disable picking for them. You starting set make require_markers to true, but this will regret (even if you make your main camera to allow picking by inserting UiPickingCamera) all UI elements you have before because now it requires to explicit to insert Pickable. For UI, I only want to insert Pickable when I want to custom the behavior of Pickable. The only reason for UI picking opt-in is performance, but I believe you will soon realize that the performance gain is not worth the DX.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think renaming the flag inside of UIPickingSettings to something more explicit like only_pickable is reasonable.

That said the reason for making picking opt-in is performance, and that performance gain is worth a learning moment in the developer experience. The DX of updating your UI to reflect your picking preferences does not seem onerous.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should remove the ability to toggle opt-out behavior at the plugin level, and then use required components as needed to get "automatic picking". We don't need more mechanisms for this! I also think that the UiPickingPlugin should be added as part of UiPlugin.

Copy link
Member Author

@chompaa chompaa Feb 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, that makes sense, I agree required components do solve the DX issue. How do we want to handle it for cameras though? I feel like having the plugin level toggle is nice in this case. I can see why UiPickingPlugin should be added as a part of UiPlugin, but I'm not sure I agree with SpritePickingPlugin being added by default. What's the consensus there? If we decide that SpritePickingPlugin be added by default then I think MeshPickingPlugin should be too.

Edit: MeshPickingPlugin will need to be considered too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think no SpritePickingPlugin by default; it's definitely feasible to write games that never use it. The same cannot be said for building UIs that don't!

Not sure on the camera config TBH 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think sprite picking and ui picking should be enabled by default. Sprite picking has almost no cost associated with it (per-entity opt-in, filtered out cheaply via archetype queries) and provides high utility, and UI picking is a core piece of the UI puzzle.

}

// Nodes that are not rendered should not be interactable
if node
.inherited_visibility
Expand Down Expand Up @@ -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

Expand Down
3 changes: 1 addition & 2 deletions examples/picking/debug_picking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions examples/picking/mesh_picking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
3 changes: 1 addition & 2 deletions examples/picking/simple_picking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
5 changes: 4 additions & 1 deletion examples/picking/sprite_picking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions examples/ui/directional_navigation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/ui/scroll.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading