Skip to content

Sprite flickering #25163

Description

@chillgames-dev

Bevy version and system info

  • bevy = { version = "0.19.0", default-features = false, features = ["2d", "bevy_ui", "bevy_ui_render"] }
  • cargo 1.98.0-nightly (fbb61be30 2026-05-26)
  • Tested on ios and macOs

If your bug is rendering-related, copy the adapter info that appears when you run Bevy.

AdapterInfo { name: "Apple M1 Pro", vendor: 0, device: 0, device_type: IntegratedGpu, device_pci_bus_id: "", driver: "", driver_info: "", backend: Metal, subgroup_min_size: 4, subgroup_max_size: 64, transient_saves_memory: true }

What you did

Reproduction

put this asset in assets/ Image

use bevy::prelude::*;
use bevy::sprite::Anchor;
use bevy::window::WindowResolution;

const GAME_SCALE: f32 = 8.0;
const WALL_COLUMNS: i32 = 12;
const WALL_ROWS: i32 = 4;
const Z_BASE: f32 = 5_000.0;
const Z_ROW_SPACING: f32 = 100.0;
const Z_LAYER_SPACING: f32 = 10.0;

#[derive(States, Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
enum ReproState {
    #[default]
    Menu,
    Gameplay,
}

#[derive(Resource)]
struct ReproAssets {
    level_image: Handle<Image>,
    tall_layout: Handle<TextureAtlasLayout>,
    square_layout: Handle<TextureAtlasLayout>,
    shadow_material: Handle<ColorMaterial>,
    shadow_mesh: Handle<Mesh>,
}

fn main() {
    App::new()
        .add_plugins(
            DefaultPlugins
                .set(ImagePlugin::default_nearest())
                .set(WindowPlugin {
                    primary_window: Some(Window {
                        title: "Bevy 0.19 state-transition flicker reproduction".into(),
                        resolution: WindowResolution::new(440, 800),
                        resizable: false,
                        ..default()
                    }),
                    ..default()
                }),
        )
        .insert_resource(ClearColor(Color::srgb(0.025, 0.03, 0.045)))
        .init_state::<ReproState>()
        .add_systems(Startup, setup)
        .add_systems(Update, transition_on_input)
        .add_systems(OnEnter(ReproState::Gameplay), spawn_gameplay)
        .add_systems(OnEnter(ReproState::Menu), announce_menu)
        .run();
}

fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
    mut meshes: ResMut<Assets<Mesh>>,
) {
    commands.spawn((
        Name::new("Persistent Camera"),
        Camera2d,
        Msaa::Off,
        Projection::Orthographic(OrthographicProjection {
            near: -100_000.0,
            far: 100_000.0,
            scale: 0.18,
            ..OrthographicProjection::default_2d()
        }),
        Transform::from_xyz(
            (WALL_COLUMNS - 1) as f32 * GAME_SCALE * 0.5,
            (WALL_ROWS - 1) as f32 * GAME_SCALE * 0.5,
            0.0,
        ),
        IsDefaultUiCamera,
    ));

    commands.spawn((
        Name::new("Instructions"),
        Text::new("press space to toggle between gameplay and menu"),
        TextFont::from_font_size(24.0),
        TextColor(Color::WHITE),
        TextLayout::justify(Justify::Center),
        Node {
            position_type: PositionType::Absolute,
            top: Val::Px(20.0),
            width: Val::Percent(100.0),
            ..default()
        },
    ));

    commands.insert_resource(ReproAssets {
        level_image: asset_server.load("level_example.png"),
        tall_layout: atlas_layouts.add(TextureAtlasLayout::from_grid(
            UVec2::new(64, 128),
            8,
            3,
            None,
            None,
        )),
        square_layout: atlas_layouts.add(TextureAtlasLayout::from_grid(
            UVec2::new(64, 64),
            8,
            6,
            None,
            None,
        )),
        shadow_material: materials.add(Color::srgba(0.0, 0.0, 0.0, 0.3)),
        shadow_mesh: meshes.add(Rectangle::new(GAME_SCALE * 0.65, GAME_SCALE * 0.45)),
    });
}

fn transition_on_input(
    keys: Res<ButtonInput<KeyCode>>,
    mouse: Res<ButtonInput<MouseButton>>,
    state: Res<State<ReproState>>,
    mut next_state: ResMut<NextState<ReproState>>,
) {
    if !keys.just_pressed(KeyCode::Space) && !mouse.just_pressed(MouseButton::Left) {
        return;
    }

    let next = match state.get() {
        ReproState::Menu => ReproState::Gameplay,
        ReproState::Gameplay => ReproState::Menu,
    };
    info!("Switching from {:?} to {next:?}", state.get());
    next_state.set(next);
}

fn announce_menu() {
    info!("Entered Menu; board contents should now be despawned");
}

fn spawn_gameplay(mut commands: Commands, assets: Res<ReproAssets>) {
    let game_board = commands
        .spawn((
            Name::new("Game Board"),
            DespawnOnExit(ReproState::Gameplay),
            Transform::default(),
            Visibility::Inherited,
        ))
        .id();

    let environment = commands
        .spawn((
            Name::new("Board Environment"),
            Transform::default(),
            Visibility::Inherited,
        ))
        .id();
    commands.entity(game_board).add_child(environment);

    commands.entity(environment).with_children(|parent| {
        for y in 0..WALL_ROWS {
            for x in 0..WALL_COLUMNS {
                let mut floor_transform = build_transform(x, y, 0.0);
                floor_transform.translation.z = -2.0;
                parent.spawn((
                    floor_transform,
                    Visibility::Inherited,
                    children![square_sprite(
                        assets.level_image.clone(),
                        assets.square_layout.clone(),
                        16,
                    )],
                ));
            }
        }

        for y in 0..WALL_ROWS {
            for x in 0..WALL_COLUMNS {
                let wall_index = x.rem_euclid(2) as usize;
                parent.spawn((
                    build_transform(x, y, -10.5),
                    Visibility::Inherited,
                    children![tall_sprite(
                        assets.level_image.clone(),
                        assets.tall_layout.clone(),
                        wall_index,
                    )],
                ));

                if x % 3 == 0 {
                    parent.spawn((
                        build_transform(x, y, 1.0),
                        Visibility::Inherited,
                        children![tall_sprite(
                            assets.level_image.clone(),
                            assets.tall_layout.clone(),
                            2 + (x as usize % 6),
                        )],
                    ));
                }
            }
        }

        for x in 0..WALL_COLUMNS {
            let owner = build_transform(x, 1, 4.0);
            parent.spawn((
                owner,
                Visibility::Inherited,
                children![
                    (
                        Mesh2d(assets.shadow_mesh.clone()),
                        MeshMaterial2d(assets.shadow_material.clone()),
                        Transform::from_xyz(0.0, -GAME_SCALE * 0.05, -owner.translation.z - 1.0),
                    ),
                    square_sprite(assets.level_image.clone(), assets.square_layout.clone(), 8,),
                ],
            ));
        }
    });
}

fn build_transform(x: i32, y: i32, layer: f32) -> Transform {
    Transform::from_xyz(
        GAME_SCALE * x as f32,
        GAME_SCALE * y as f32,
        Z_BASE - y as f32 * Z_ROW_SPACING + layer * Z_LAYER_SPACING,
    )
}

fn tall_sprite(
    image: Handle<Image>,
    layout: Handle<TextureAtlasLayout>,
    index: usize,
) -> (Anchor, Sprite) {
    (
        Anchor::from(Vec2::new(0.0, -0.25)),
        Sprite {
            image,
            texture_atlas: Some(TextureAtlas { layout, index }),
            custom_size: Some(Vec2::new(GAME_SCALE, GAME_SCALE * 2.0)),
            ..default()
        },
    )
}

fn square_sprite(image: Handle<Image>, layout: Handle<TextureAtlasLayout>, index: usize) -> Sprite {
    Sprite {
        image,
        texture_atlas: Some(TextureAtlas { layout, index }),
        custom_size: Some(Vec2::splat(GAME_SCALE)),
        ..default()
    }
}

What went wrong

Sprites start flickering

Screen.Recording.2026-07-25.at.9.22.49.PM.mov

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-RenderingDrawing game state to the screenC-BugAn unexpected or incorrect behaviorP-RegressionFunctionality that used to work but no longer does. Add a test for this!S-Needs-InvestigationThis issue requires detective work to figure out what's going wrong

    Type

    No type

    Projects

    Status
    Needs SME Triage

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions