bevyengine / bevyengine/bevy

`TargetCamera` breaks UI mouse interaction

Open
#12,836 6 comments 2 reactions 0 assignees View on GitHub
A-Picking A-UI C-Bug
Dominant language
Rust
Stars
48.2k
Forks
4.8k
Avg merge
3d 16h
Merged PRs (30d)
171

Description

## Bevy version

`0.13.1`

## What you did

This is a combination of [2d/pixel_grid_snap.rs](https://github.com/bevyengine/bevy/blob/release-0.13.1/examples/2d/pixel_grid_snap.rs) and [ui/button.rs](https://github.com/bevyengine/bevy/blob/release-0.13.1/examples/ui/button.rs) from the examples:

```rust
use bevy::{
prelude::*,
render::{
camera::RenderTarget,
render_resource::{
Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
},
view::RenderLayers,
},
window::WindowResized,
};

/// In-game resolution width.
const RES_WIDTH: u32 = 160;

/// In-game resolution height.
const RES_HEIGHT: u32 = 90;

/// Default render layers for pixel-perfect rendering.
/// You can skip adding this component, as this is the default.
const PIXEL_PERFECT_LAYERS: RenderLayers = RenderLayers::layer(0);

/// Render layers for high-resolution rendering.
const HIGH_RES_LAYERS: RenderLayers = RenderLayers::layer(1);

fn main() {
App::new()
.add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()))
.insert_resource(Msaa::Off)
.add_systems(Startup, (setup_camera, setup_ui).chain())
.add_systems(Update, (button_system, fit_canvas))
.run();
}

/// Low-resolution texture that contains the pixel-perfect world.
/// Canvas itself is rendered to the high-resolution world.
#[derive(Component)]
struct Canvas;

/// Camera that renders the pixel-perfect world to the [`Canvas`].
#[derive(Component)]
struct InGameCamera;

/// Camera that renders the [`Canvas`] (and other graphics on [`HIGH_RES_LAYERS`]) to the screen.
#[derive(Component)]
struct OuterCamera;

/// Our button
#[derive(Component)]
struct MyButton;

fn setup_ui(mut commands: Commands, query: Query>) {
commands
.spawn((
NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
..default()
},
TargetCamera(query.single()),
))
.with_children(|parent| {
parent
.spawn((
MyButton,
ButtonBundle {
style: Style {
width: Val::Px(50.0),
height: Val::Px(20.0),
border: UiRect::all(Val::Px(2.0)),
// horizontally center child text
justify_content: JustifyContent::Center,
// vertically center child text
align_items: AlignItems::Center,
..default()
},
border_color: BorderColor(Color::BLACK),
background_color: BackgroundColor(Color::rgb(0.15, 0.15, 0.15)),
..default()
},
))
.with_children(|parent| {
parent.spawn(TextBundle::from_section(
"Button",
TextStyle {
font_size: 10.0,
color: Color::rgb(0.9, 0.9, 0.9),
..default()
},
));
});
});
}

fn setup_camera(mut commands: Commands, mut images: ResMut>) {
let canvas_size = Extent3d {
width: RES_WIDTH,
height: RES_HEIGHT,
..default()
};

// this Image serves as a canvas representing the low-resolution game screen
let mut canvas = Image {
texture_descriptor: TextureDescriptor {
label: None,
size: canvas_size,
dimension: TextureDimension::D2,
format: TextureFormat::Bgra8UnormSrgb,
mip_level_count: 1,
sample_count: 1,
usage: TextureUsages::TEXTURE_BINDING
| TextureUsages::COPY_DST
| TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
},
..default()
};

// fill image.data with zeroes
canvas.resize(canvas_size);

let image_handle = images.add(canvas);

// this camera renders whatever is on `PIXEL_PERFECT_LAYERS` to the canvas
commands.spawn((
Camera2dBundle {
camera: Camera {
// render before the "main pass" camera
order: -1,
target: RenderTarget::Image(image_handle.clone()),
..default()
},
..default()
},
InGameCamera,
PIXEL_PERFECT_LAYERS,
));

// spawn the canvas
commands.spawn((
SpriteBundle {
texture: image_handle,
..default()
},
Canvas,
HIGH_RES_LAYERS,
));

// the "outer" camera renders whatever is on `HIGH_RES_LAYERS` to the screen.
// here, the canvas and one of the sample sprites will be rendered by this camera
commands.spawn((Camera2dBundle::default(), OuterCamera, HIGH_RES_LAYERS));
}

fn button_system(mut interaction_query: Query<&Interaction, (Changed, With)>) {
for interaction in &mut interaction_query {
match *interaction {
Interaction::Pressed => {
println!("pressed!");
}
Interaction::Hovered => {
println!("hovered!");
}
Interaction::None => {
println!("none!");
}
}
}
}

fn fit_canvas(
mut resize_events: EventReader,
mut projections: Query<&mut OrthographicProjection, With>,
) {
for event in resize_events.read() {
let h_scale = event.width / RES_WIDTH as f32;
let v_scale = event.height / RES_HEIGHT as f32;
let mut projection = projections.single_mut();
projection.scale = 1. / h_scale.min(v_scale).round();
}
}
```

## What went wrong

Button interaction simply does not work at all. Hovering and clicking the button should cause `hovered!` and `pressed!` to be printed to the console, but it does nothing here.

I believe this is a **critical** issue for pixel-art games as this prevents mouse interaction from working *at all*. I hope that there is at least a workaround for such a show-stopping bug.

## Additional information

Some relevant PRs:
- https://github.com/bevyengine/bevy/pull/10559
- https://github.com/bevyengine/bevy/pull/12502
- https://github.com/bevyengine/bevy/pull/5815

Relevant issues:
- https://github.com/bevyengine/bevy/issues/5476
- https://github.com/bevyengine/bevy/issues/5479

Contributor guide

Open the contributing guide

Research direction

Reproduce the issue by combining 2d/pixel_grid_snap.rs with ui/button.rs from the Bevy 0.13.1 examples. Start by tracing how TargetCamera and UI interaction behave in that setup, then read the linked pull requests and related issues. Done means the button reports hovered and pressed interactions while the pixel-perfect canvas remains functional.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
computer-graphics, frontend, game-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.