diff --git a/Cargo.toml b/Cargo.toml index 9602ef33fbb58..eb6cf2816dae4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -778,6 +778,17 @@ description = "Renders an animated sprite" category = "2D Rendering" wasm = true +[[example]] +name = "sprite_sorting" +path = "examples/2d/sprite_sorting.rs" +doc-scrape-examples = true + +[package.metadata.example.sprite_sorting] +name = "Sprite Sorting" +description = "Demonstrates how to sort sprites" +category = "2D Rendering" +wasm = true + [[example]] name = "sprite_tile" path = "examples/2d/sprite_tile.rs" diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index 725ac38ed9762..6058504c8c7b4 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -343,9 +343,25 @@ impl CachedRenderPipelinePhaseItem for AlphaMask2d { } } +#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Copy, Clone)] +pub struct Transparent2dSortKey { + z_index: i32, + bias: FloatOrd, +} + +impl Transparent2dSortKey { + pub fn new(z_index: i32, bias: Option<f32>) -> Transparent2dSortKey { + Transparent2dSortKey { + z_index, + // nans sort after any valid specified y sort + bias: FloatOrd(bias.unwrap_or(f32::NAN)), + } + } +} + /// Transparent 2D [`SortedPhaseItem`]s. pub struct Transparent2d { - pub sort_key: FloatOrd, + pub sort_key: Transparent2dSortKey, pub entity: (Entity, MainEntity), pub pipeline: CachedRenderPipelineId, pub draw_function: DrawFunctionId, @@ -395,7 +411,7 @@ impl PhaseItem for Transparent2d { } impl SortedPhaseItem for Transparent2d { - type SortKey = FloatOrd; + type SortKey = Transparent2dSortKey; #[inline] fn sort_key(&self) -> Self::SortKey { @@ -405,7 +421,7 @@ impl SortedPhaseItem for Transparent2d { #[inline] fn sort(items: &mut [Self]) { // radsort is a stable radix sort that performed better than `slice::sort_by_key` or `slice::sort_unstable_by_key`. - radsort::sort_by_key(items, |item| item.sort_key().0); + radsort::sort_by_key(items, |item| (item.sort_key.z_index, item.sort_key.bias.0)); } fn indexed(&self) -> bool { diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index a97071249d1a4..90b7aad73b83a 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -6,7 +6,7 @@ use crate::{ }; use bevy_app::{App, Plugin}; use bevy_asset::{load_embedded_asset, Handle}; -use bevy_core_pipeline::core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT}; +use bevy_core_pipeline::core_2d::{Transparent2d, Transparent2dSortKey, CORE_2D_DEPTH_FORMAT}; use bevy_ecs::{ prelude::Entity, @@ -16,7 +16,6 @@ use bevy_ecs::{ world::{FromWorld, World}, }; use bevy_image::BevyDefault as _; -use bevy_math::FloatOrd; use bevy_render::sync_world::MainEntity; use bevy_render::{ render_asset::{prepare_assets, RenderAssets}, @@ -343,7 +342,7 @@ fn queue_line_gizmos_2d( entity: (entity, *main_entity), draw_function, pipeline, - sort_key: FloatOrd(f32::INFINITY), + sort_key: Transparent2dSortKey::new(i32::MAX, None), batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, extracted_index: usize::MAX, @@ -365,7 +364,7 @@ fn queue_line_gizmos_2d( entity: (entity, *main_entity), draw_function: draw_function_strip, pipeline, - sort_key: FloatOrd(f32::INFINITY), + sort_key: Transparent2dSortKey::new(i32::MAX, None), batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, extracted_index: usize::MAX, @@ -425,7 +424,7 @@ fn queue_line_joint_gizmos_2d( entity: (entity, *main_entity), draw_function, pipeline, - sort_key: FloatOrd(f32::INFINITY), + sort_key: Transparent2dSortKey::new(i32::MAX, None), batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, extracted_index: usize::MAX, diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 3f76b516cdd3f..389c3a9c5a45d 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -5,6 +5,7 @@ use crate::{ use bevy_app::{App, Plugin, PostUpdate}; use bevy_asset::prelude::AssetChanged; use bevy_asset::{AsAssetId, Asset, AssetApp, AssetEventSystems, AssetId, AssetServer, Handle}; +use bevy_core_pipeline::core_2d::Transparent2dSortKey; use bevy_core_pipeline::{ core_2d::{ AlphaMask2d, AlphaMask2dBinKey, BatchSetKey2d, Opaque2d, Opaque2dBinKey, Transparent2d, @@ -18,7 +19,6 @@ use bevy_ecs::{ prelude::*, system::{lifetimeless::SRes, SystemParamItem}, }; -use bevy_math::FloatOrd; use bevy_platform::collections::HashMap; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_render::camera::extract_cameras; @@ -839,7 +839,6 @@ pub fn queue_material2d_meshes<M: Material2d>( }; mesh_instance.material_bind_group_id = material_2d.get_bind_group_id(); - let mesh_z = mesh_instance.transforms.world_from_local.translation.z; // We don't support multidraw yet for 2D meshes, so we use this // custom logic to generate the `BinnedRenderPhaseType` instead of @@ -852,8 +851,11 @@ pub fn queue_material2d_meshes<M: Material2d>( BinnedRenderPhaseType::UnbatchableMesh }; - match material_2d.properties.alpha_mode { - AlphaMode2d::Opaque => { + let needs_sort = mesh_instance.z_index.is_some() + || mesh_instance.y_sort + || mesh_instance.sort_bias.is_some(); + match (material_2d.properties.alpha_mode, needs_sort) { + (AlphaMode2d::Opaque, false) => { let bin_key = Opaque2dBinKey { pipeline: pipeline_id, draw_function: material_2d.properties.draw_function_id, @@ -871,7 +873,7 @@ pub fn queue_material2d_meshes<M: Material2d>( current_change_tick, ); } - AlphaMode2d::Mask(_) => { + (AlphaMode2d::Mask(_), false) => { let bin_key = AlphaMask2dBinKey { pipeline: pipeline_id, draw_function: material_2d.properties.draw_function_id, @@ -889,7 +891,21 @@ pub fn queue_material2d_meshes<M: Material2d>( current_change_tick, ); } - AlphaMode2d::Blend => { + (AlphaMode2d::Blend, false) | (_, true) => { + let mesh_y = mesh_instance.transforms.world_from_local.translation.y; + let mesh_z = mesh_instance.transforms.world_from_local.translation.z; + let z_bias = material_2d.properties.depth_bias; + let sort_bias = mesh_instance.sort_bias; + let bias = if mesh_instance.y_sort { + mesh_y + z_bias + sort_bias.unwrap_or_default() + } else { + mesh_z + z_bias + sort_bias.unwrap_or_default() + }; + + let sort_key = Transparent2dSortKey::new( + mesh_instance.z_index.unwrap_or_default(), + Some(bias), + ); transparent_phase.add(Transparent2d { entity: (*render_entity, *visible_entity), draw_function: material_2d.properties.draw_function_id, @@ -898,7 +914,7 @@ pub fn queue_material2d_meshes<M: Material2d>( // lowest sort key and getting closer should increase. As we have // -z in front of the camera, the largest distance is -far with values increasing toward the // camera. As such we can just use mesh_z as the distance - sort_key: FloatOrd(mesh_z + material_2d.properties.depth_bias), + sort_key, // Batching is done in batch_and_prepare_render_phase batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index a3d9ee3eb23fd..a04f0f388c06e 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -137,6 +137,26 @@ impl Plugin for Mesh2dRenderPlugin { } } +/// Describes the layer in which the sprite or mesh should be rendered. Higher values are rendered +/// on top of lower values, with a default value of 0. This is useful for controlling the rendering +/// order of sprites and always takes precedence over [`YSort`] or [`SortBias`]. +#[derive(Component, Deref, DerefMut, Default, Debug, Clone)] +pub struct ZIndex(pub i32); + +/// A marker component that enables Y-sorting (depth sorting) for sprites and meshes. +/// +/// When attached to an entity, this component indicates that the entity should be rendered +/// in draw order based on its Y position. Entities with higher Y values (higher on screen) +/// are drawn first, creating a depth illusion where objects lower on the screen appear +/// in front of objects higher on the screen. +#[derive(Component, Default, Debug, Clone)] +pub struct YSort; + +/// An arbitrary bias value that can be applied to the sorting order of sprites and meshes and is +/// applied after the [`ZIndex`] or added to the Y position of the entity if [`YSort`] is enabled. +#[derive(Component, Deref, DerefMut, Default, Debug, Clone)] +pub struct SortBias(pub f32); + #[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] pub struct ViewKeyCache(MainEntityHashMap<Mesh2dPipelineKey>); @@ -228,6 +248,9 @@ pub struct RenderMesh2dInstance { pub material_bind_group_id: Material2dBindGroupId, pub automatic_batching: bool, pub tag: u32, + pub z_index: Option<i32>, + pub y_sort: bool, + pub sort_bias: Option<f32>, } #[derive(Default, Resource, Deref, DerefMut)] @@ -245,13 +268,27 @@ pub fn extract_mesh2d( &GlobalTransform, &Mesh2d, Option<&MeshTag>, + Option<&ZIndex>, + Has<YSort>, + Option<&SortBias>, Has<NoAutomaticBatching>, )>, >, ) { render_mesh_instances.clear(); - for (entity, view_visibility, transform, handle, tag, no_automatic_batching) in &query { + for ( + entity, + view_visibility, + transform, + handle, + tag, + z_index, + y_sort, + sort_bias, + no_automatic_batching, + ) in &query + { if !view_visibility.get() { continue; } @@ -266,6 +303,9 @@ pub fn extract_mesh2d( material_bind_group_id: Material2dBindGroupId::default(), automatic_batching: !no_automatic_batching, tag: tag.map_or(0, |i| **i), + z_index: z_index.cloned().map(|x| x.0), + y_sort, + sort_bias: sort_bias.cloned().map(|sb| sb.0), }, ); } diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 7602addc0b793..9c90dbd3fdcde 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -1,8 +1,8 @@ -use core::ops::Range; - -use crate::{Anchor, ComputedTextureSlices, ScalingMode, Sprite}; +use crate::{Anchor, ComputedTextureSlices, ScalingMode, SortBias, Sprite, YSort, ZIndex}; use bevy_asset::{load_embedded_asset, AssetEvent, AssetId, Assets, Handle}; + use bevy_color::{ColorToComponents, LinearRgba}; +use bevy_core_pipeline::core_2d::Transparent2dSortKey; use bevy_core_pipeline::{ core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT}, tonemapping::{ @@ -17,7 +17,7 @@ use bevy_ecs::{ system::{lifetimeless::*, SystemParamItem, SystemState}, }; use bevy_image::{BevyDefault, Image, ImageSampler, TextureAtlasLayout, TextureFormatPixelInfo}; -use bevy_math::{Affine3A, FloatOrd, Quat, Rect, Vec2, Vec4}; +use bevy_math::{Affine3A, Quat, Rect, Vec2, Vec4}; use bevy_platform::collections::HashMap; use bevy_render::view::{RenderVisibleEntities, RetainedViewEntity}; use bevy_render::{ @@ -41,6 +41,7 @@ use bevy_render::{ }; use bevy_transform::components::GlobalTransform; use bytemuck::{Pod, Zeroable}; +use core::ops::Range; use fixedbitset::FixedBitSet; #[derive(Resource)] @@ -343,6 +344,9 @@ pub struct ExtractedSprite { pub flip_x: bool, pub flip_y: bool, pub kind: ExtractedSpriteKind, + pub z_index: i32, + pub y_sort: bool, + pub sort_bias: Option<f32>, } pub enum ExtractedSpriteKind { @@ -398,13 +402,26 @@ pub fn extract_sprites( &GlobalTransform, &Anchor, Option<&ComputedTextureSlices>, + Option<&ZIndex>, + Has<YSort>, + Option<&SortBias>, )>, >, ) { extracted_sprites.sprites.clear(); extracted_slices.slices.clear(); - for (main_entity, render_entity, view_visibility, sprite, transform, anchor, slices) in - sprite_query.iter() + for ( + main_entity, + render_entity, + view_visibility, + sprite, + transform, + anchor, + slices, + z_index, + y_sort, + sort_bias, + ) in sprite_query.iter() { if !view_visibility.get() { continue; @@ -427,6 +444,9 @@ pub fn extract_sprites( kind: ExtractedSpriteKind::Slices { indices: start..end, }, + z_index: z_index.cloned().map_or(0, |z| z.0), + y_sort, + sort_bias: sort_bias.cloned().map(|sb| sb.0), }); } else { let atlas_rect = sprite @@ -460,6 +480,9 @@ pub fn extract_sprites( // Pass the custom size custom_size: sprite.custom_size, }, + z_index: z_index.cloned().map_or(0, |z| z.0), + y_sort, + sort_bias: sort_bias.cloned().map(|sb| sb.0), }); } } @@ -595,7 +618,14 @@ pub fn queue_sprites( } // These items will be sorted by depth with other phase items - let sort_key = FloatOrd(extracted_sprite.transform.translation().z); + let sort_key = Transparent2dSortKey::new( + extracted_sprite.z_index, + extracted_sprite + .y_sort + .then_some(extracted_sprite.transform.translation().y) + .map(|y| y + extracted_sprite.sort_bias.unwrap_or(0.0)) + .or(extracted_sprite.sort_bias), + ); // Add the item to the render phase transparent_phase.add(Transparent2d { diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 5069804df8672..9915a0551d59c 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -238,6 +238,9 @@ pub fn extract_text2d_sprite( kind: bevy_sprite::ExtractedSpriteKind::Slices { indices: start..end, }, + z_index: 0, + y_sort: false, + sort_bias: None, }); start = end; } diff --git a/examples/2d/2d_viewport_to_world.rs b/examples/2d/2d_viewport_to_world.rs index ce611bd4e9c50..0b3309a807936 100644 --- a/examples/2d/2d_viewport_to_world.rs +++ b/examples/2d/2d_viewport_to_world.rs @@ -183,6 +183,7 @@ fn setup( commands.spawn(( Mesh2d(meshes.add(Rectangle::new(50000.0, 50000.0))), MeshMaterial2d(materials.add(Color::linear_rgb(0.01, 0.01, 0.01))), - Transform::from_translation(Vec3::new(0.0, 0.0, -200.0)), + Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)), + bevy::sprite::ZIndex(-1), )); } diff --git a/examples/2d/mesh2d_alpha_mode.rs b/examples/2d/mesh2d_alpha_mode.rs index 60bb6ef3a6147..b7e766f4f6620 100644 --- a/examples/2d/mesh2d_alpha_mode.rs +++ b/examples/2d/mesh2d_alpha_mode.rs @@ -46,7 +46,8 @@ fn setup( texture: Some(texture_handle.clone()), ..default() })), - Transform::from_xyz(-300.0, 0.0, 1.0), + Transform::from_xyz(-300.0, 0.0, 0.0), + bevy::sprite::ZIndex(1), )); commands.spawn(( Mesh2d(mesh_handle.clone()), @@ -56,7 +57,8 @@ fn setup( texture: Some(texture_handle.clone()), ..default() })), - Transform::from_xyz(-200.0, 0.0, -1.0), + Transform::from_xyz(-200.0, 0.0, 0.0), + bevy::sprite::ZIndex(-1), )); // Test the interaction between opaque/mask and transparent meshes @@ -82,7 +84,8 @@ fn setup( texture: Some(texture_handle.clone()), ..default() })), - Transform::from_xyz(300.0, 0.0, 1.0), + Transform::from_xyz(300.0, 0.0, 0.0), + bevy::sprite::ZIndex(1), )); commands.spawn(( Mesh2d(mesh_handle.clone()), @@ -92,6 +95,7 @@ fn setup( texture: Some(texture_handle), ..default() })), - Transform::from_xyz(400.0, 0.0, -1.0), + Transform::from_xyz(400.0, 0.0, 0.0), + bevy::sprite::ZIndex(-1), )); } diff --git a/examples/2d/mesh2d_manual.rs b/examples/2d/mesh2d_manual.rs index 6354aa468c40d..2ed0ee23c2664 100644 --- a/examples/2d/mesh2d_manual.rs +++ b/examples/2d/mesh2d_manual.rs @@ -5,11 +5,12 @@ //! //! [`Material2d`]: bevy::sprite::Material2d +use bevy::core_pipeline::core_2d::Transparent2dSortKey; use bevy::{ asset::weak_handle, color::palettes::basic::YELLOW, core_pipeline::core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT}, - math::{ops, FloatOrd}, + math::ops, prelude::*, render::{ mesh::{Indices, MeshVertexAttribute, RenderMesh}, @@ -367,6 +368,9 @@ pub fn extract_colored_mesh2d( material_bind_group_id: Material2dBindGroupId::default(), automatic_batching: false, tag: 0, + z_index: None, + y_sort: false, + sort_bias: None, }, ); } @@ -404,7 +408,6 @@ pub fn queue_colored_mesh2d( for (render_entity, visible_entity) in visible_entities.iter::<Mesh2d>() { if let Some(mesh_instance) = render_mesh_instances.get(visible_entity) { let mesh2d_handle = mesh_instance.mesh_asset_id; - let mesh2d_transforms = &mesh_instance.transforms; // Get our specialized pipeline let mut mesh2d_key = mesh_key; let Some(mesh) = render_meshes.get(mesh2d_handle) else { @@ -415,14 +418,12 @@ pub fn queue_colored_mesh2d( let pipeline_id = pipelines.specialize(&pipeline_cache, &colored_mesh2d_pipeline, mesh2d_key); - let mesh_z = mesh2d_transforms.world_from_local.translation.z; transparent_phase.add(Transparent2d { entity: (*render_entity, *visible_entity), draw_function: draw_colored_mesh2d, pipeline: pipeline_id, - // The 2d render items are sorted according to their z value before rendering, - // in order to get correct transparency - sort_key: FloatOrd(mesh_z), + // Transparent 2d items are sorted by a combination of z-index and sort bias + sort_key: Transparent2dSortKey::new(0, None), // This material is not batched batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, diff --git a/examples/2d/pixel_grid_snap.rs b/examples/2d/pixel_grid_snap.rs index 4f946f9f941b4..6076d1d40f278 100644 --- a/examples/2d/pixel_grid_snap.rs +++ b/examples/2d/pixel_grid_snap.rs @@ -54,7 +54,8 @@ fn setup_sprite(mut commands: Commands, asset_server: Res<AssetServer>) { // The sample sprite that will be rendered to the pixel-perfect canvas commands.spawn(( Sprite::from_image(asset_server.load("pixel/bevy_pixel_dark.png")), - Transform::from_xyz(-45., 20., 2.), + Transform::from_xyz(-45., 20., 0.), + bevy::sprite::ZIndex(2), Rotate, PIXEL_PERFECT_LAYERS, )); @@ -62,7 +63,8 @@ fn setup_sprite(mut commands: Commands, asset_server: Res<AssetServer>) { // The sample sprite that will be rendered to the high-res "outer world" commands.spawn(( Sprite::from_image(asset_server.load("pixel/bevy_pixel_light.png")), - Transform::from_xyz(-45., -20., 2.), + Transform::from_xyz(-45., -20., 0.), + bevy::sprite::ZIndex(2), Rotate, HIGH_RES_LAYERS, )); @@ -77,7 +79,8 @@ fn setup_mesh( commands.spawn(( Mesh2d(meshes.add(Capsule2d::default())), MeshMaterial2d(materials.add(Color::BLACK)), - Transform::from_xyz(25., 0., 2.).with_scale(Vec3::splat(32.)), + Transform::from_xyz(25., 0., 0.).with_scale(Vec3::splat(32.)), + bevy::sprite::ZIndex(2), Rotate, PIXEL_PERFECT_LAYERS, )); diff --git a/examples/2d/sprite_sorting.rs b/examples/2d/sprite_sorting.rs new file mode 100644 index 0000000000000..d36e5e2d5fef8 --- /dev/null +++ b/examples/2d/sprite_sorting.rs @@ -0,0 +1,235 @@ +//! Demonstrates sprite rendering order using `ZIndex`, `YSort`, and `SortBias` components. +//! +//! This example shows how different sorting methods interact: +//! - `bevy::sprite::ZIndex(i32)`: Absolute rendering order (higher = on top) +//! - `YSort`: Automatic sorting based on Y position (lower Y = behind) +//! - `SortBias`: Fine-tune sorting without changing actual position + +use bevy::sprite::YSort; +use bevy::{prelude::*, sprite::SortBias}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, (move_sprites, update_info_text)) + .run(); +} + +#[derive(Component)] +struct Movable { + label: String, +} + +#[derive(Component)] +struct InfoText; + +fn setup(mut commands: Commands) { + commands.spawn(Camera2d); + + commands.spawn(( + Text::new( + "WASD: Move white sprite | Q/E: Adjust sort bias\n\ + Hover over sprites to see their sorting properties", + ), + Node { + position_type: PositionType::Absolute, + top: Val::Px(12.0), + left: Val::Px(12.0), + ..default() + }, + InfoText, + )); + + // Left + + commands.spawn(( + Sprite { + color: Color::srgb(0.8, 0.2, 0.2), + custom_size: Some(Vec2::splat(60.0)), + ..default() + }, + Transform::from_translation(Vec3::new(-300.0, 50.0, 0.0)), + bevy::sprite::ZIndex(10), + )); + + commands.spawn(( + Sprite { + color: Color::srgb(0.2, 0.2, 0.8), + custom_size: Some(Vec2::splat(60.0)), + ..default() + }, + Transform::from_translation(Vec3::new(-280.0, 30.0, 0.0)), + bevy::sprite::ZIndex(5), + )); + + commands.spawn(( + Sprite { + color: Color::srgb(0.2, 0.8, 0.2), + custom_size: Some(Vec2::splat(60.0)), + ..default() + }, + Transform::from_translation(Vec3::new(-260.0, 70.0, 0.0)), + bevy::sprite::ZIndex(15), + )); + + // Center + + for i in 0..3 { + let y = -50.0 + i as f32 * 40.0; + commands.spawn(( + Sprite { + color: Color::srgb(0.9, 0.5, 0.1), + custom_size: Some(Vec2::splat(60.0)), + ..default() + }, + Transform::from_translation(Vec3::new(-50.0 + i as f32 * 20.0, y, 0.0)), + YSort, + )); + } + + // Right + + commands.spawn(( + Sprite { + color: Color::srgb(0.6, 0.2, 0.8), + custom_size: Some(Vec2::splat(60.0)), + ..default() + }, + Transform::from_translation(Vec3::new(200.0, 0.0, 0.0)), + YSort, + SortBias(-20.0), + )); + + commands.spawn(( + Sprite { + color: Color::srgb(0.2, 0.8, 0.8), + custom_size: Some(Vec2::splat(60.0)), + ..default() + }, + Transform::from_translation(Vec3::new(220.0, 10.0, 0.0)), + YSort, + SortBias(20.0), + )); + + commands.spawn(( + Sprite { + color: Color::srgb(0.8, 0.8, 0.2), + custom_size: Some(Vec2::splat(60.0)), + ..default() + }, + Transform::from_translation(Vec3::new(240.0, 0.0, 0.0)), + YSort, + )); + + // Moveable sprite + commands.spawn(( + Sprite { + color: Color::WHITE, + custom_size: Some(Vec2::splat(50.0)), + ..default() + }, + Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)), + YSort, + SortBias(0.0), + Movable { + label: "Movable (YSort + Bias: 0)".to_string(), + }, + )); + + // Background grid + for x in -4..=4 { + for y in -3..=3 { + commands.spawn(( + Sprite { + color: Color::srgba(0.3, 0.3, 0.3, 0.1), + custom_size: Some(Vec2::splat(30.0)), + ..default() + }, + Transform::from_translation(Vec3::new(x as f32 * 100.0, y as f32 * 100.0, 0.0)), + bevy::sprite::ZIndex(-100), // Always behind everything + )); + } + } + + // Labels + + commands.spawn(( + Text::new("ZIndex Only"), + Node { + position_type: PositionType::Absolute, + left: Val::Px(300.0), + bottom: Val::Px(50.0), + ..default() + }, + bevy::sprite::ZIndex(1000), + )); + + commands.spawn(( + Text::new("YSort Only"), + Node { + position_type: PositionType::Absolute, + left: Val::Px(550.0), + bottom: Val::Px(50.0), + ..default() + }, + bevy::sprite::ZIndex(1000), + )); + + commands.spawn(( + Text::new("YSort + SortBias"), + Node { + position_type: PositionType::Absolute, + right: Val::Px(300.0), + bottom: Val::Px(50.0), + ..default() + }, + bevy::sprite::ZIndex(1000), + )); +} + +fn move_sprites( + mut query: Query<(&mut Transform, &mut SortBias, &mut Movable)>, + keyboard: Res<ButtonInput<KeyCode>>, + time: Res<Time>, +) { + let speed = 150.0; + let bias_speed = 50.0; + let delta = time.delta_secs(); + + for (mut transform, mut sort_bias, mut movable) in &mut query { + if keyboard.pressed(KeyCode::KeyA) { + transform.translation.x -= speed * delta; + } + if keyboard.pressed(KeyCode::KeyD) { + transform.translation.x += speed * delta; + } + if keyboard.pressed(KeyCode::KeyW) { + transform.translation.y += speed * delta; + } + if keyboard.pressed(KeyCode::KeyS) { + transform.translation.y -= speed * delta; + } + + // Adjust sort bias + if keyboard.pressed(KeyCode::KeyQ) { + sort_bias.0 -= bias_speed * delta; + } + if keyboard.pressed(KeyCode::KeyE) { + sort_bias.0 += bias_speed * delta; + } + + // Update label + movable.label = format!( + "Movable (Y: {:.0}, Bias: {:.0})", + transform.translation.y, sort_bias.0 + ); + } +} + +fn update_info_text(mut text: Single<&mut Text, With<InfoText>>, movable: Single<&Movable>) { + text.0 = format!( + "WASD: Move white sprite | Q/E: Adjust sort bias\n{}", + movable.label + ); +} diff --git a/examples/README.md b/examples/README.md index 4f5030563aa7d..2411972462293 100644 --- a/examples/README.md +++ b/examples/README.md @@ -126,6 +126,7 @@ Example | Description [Sprite Scale](../examples/2d/sprite_scale.rs) | Shows how a sprite can be scaled into a rectangle while keeping the aspect ratio [Sprite Sheet](../examples/2d/sprite_sheet.rs) | Renders an animated sprite [Sprite Slice](../examples/2d/sprite_slice.rs) | Showcases slicing sprites into sections that can be scaled independently via the 9-patch technique +[Sprite Sorting](../examples/2d/sprite_sorting.rs) | Demonstrates how to sort sprites [Sprite Tile](../examples/2d/sprite_tile.rs) | Renders a sprite tiled in a grid [Text 2D](../examples/2d/text2d.rs) | Generates text in 2D [Texture Atlas](../examples/2d/texture_atlas.rs) | Generates a texture atlas (sprite sheet) from individual sprites