Skip to content

Commit 7d40e3e

Browse files
authored
Migrate bevy_sprite to required components (#15489)
# Objective Continue migration of bevy APIs to required components, following guidance of https://hackmd.io/@bevy/required_components/ ## Solution - Make `Sprite` require `Transform` and `Visibility` and `SyncToRenderWorld` - move image and texture atlas handles into `Sprite` - deprecate `SpriteBundle` - remove engine uses of `SpriteBundle` ## Testing ran cargo tests on bevy_sprite and tested several sprite examples. --- ## Migration Guide Replace all uses of `SpriteBundle` with `Sprite`. There are several new convenience constructors: `Sprite::from_image`, `Sprite::from_atlas_image`, `Sprite::from_color`. WARNING: use of `Handle<Image>` and `TextureAtlas` as components on sprite entities will NO LONGER WORK. Use the fields on `Sprite` instead. I would have removed the `Component` impls from `TextureAtlas` and `Handle<Image>` except it is still used within ui. We should fix this moving forward with the migration.
1 parent 219b593 commit 7d40e3e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+462
-682
lines changed

crates/bevy_ecs/src/bundle.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,6 @@ use core::{any::TypeId, ptr::NonNull};
7777
/// Additionally, [Tuples](`tuple`) of bundles are also [`Bundle`] (with up to 15 bundles).
7878
/// These bundles contain the items of the 'inner' bundles.
7979
/// This is a convenient shorthand which is primarily used when spawning entities.
80-
/// For example, spawning an entity using the bundle `(SpriteBundle {...}, PlayerMarker)`
81-
/// will spawn an entity with components required for a 2d sprite, and the `PlayerMarker` component.
8280
///
8381
/// [`unit`], otherwise known as [`()`](`unit`), is a [`Bundle`] containing no components (since it
8482
/// can also be considered as the empty tuple).

crates/bevy_sprite/src/bundle.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#![expect(deprecated)]
12
use crate::Sprite;
23
use bevy_asset::Handle;
34
use bevy_ecs::bundle::Bundle;
@@ -16,6 +17,10 @@ use bevy_transform::components::{GlobalTransform, Transform};
1617
/// - [`ImageScaleMode`](crate::ImageScaleMode) to enable either slicing or tiling of the texture
1718
/// - [`TextureAtlas`](crate::TextureAtlas) to draw a specific section of the texture
1819
#[derive(Bundle, Clone, Debug, Default)]
20+
#[deprecated(
21+
since = "0.15.0",
22+
note = "Use the `Sprite` component instead. Inserting it will now also insert `Transform` and `Visibility` automatically."
23+
)]
1924
pub struct SpriteBundle {
2025
/// Specifies the rendering properties of the sprite, such as color tint and flip.
2126
pub sprite: Sprite,

crates/bevy_sprite/src/lib.rs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,9 @@ pub fn calculate_bounds_2d(
185185
atlases: Res<Assets<TextureAtlasLayout>>,
186186
meshes_without_aabb: Query<(Entity, &Mesh2d), (Without<Aabb>, Without<NoFrustumCulling>)>,
187187
sprites_to_recalculate_aabb: Query<
188-
(Entity, &Sprite, &Handle<Image>, Option<&TextureAtlas>),
188+
(Entity, &Sprite),
189189
(
190-
Or<(Without<Aabb>, Changed<Sprite>, Changed<TextureAtlas>)>,
190+
Or<(Without<Aabb>, Changed<Sprite>)>,
191191
Without<NoFrustumCulling>,
192192
),
193193
>,
@@ -199,13 +199,13 @@ pub fn calculate_bounds_2d(
199199
}
200200
}
201201
}
202-
for (entity, sprite, texture_handle, atlas) in &sprites_to_recalculate_aabb {
202+
for (entity, sprite) in &sprites_to_recalculate_aabb {
203203
if let Some(size) = sprite
204204
.custom_size
205205
.or_else(|| sprite.rect.map(|rect| rect.size()))
206-
.or_else(|| match atlas {
206+
.or_else(|| match &sprite.texture_atlas {
207207
// We default to the texture size for regular sprites
208-
None => images.get(texture_handle).map(Image::size_f32),
208+
None => images.get(&sprite.image).map(Image::size_f32),
209209
// We default to the drawn rect for atlas sprites
210210
Some(atlas) => atlas
211211
.texture_rect(&atlases)
@@ -259,10 +259,7 @@ mod test {
259259
app.add_systems(Update, calculate_bounds_2d);
260260

261261
// Add entities
262-
let entity = app
263-
.world_mut()
264-
.spawn((Sprite::default(), image_handle))
265-
.id();
262+
let entity = app.world_mut().spawn(Sprite::from_image(image_handle)).id();
266263

267264
// Verify that the entity does not have an AABB
268265
assert!(!app

crates/bevy_sprite/src/picking_backend.rs

Lines changed: 76 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
use core::cmp::Reverse;
66

7-
use crate::{Sprite, TextureAtlas, TextureAtlasLayout};
7+
use crate::{Sprite, TextureAtlasLayout};
88
use bevy_app::prelude::*;
99
use bevy_asset::prelude::*;
1010
use bevy_ecs::prelude::*;
@@ -32,8 +32,6 @@ pub fn sprite_picking(
3232
sprite_query: Query<(
3333
Entity,
3434
&Sprite,
35-
Option<&TextureAtlas>,
36-
&Handle<Image>,
3735
&GlobalTransform,
3836
Option<&PickingBehavior>,
3937
&ViewVisibility,
@@ -42,9 +40,9 @@ pub fn sprite_picking(
4240
) {
4341
let mut sorted_sprites: Vec<_> = sprite_query
4442
.iter()
45-
.filter(|x| !x.4.affine().is_nan())
43+
.filter(|x| !x.2.affine().is_nan())
4644
.collect();
47-
sorted_sprites.sort_by_key(|x| Reverse(FloatOrd(x.4.translation().z)));
45+
sorted_sprites.sort_by_key(|x| Reverse(FloatOrd(x.2.translation().z)));
4846

4947
let primary_window = primary_window.get_single().ok();
5048

@@ -77,82 +75,79 @@ pub fn sprite_picking(
7775
.iter()
7876
.copied()
7977
.filter(|(.., visibility)| visibility.get())
80-
.filter_map(
81-
|(entity, sprite, atlas, image, sprite_transform, picking_behavior, ..)| {
82-
if blocked {
83-
return None;
84-
}
85-
86-
// Hit box in sprite coordinate system
87-
let extents = match (sprite.custom_size, atlas) {
88-
(Some(custom_size), _) => custom_size,
89-
(None, None) => images.get(image)?.size().as_vec2(),
90-
(None, Some(atlas)) => texture_atlas_layout
91-
.get(&atlas.layout)
92-
.and_then(|layout| layout.textures.get(atlas.index))
93-
// Dropped atlas layouts and indexes out of bounds are rendered as a sprite
94-
.map_or(images.get(image)?.size().as_vec2(), |rect| {
95-
rect.size().as_vec2()
96-
}),
97-
};
98-
let anchor = sprite.anchor.as_vec();
99-
let center = -anchor * extents;
100-
let rect = Rect::from_center_half_size(center, extents / 2.0);
101-
102-
// Transform cursor line segment to sprite coordinate system
103-
let world_to_sprite = sprite_transform.affine().inverse();
104-
let cursor_start_sprite =
105-
world_to_sprite.transform_point3(cursor_ray_world.origin);
106-
let cursor_end_sprite = world_to_sprite.transform_point3(cursor_ray_end);
107-
108-
// Find where the cursor segment intersects the plane Z=0 (which is the sprite's
109-
// plane in sprite-local space). It may not intersect if, for example, we're
110-
// viewing the sprite side-on
111-
if cursor_start_sprite.z == cursor_end_sprite.z {
112-
// Cursor ray is parallel to the sprite and misses it
113-
return None;
114-
}
115-
let lerp_factor =
116-
f32::inverse_lerp(cursor_start_sprite.z, cursor_end_sprite.z, 0.0);
117-
if !(0.0..=1.0).contains(&lerp_factor) {
118-
// Lerp factor is out of range, meaning that while an infinite line cast by
119-
// the cursor would intersect the sprite, the sprite is not between the
120-
// camera's near and far planes
121-
return None;
122-
}
123-
// Otherwise we can interpolate the xy of the start and end positions by the
124-
// lerp factor to get the cursor position in sprite space!
125-
let cursor_pos_sprite = cursor_start_sprite
126-
.lerp(cursor_end_sprite, lerp_factor)
127-
.xy();
128-
129-
let is_cursor_in_sprite = rect.contains(cursor_pos_sprite);
130-
131-
blocked = is_cursor_in_sprite
132-
&& picking_behavior.map(|p| p.should_block_lower) != Some(false);
133-
134-
is_cursor_in_sprite.then(|| {
135-
let hit_pos_world =
136-
sprite_transform.transform_point(cursor_pos_sprite.extend(0.0));
137-
// Transform point from world to camera space to get the Z distance
138-
let hit_pos_cam = cam_transform
139-
.affine()
140-
.inverse()
141-
.transform_point3(hit_pos_world);
142-
// HitData requires a depth as calculated from the camera's near clipping plane
143-
let depth = -cam_ortho.near - hit_pos_cam.z;
144-
(
145-
entity,
146-
HitData::new(
147-
cam_entity,
148-
depth,
149-
Some(hit_pos_world),
150-
Some(*sprite_transform.back()),
151-
),
152-
)
153-
})
154-
},
155-
)
78+
.filter_map(|(entity, sprite, sprite_transform, picking_behavior, ..)| {
79+
if blocked {
80+
return None;
81+
}
82+
83+
// Hit box in sprite coordinate system
84+
let extents = match (sprite.custom_size, &sprite.texture_atlas) {
85+
(Some(custom_size), _) => custom_size,
86+
(None, None) => images.get(&sprite.image)?.size().as_vec2(),
87+
(None, Some(atlas)) => texture_atlas_layout
88+
.get(&atlas.layout)
89+
.and_then(|layout| layout.textures.get(atlas.index))
90+
// Dropped atlas layouts and indexes out of bounds are rendered as a sprite
91+
.map_or(images.get(&sprite.image)?.size().as_vec2(), |rect| {
92+
rect.size().as_vec2()
93+
}),
94+
};
95+
let anchor = sprite.anchor.as_vec();
96+
let center = -anchor * extents;
97+
let rect = Rect::from_center_half_size(center, extents / 2.0);
98+
99+
// Transform cursor line segment to sprite coordinate system
100+
let world_to_sprite = sprite_transform.affine().inverse();
101+
let cursor_start_sprite = world_to_sprite.transform_point3(cursor_ray_world.origin);
102+
let cursor_end_sprite = world_to_sprite.transform_point3(cursor_ray_end);
103+
104+
// Find where the cursor segment intersects the plane Z=0 (which is the sprite's
105+
// plane in sprite-local space). It may not intersect if, for example, we're
106+
// viewing the sprite side-on
107+
if cursor_start_sprite.z == cursor_end_sprite.z {
108+
// Cursor ray is parallel to the sprite and misses it
109+
return None;
110+
}
111+
let lerp_factor =
112+
f32::inverse_lerp(cursor_start_sprite.z, cursor_end_sprite.z, 0.0);
113+
if !(0.0..=1.0).contains(&lerp_factor) {
114+
// Lerp factor is out of range, meaning that while an infinite line cast by
115+
// the cursor would intersect the sprite, the sprite is not between the
116+
// camera's near and far planes
117+
return None;
118+
}
119+
// Otherwise we can interpolate the xy of the start and end positions by the
120+
// lerp factor to get the cursor position in sprite space!
121+
let cursor_pos_sprite = cursor_start_sprite
122+
.lerp(cursor_end_sprite, lerp_factor)
123+
.xy();
124+
125+
let is_cursor_in_sprite = rect.contains(cursor_pos_sprite);
126+
127+
blocked = is_cursor_in_sprite
128+
&& picking_behavior.map(|p| p.should_block_lower) != Some(false);
129+
130+
is_cursor_in_sprite.then(|| {
131+
let hit_pos_world =
132+
sprite_transform.transform_point(cursor_pos_sprite.extend(0.0));
133+
// Transform point from world to camera space to get the Z distance
134+
let hit_pos_cam = cam_transform
135+
.affine()
136+
.inverse()
137+
.transform_point3(hit_pos_world);
138+
// HitData requires a depth as calculated from the camera's near clipping plane
139+
let depth = -cam_ortho.near - hit_pos_cam.z;
140+
(
141+
entity,
142+
HitData::new(
143+
cam_entity,
144+
depth,
145+
Some(hit_pos_world),
146+
Some(*sprite_transform.back()),
147+
),
148+
)
149+
})
150+
})
156151
.collect();
157152

158153
let order = camera.order as f32;

crates/bevy_sprite/src/render/mod.rs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
use core::ops::Range;
22

33
use crate::{
4-
texture_atlas::{TextureAtlas, TextureAtlasLayout},
5-
ComputedTextureSlices, Sprite, WithSprite, SPRITE_SHADER_HANDLE,
4+
texture_atlas::TextureAtlasLayout, ComputedTextureSlices, Sprite, WithSprite,
5+
SPRITE_SHADER_HANDLE,
66
};
7-
use bevy_asset::{AssetEvent, AssetId, Assets, Handle};
7+
use bevy_asset::{AssetEvent, AssetId, Assets};
88
use bevy_color::{ColorToComponents, LinearRgba};
99
use bevy_core_pipeline::{
1010
core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT},
@@ -377,15 +377,12 @@ pub fn extract_sprites(
377377
&ViewVisibility,
378378
&Sprite,
379379
&GlobalTransform,
380-
&Handle<Image>,
381-
Option<&TextureAtlas>,
382380
Option<&ComputedTextureSlices>,
383381
)>,
384382
>,
385383
) {
386384
extracted_sprites.sprites.clear();
387-
for (original_entity, entity, view_visibility, sprite, transform, handle, sheet, slices) in
388-
sprite_query.iter()
385+
for (original_entity, entity, view_visibility, sprite, transform, slices) in sprite_query.iter()
389386
{
390387
if !view_visibility.get() {
391388
continue;
@@ -394,12 +391,14 @@ pub fn extract_sprites(
394391
if let Some(slices) = slices {
395392
extracted_sprites.sprites.extend(
396393
slices
397-
.extract_sprites(transform, original_entity, sprite, handle)
394+
.extract_sprites(transform, original_entity, sprite)
398395
.map(|e| (commands.spawn(TemporaryRenderEntity).id(), e)),
399396
);
400397
} else {
401-
let atlas_rect =
402-
sheet.and_then(|s| s.texture_rect(&texture_atlases).map(|r| r.as_rect()));
398+
let atlas_rect = sprite
399+
.texture_atlas
400+
.as_ref()
401+
.and_then(|s| s.texture_rect(&texture_atlases).map(|r| r.as_rect()));
403402
let rect = match (atlas_rect, sprite.rect) {
404403
(None, None) => None,
405404
(None, Some(sprite_rect)) => Some(sprite_rect),
@@ -423,7 +422,7 @@ pub fn extract_sprites(
423422
custom_size: sprite.custom_size,
424423
flip_x: sprite.flip_x,
425424
flip_y: sprite.flip_y,
426-
image_handle_id: handle.id(),
425+
image_handle_id: sprite.image.id(),
427426
anchor: sprite.anchor.as_vec(),
428427
original_entity: Some(original_entity),
429428
},

crates/bevy_sprite/src/sprite.rs

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
1+
use bevy_asset::Handle;
12
use bevy_color::Color;
23
use bevy_ecs::{component::Component, reflect::ReflectComponent};
34
use bevy_math::{Rect, Vec2};
45
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
6+
use bevy_render::{sync_world::SyncToRenderWorld, texture::Image, view::Visibility};
7+
use bevy_transform::components::Transform;
58

6-
use crate::TextureSlicer;
9+
use crate::{TextureAtlas, TextureSlicer};
710

8-
/// Specifies the rendering properties of a sprite.
9-
///
10-
/// This is commonly used as a component within [`SpriteBundle`](crate::bundle::SpriteBundle).
11+
/// Describes a sprite to be rendered to a 2D camera
1112
#[derive(Component, Debug, Default, Clone, Reflect)]
13+
#[require(Transform, Visibility, SyncToRenderWorld)]
1214
#[reflect(Component, Default, Debug)]
1315
pub struct Sprite {
16+
/// The image used to render the sprite
17+
pub image: Handle<Image>,
18+
/// The (optional) texture atlas used to render the sprite
19+
pub texture_atlas: Option<TextureAtlas>,
1420
/// The sprite's color tint
1521
pub color: Color,
1622
/// Flip the sprite along the `X` axis
@@ -21,9 +27,9 @@ pub struct Sprite {
2127
/// of the sprite's image
2228
pub custom_size: Option<Vec2>,
2329
/// An optional rectangle representing the region of the sprite's image to render, instead of rendering
24-
/// the full image. This is an easy one-off alternative to using a [`TextureAtlas`](crate::TextureAtlas).
30+
/// the full image. This is an easy one-off alternative to using a [`TextureAtlas`].
2531
///
26-
/// When used with a [`TextureAtlas`](crate::TextureAtlas), the rect
32+
/// When used with a [`TextureAtlas`], the rect
2733
/// is offset by the atlas's minimal (top-left) corner position.
2834
pub rect: Option<Rect>,
2935
/// [`Anchor`] point of the sprite in the world
@@ -38,6 +44,38 @@ impl Sprite {
3844
..Default::default()
3945
}
4046
}
47+
48+
/// Create a sprite from an image
49+
pub fn from_image(image: Handle<Image>) -> Self {
50+
Self {
51+
image,
52+
..Default::default()
53+
}
54+
}
55+
56+
/// Create a sprite from an image, with an associated texture atlas
57+
pub fn from_atlas_image(image: Handle<Image>, atlas: TextureAtlas) -> Self {
58+
Self {
59+
image,
60+
texture_atlas: Some(atlas),
61+
..Default::default()
62+
}
63+
}
64+
65+
/// Create a sprite from a solid color
66+
pub fn from_color(color: impl Into<Color>, size: Vec2) -> Self {
67+
Self {
68+
color: color.into(),
69+
custom_size: Some(size),
70+
..Default::default()
71+
}
72+
}
73+
}
74+
75+
impl From<Handle<Image>> for Sprite {
76+
fn from(image: Handle<Image>) -> Self {
77+
Self::from_image(image)
78+
}
4179
}
4280

4381
/// Controls how the image is altered when scaled.
@@ -58,7 +96,7 @@ pub enum ImageScaleMode {
5896
},
5997
}
6098

61-
/// How a sprite is positioned relative to its [`Transform`](bevy_transform::components::Transform).
99+
/// How a sprite is positioned relative to its [`Transform`].
62100
/// It defaults to `Anchor::Center`.
63101
#[derive(Component, Debug, Clone, Copy, PartialEq, Default, Reflect)]
64102
#[reflect(Component, Default, Debug, PartialEq)]

0 commit comments

Comments
 (0)