
# Objective Fixes a part of #14274. Bevy has an incredibly inconsistent naming convention for its system sets, both internally and across the ecosystem. <img alt="System sets in Bevy" src="https://github.com/user-attachments/assets/d16e2027-793f-4ba4-9cc9-e780b14a5a1b" width="450" /> *Names of public system set types in Bevy* Most Bevy types use a naming of `FooSystem` or just `Foo`, but there are also a few `FooSystems` and `FooSet` types. In ecosystem crates on the other hand, `FooSet` is perhaps the most commonly used name in general. Conventions being so wildly inconsistent can make it harder for users to pick names for their own types, to search for system sets on docs.rs, or to even discern which types *are* system sets. To reign in the inconsistency a bit and help unify the ecosystem, it would be good to establish a common recommended naming convention for system sets in Bevy itself, similar to how plugins are commonly suffixed with `Plugin` (ex: `TimePlugin`). By adopting a consistent naming convention in first-party Bevy, we can softly nudge ecosystem crates to follow suit (for types where it makes sense to do so). Choosing a naming convention is also relevant now, as the [`bevy_cli` recently adopted lints](https://github.com/TheBevyFlock/bevy_cli/pull/345) to enforce naming for plugins and system sets, and the recommended naming used for system sets is still a bit open. ## Which Name To Use? Now the contentious part: what naming convention should we actually adopt? This was discussed on the Bevy Discord at the end of last year, starting [here](<https://discord.com/channels/691052431525675048/692572690833473578/1310659954683936789>). `FooSet` and `FooSystems` were the clear favorites, with `FooSet` very narrowly winning an unofficial poll. However, it seems to me like the consensus was broadly moving towards `FooSystems` at the end and after the poll, with Cart ([source](https://discord.com/channels/691052431525675048/692572690833473578/1311140204974706708)) and later Alice ([source](https://discord.com/channels/691052431525675048/692572690833473578/1311092530732859533)) and also me being in favor of it. Let's do a quick pros and cons list! Of course these are just what I thought of, so take it with a grain of salt. `FooSet`: - Pro: Nice and short! - Pro: Used by many ecosystem crates. - Pro: The `Set` suffix comes directly from the trait name `SystemSet`. - Pro: Pairs nicely with existing APIs like `in_set` and `configure_sets`. - Con: `Set` by itself doesn't actually indicate that it's related to systems *at all*, apart from the implemented trait. A set of what? - Con: Is `FooSet` a set of `Foo`s or a system set related to `Foo`? Ex: `ContactSet`, `MeshSet`, `EnemySet`... `FooSystems`: - Pro: Very clearly indicates that the type represents a collection of systems. The actual core concept, system(s), is in the name. - Pro: Parallels nicely with `FooPlugins` for plugin groups. - Pro: Low risk of conflicts with other names or misunderstandings about what the type is. - Pro: In most cases, reads *very* nicely and clearly. Ex: `PhysicsSystems` and `AnimationSystems` as opposed to `PhysicsSet` and `AnimationSet`. - Pro: Easy to search for on docs.rs. - Con: Usually results in longer names. - Con: Not yet as widely used. Really the big problem with `FooSet` is that it doesn't actually describe what it is. It describes what *kind of thing* it is (a set of something), but not *what it is a set of*, unless you know the type or check its docs or implemented traits. `FooSystems` on the other hand is much more self-descriptive in this regard, at the cost of being a bit longer to type. Ultimately, in some ways it comes down to preference and how you think of system sets. Personally, I was originally in favor of `FooSet`, but have been increasingly on the side of `FooSystems`, especially after seeing what the new names would actually look like in Avian and now Bevy. I prefer it because it usually reads better, is much more clearly related to groups of systems than `FooSet`, and overall *feels* more correct and natural to me in the long term. For these reasons, and because Alice and Cart also seemed to share a preference for it when it was previously being discussed, I propose that we adopt a `FooSystems` naming convention where applicable. ## Solution Rename Bevy's system set types to use a consistent `FooSet` naming where applicable. - `AccessibilitySystem` → `AccessibilitySystems` - `GizmoRenderSystem` → `GizmoRenderSystems` - `PickSet` → `PickingSystems` - `RunFixedMainLoopSystem` → `RunFixedMainLoopSystems` - `TransformSystem` → `TransformSystems` - `RemoteSet` → `RemoteSystems` - `RenderSet` → `RenderSystems` - `SpriteSystem` → `SpriteSystems` - `StateTransitionSteps` → `StateTransitionSystems` - `RenderUiSystem` → `RenderUiSystems` - `UiSystem` → `UiSystems` - `Animation` → `AnimationSystems` - `AssetEvents` → `AssetEventSystems` - `TrackAssets` → `AssetTrackingSystems` - `UpdateGizmoMeshes` → `GizmoMeshSystems` - `InputSystem` → `InputSystems` - `InputFocusSet` → `InputFocusSystems` - `ExtractMaterialsSet` → `MaterialExtractionSystems` - `ExtractMeshesSet` → `MeshExtractionSystems` - `RumbleSystem` → `RumbleSystems` - `CameraUpdateSystem` → `CameraUpdateSystems` - `ExtractAssetsSet` → `AssetExtractionSystems` - `Update2dText` → `Text2dUpdateSystems` - `TimeSystem` → `TimeSystems` - `AudioPlaySet` → `AudioPlaybackSystems` - `SendEvents` → `EventSenderSystems` - `EventUpdates` → `EventUpdateSystems` A lot of the names got slightly longer, but they are also a lot more consistent, and in my opinion the majority of them read much better. For a few of the names I took the liberty of rewording things a bit; definitely open to any further naming improvements. There are still also cases where the `FooSystems` naming doesn't really make sense, and those I left alone. This primarily includes system sets like `Interned<dyn SystemSet>`, `EnterSchedules<S>`, `ExitSchedules<S>`, or `TransitionSchedules<S>`, where the type has some special purpose and semantics. ## Todo - [x] Should I keep all the old names as deprecated type aliases? I can do this, but to avoid wasting work I'd prefer to first reach consensus on whether these renames are even desired. - [x] Migration guide - [x] Release notes
256 lines
10 KiB
Rust
256 lines
10 KiB
Rust
//! A [`bevy_picking`] backend for sprites. Works for simple sprites and sprite atlases. Works for
|
|
//! sprites with arbitrary transforms.
|
|
//!
|
|
//! By default, picking for sprites is based on pixel opacity.
|
|
//! A sprite is picked only when a pointer is over an opaque pixel.
|
|
//! Alternatively, you can configure picking to be based on sprite bounds.
|
|
//!
|
|
//! ## Implementation Notes
|
|
//!
|
|
//! - The `position` reported in `HitData` in in world space, and the `normal` is a normalized
|
|
//! vector provided by the target's `GlobalTransform::back()`.
|
|
|
|
use crate::Sprite;
|
|
use bevy_app::prelude::*;
|
|
use bevy_asset::prelude::*;
|
|
use bevy_color::Alpha;
|
|
use bevy_ecs::prelude::*;
|
|
use bevy_image::prelude::*;
|
|
use bevy_math::{prelude::*, FloatExt};
|
|
use bevy_picking::backend::prelude::*;
|
|
use bevy_reflect::prelude::*;
|
|
use bevy_render::prelude::*;
|
|
use bevy_transform::prelude::*;
|
|
use bevy_window::PrimaryWindow;
|
|
|
|
/// 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;
|
|
|
|
/// How should the [`SpritePickingPlugin`] handle picking and how should it handle transparent pixels
|
|
#[derive(Debug, Clone, Copy, Reflect)]
|
|
#[reflect(Debug, Clone)]
|
|
pub enum SpritePickingMode {
|
|
/// Even if a sprite is picked on a transparent pixel, it should still count within the backend.
|
|
/// Only consider the rect of a given sprite.
|
|
BoundingBox,
|
|
/// Ignore any part of a sprite which has a lower alpha value than the threshold (inclusive)
|
|
/// Threshold is given as an f32 representing the alpha value in a Bevy Color Value
|
|
AlphaThreshold(f32),
|
|
}
|
|
|
|
/// Runtime settings for the [`SpritePickingPlugin`].
|
|
#[derive(Resource, Reflect)]
|
|
#[reflect(Resource, Default)]
|
|
pub struct SpritePickingSettings {
|
|
/// When set to `true` sprite picking will only consider cameras marked with
|
|
/// [`SpritePickingCamera`]. Defaults to `false`.
|
|
/// Regardless of this setting, only sprites marked with [`Pickable`] will be considered.
|
|
///
|
|
/// This setting is provided to give you fine-grained control over which cameras
|
|
/// should be used by the sprite picking backend at runtime.
|
|
pub require_markers: bool,
|
|
/// Should the backend count transparent pixels as part of the sprite for picking purposes or should it use the bounding box of the sprite alone.
|
|
///
|
|
/// Defaults to an inclusive alpha threshold of 0.1
|
|
pub picking_mode: SpritePickingMode,
|
|
}
|
|
|
|
impl Default for SpritePickingSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
require_markers: false,
|
|
picking_mode: SpritePickingMode::AlphaThreshold(0.1),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Enables the sprite picking backend, allowing you to click on, hover over and drag sprites.
|
|
#[derive(Clone)]
|
|
pub struct SpritePickingPlugin;
|
|
|
|
impl Plugin for SpritePickingPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.init_resource::<SpritePickingSettings>()
|
|
.register_type::<SpritePickingCamera>()
|
|
.register_type::<SpritePickingMode>()
|
|
.register_type::<SpritePickingSettings>()
|
|
.add_systems(PreUpdate, sprite_picking.in_set(PickingSystems::Backend));
|
|
}
|
|
}
|
|
|
|
fn sprite_picking(
|
|
pointers: Query<(&PointerId, &PointerLocation)>,
|
|
cameras: Query<(
|
|
Entity,
|
|
&Camera,
|
|
&GlobalTransform,
|
|
&Projection,
|
|
Has<SpritePickingCamera>,
|
|
)>,
|
|
primary_window: Query<Entity, With<PrimaryWindow>>,
|
|
images: Res<Assets<Image>>,
|
|
texture_atlas_layout: Res<Assets<TextureAtlasLayout>>,
|
|
settings: Res<SpritePickingSettings>,
|
|
sprite_query: Query<(
|
|
Entity,
|
|
&Sprite,
|
|
&GlobalTransform,
|
|
&Pickable,
|
|
&ViewVisibility,
|
|
)>,
|
|
mut output: EventWriter<PointerHits>,
|
|
) {
|
|
let mut sorted_sprites: Vec<_> = sprite_query
|
|
.iter()
|
|
.filter_map(|(entity, sprite, transform, pickable, vis)| {
|
|
if !transform.affine().is_nan() && vis.get() {
|
|
Some((entity, sprite, transform, pickable))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
// radsort is a stable radix sort that performed better than `slice::sort_by_key`
|
|
radsort::sort_by_key(&mut sorted_sprites, |(_, _, transform, _)| {
|
|
-transform.translation().z
|
|
});
|
|
|
|
let primary_window = primary_window.single().ok();
|
|
|
|
for (pointer, location) in pointers.iter().filter_map(|(pointer, pointer_location)| {
|
|
pointer_location.location().map(|loc| (pointer, loc))
|
|
}) {
|
|
let mut blocked = false;
|
|
let Some((cam_entity, camera, cam_transform, Projection::Orthographic(cam_ortho), _)) =
|
|
cameras
|
|
.iter()
|
|
.filter(|(_, camera, _, _, cam_can_pick)| {
|
|
let marker_requirement = !settings.require_markers || *cam_can_pick;
|
|
camera.is_active && marker_requirement
|
|
})
|
|
.find(|(_, camera, _, _, _)| {
|
|
camera
|
|
.target
|
|
.normalize(primary_window)
|
|
.is_some_and(|x| x == location.target)
|
|
})
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
let viewport_pos = camera
|
|
.logical_viewport_rect()
|
|
.map(|v| v.min)
|
|
.unwrap_or_default();
|
|
let pos_in_viewport = location.position - viewport_pos;
|
|
|
|
let Ok(cursor_ray_world) = camera.viewport_to_world(cam_transform, pos_in_viewport) else {
|
|
continue;
|
|
};
|
|
let cursor_ray_len = cam_ortho.far - cam_ortho.near;
|
|
let cursor_ray_end = cursor_ray_world.origin + cursor_ray_world.direction * cursor_ray_len;
|
|
|
|
let picks: Vec<(Entity, HitData)> = sorted_sprites
|
|
.iter()
|
|
.copied()
|
|
.filter_map(|(entity, sprite, sprite_transform, pickable)| {
|
|
if blocked {
|
|
return None;
|
|
}
|
|
|
|
// Transform cursor line segment to sprite coordinate system
|
|
let world_to_sprite = sprite_transform.affine().inverse();
|
|
let cursor_start_sprite = world_to_sprite.transform_point3(cursor_ray_world.origin);
|
|
let cursor_end_sprite = world_to_sprite.transform_point3(cursor_ray_end);
|
|
|
|
// Find where the cursor segment intersects the plane Z=0 (which is the sprite's
|
|
// plane in sprite-local space). It may not intersect if, for example, we're
|
|
// viewing the sprite side-on
|
|
if cursor_start_sprite.z == cursor_end_sprite.z {
|
|
// Cursor ray is parallel to the sprite and misses it
|
|
return None;
|
|
}
|
|
let lerp_factor =
|
|
f32::inverse_lerp(cursor_start_sprite.z, cursor_end_sprite.z, 0.0);
|
|
if !(0.0..=1.0).contains(&lerp_factor) {
|
|
// Lerp factor is out of range, meaning that while an infinite line cast by
|
|
// the cursor would intersect the sprite, the sprite is not between the
|
|
// camera's near and far planes
|
|
return None;
|
|
}
|
|
// Otherwise we can interpolate the xy of the start and end positions by the
|
|
// lerp factor to get the cursor position in sprite space!
|
|
let cursor_pos_sprite = cursor_start_sprite
|
|
.lerp(cursor_end_sprite, lerp_factor)
|
|
.xy();
|
|
|
|
let Ok(cursor_pixel_space) = sprite.compute_pixel_space_point(
|
|
cursor_pos_sprite,
|
|
&images,
|
|
&texture_atlas_layout,
|
|
) else {
|
|
return None;
|
|
};
|
|
|
|
// Since the pixel space coordinate is `Ok`, we know the cursor is in the bounds of
|
|
// the sprite.
|
|
|
|
let cursor_in_valid_pixels_of_sprite = 'valid_pixel: {
|
|
match settings.picking_mode {
|
|
SpritePickingMode::AlphaThreshold(cutoff) => {
|
|
let Some(image) = images.get(&sprite.image) else {
|
|
// [`Sprite::from_color`] returns a defaulted handle.
|
|
// This handle doesn't return a valid image, so returning false here would make picking "color sprites" impossible
|
|
break 'valid_pixel true;
|
|
};
|
|
// grab pixel and check alpha
|
|
let Ok(color) = image.get_color_at(
|
|
cursor_pixel_space.x as u32,
|
|
cursor_pixel_space.y as u32,
|
|
) else {
|
|
// We don't know how to interpret the pixel.
|
|
break 'valid_pixel false;
|
|
};
|
|
// Check the alpha is above the cutoff.
|
|
color.alpha() > cutoff
|
|
}
|
|
SpritePickingMode::BoundingBox => true,
|
|
}
|
|
};
|
|
|
|
blocked = cursor_in_valid_pixels_of_sprite && pickable.should_block_lower;
|
|
|
|
cursor_in_valid_pixels_of_sprite.then(|| {
|
|
let hit_pos_world =
|
|
sprite_transform.transform_point(cursor_pos_sprite.extend(0.0));
|
|
// Transform point from world to camera space to get the Z distance
|
|
let hit_pos_cam = cam_transform
|
|
.affine()
|
|
.inverse()
|
|
.transform_point3(hit_pos_world);
|
|
// HitData requires a depth as calculated from the camera's near clipping plane
|
|
let depth = -cam_ortho.near - hit_pos_cam.z;
|
|
(
|
|
entity,
|
|
HitData::new(
|
|
cam_entity,
|
|
depth,
|
|
Some(hit_pos_world),
|
|
Some(*sprite_transform.back()),
|
|
),
|
|
)
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
let order = camera.order as f32;
|
|
output.write(PointerHits::new(*pointer, picks, order));
|
|
}
|
|
}
|