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:
parent
c9f37efeb7
commit
f04406ccce
@ -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::{
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user