Bevy Windows
A window is the thing our app gets rendered on.
Windows are an abstraction for interfacing with the operating system your app runs on. That means some settings will be platform specific.
Windows are provided through a collaboration between the bevy::window
and bevy::winit
crates. bevy::window
is a clean internal ECS representation of windows and bevy::winit
provides a platform specific implementation of the window event loop.
Before Bevy 0.10
, it used to manage our windows through a Windows
resource, but has since been changed to a component Window
.
Cross platform support is generally good except for wayland on nvidia GPUs.
Size matters
There are three sizes associated with a window:
- Physical Size which represents the actual height and width in physical pixels the window occupies on the monitor
- Logical Size which represents the size that should be used to scale elements inside the window, measured in logical pixels
- Requested Size measured in logical pixels, which is the value submitted to the API when creating the window, or requesting that it be resized.
The reason logical size and physical size are separated and can be different is to account for:
- Differences in monitor pixel densities
- Operating system settings for pixel densities
- Bevy
App
specified scale factor between both
The physical size is the base size, completely unscaled. When we apply the scale_factor
to our window we get the logical size.
The different sizes are handled through the WindowResolution
struct (not a component) which is on the Window
component. The default looks like:
WindowResolution {
physical_width: 1280,
physical_height: 720,
scale_factor_override: None,
scale_factor: 1.0,
}
You can use multiple windows on an app. By default, a PrimaryWindow
gets spawned by WindowPlugin
, contained in DefaultPlugins
.
The default plugin looks like:
WindowPlugin {
// Spawns this component with a PrimaryWindow component
primary_window: Some(Window::default()),
// Close our app when all windows close
exit_condition: ExitCondition::OnAllClosed,
// Should we close windows when the user does?
close_when_requested: true,
}
The plugin is adding some default systems for window handling and some events we can read from:
WindowResized
WindowCreated
WindowClosed
WindowCloseRequested
RequestRedraw
CursorMoved
CursorEntered
CursorLeft
Ime
WindowFocused
WindowScaleFactorChanged
WindowBackendScaleFactorChanged
FileDragAndDrop
WindowMoved
WindowThemeChanged
Bevy before 0.14
used to have ReceivedCharacter
, but it is now deprecated in favor of KeyboardInput
while winit
reworks their keyboard system.
Its common to want to exit your game with a hotkey when prototyping. Bevy used to expose a system (in < 0.14
) bevy::window::close_on_esc
that got removed. But its common to put a system like this into your game to close when pressing Esc
// Close the focused window whenever the escape key (Esc) is pressed
// This is useful for examples or prototyping.
pub fn close_on_esc(
mut commands: Commands,
focused_windows: Query<(Entity, &Window)>,
input: Res<ButtonInput<KeyCode>>,
) {
for (window, focus) in focused_windows.iter() {
if !focus.focused {
continue;
}
if input.just_pressed(KeyCode::Escape) {
commands.entity(window).despawn();
}
}
}
The Window
that got created can be queried just like our other components. When we hit the Esc key our app will close any focused windows by despawning their entities.
But in reality, the actual closing of the windows on your OS is done through the bevy::winit::despawn_window
system which reads our internal representation we create through bevy::window
(see further down the page).
The actual Window
component contains settings for how our windows should be positioned and rendered on the screen.
Windows control their rendering depending on the use case with a bevy::winit::WinitSettings
resource:
use bevy::winit::WinitSettings;
fn main() {
App::new()
// Set the background color of our window
.insert_resource(ClearColor(Color::srgb(0.5, 0.5, 0.9)))
// Continuous rendering for games - bevy's default.
.insert_resource(WinitSettings::game())
// Power-saving reactive rendering for applications.
.insert_resource(WinitSettings::desktop_app());
}
WininitSettings
is from another bevy crate bevy::winit
which handles actually creating the window on our OS. So there is a clean internal representation of your windows in bevy::window
but the reality is that every platform has its own way of creating windows.
So when your Window
is despawning its actually calling out to the despawn_windows
system provided by the WininitPlugin
.
We can initialize a Window
through the WindowPlugin
like:
use bevy::window::PresentMode;
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "I am a window!".into(),
resolution: WindowResolution::new(500., 300.).with_scale_factor_override(1.0),
present_mode: PresentMode::AutoVsync,
// Tells wasm to resize the window according to the available canvas
fit_canvas_to_parent: true,
// Tells wasm not to override default event handling, like F5, Ctrl+R etc.
prevent_default_event_handling: false,
..default()
}),
..default()
}))
}
Bevy lets us customize plugin sets like DefaultPlugins
which let us override the defaults of any core Bevy plugin like we do above.
Whatever we provide to the primary_window
field of the WindowPlugin
will become an entity with both whatever we passed in plus an additional PrimaryWindow
component.
This PrimaryWindow
marker component can be useful for managing the main window of your game if you have to manage multiple windows.
use bevy::window::PrimaryWindow;
fn get_window_dimensions(window_query: Query<&Window, With<PrimaryWindow>>) {
let window = window_query.single();
info!(
"The windows resolution is {:0.0} by {:0.0}",
window.resolution.width(),
window.resolution.height()
)
}
Reading and writing window data
Your Window
can be accessed in a Query
and its data can be modified just like your other components.
pub fn inspect_window(windows: Query<&Window>) {
for window in windows.iter() {
let focused = window.focused;
println!("Window is focused: {:?}", focused);
// The size after scaling:
let logical_width = window.width();
let logical_height = window.height();
println!("Logical size: {:?} x {:?}", logical_width, logical_height);
// The size before scaling:
let physical_width = window.physical_width();
let physical_height = window.physical_height();
println!(
"physical size: {:?} x {:?}",
physical_width, physical_height
);
// Cursor position in logical sizes, this would return None if our
// cursor is outside of the window:
if let Some(logical_cursor_position) = window.cursor_position() {
println!("Logical cursor position: {:?}", logical_cursor_position);
}
// Cursor position in physical sizes, this would return None if our
// cursor is outside of the window:
if let Some(physical_cursor_position) = window.physical_cursor_position() {
println!("Physical cursor position: {:?}", physical_cursor_position);
}
}
}
Changing the resolution
For example we can change the resolution by overriding certain settings:
// This system shows how to request the window to resize the resolution
fn adjust_resolution(
keys: Res<ButtonInput<KeyCode>>,
mut windows: Query<&mut Window>
) {
let mut window = windows.single_mut();
if keys.just_pressed(KeyCode::ArrowUp) {
let scale_factor_override = window.resolution.scale_factor_override();
window
.resolution
.set_scale_factor_override(scale_factor_override.map(|n| n + 1.0));
}
}
More commonly you would set up some kind of resolution settings where you could toggle between fixed sizes:
/// Stores the various window-resolutions we can select between.
#[derive(Resource)]
struct ResolutionSettings {
large: Vec2,
medium: Vec2,
small: Vec2,
}
fn toggle_resolution(
keys: Res<ButtonInput<KeyCode>>,
mut windows: Query<&mut Window>,
resolution: Res<ResolutionSettings>,
) {
let mut window = windows.single_mut();
if keys.just_pressed(KeyCode::Digit1) {
let res = resolution.small;
window.resolution.set(res.x, res.y);
}
if keys.just_pressed(KeyCode::Digit2) {
let res = resolution.medium;
window.resolution.set(res.x, res.y);
}
if keys.just_pressed(KeyCode::Digit3) {
let res = resolution.large;
window.resolution.set(res.x, res.y);
}
}
Updating text
We will also need to react to changes for things like Text
:
/// Marker component for the text that displays the current resolution.
#[derive(Component)]
struct ResolutionText;
// This system shows how to respond to a window being resized.
// Whenever the window is resized, the text will update with the new resolution.
fn on_resize_system(
mut q: Query<&mut Text, With<ResolutionText>>,
mut resize_reader: EventReader<WindowResized>,
) {
let mut text = q.single_mut();
for e in resize_reader.read() {
// When resolution is being changed
text.0 = format!("{:.1} x {:.1}", e.width, e.height);
}
}
Changing the window title:
// This system will then change the title during execution
fn change_title(mut windows: Query<&mut Window>, time: Res<Time>) {
let mut window = windows.single_mut();
window.title = format!(
"Seconds since startup: {}",
time.elapsed().as_secs_f32().round()
);
}
Cursors
A Cursor
is our interface to interacting with the mouse. Each cursor has a CursorGrabMode
which lets us define how a users cursor is grabbed by the window:
pub enum CursorGrabMode {
// The cursor can freely leave the window.
#[default]
None,
// The cursor is confined to the window area.
Confined,
// The cursor is locked inside the window area to a certain position.
Locked,
}
We can use this to toggle our cursors on certain actions in a game. We can imagine building an FPS and wanting our cursor to constantly place itself in the center to keep our mouse locked to the game screen during gameplay:
use bevy::window::CursorGrabMode;
fn toggle_cursor(
mut windows: Query<&mut Window>,
input: Res<ButtonInput<KeyCode>>
) {
if input.just_pressed(KeyCode::Space) {
let mut window = windows.single_mut();
window.cursor_options.visible = !window.cursor_options.visible;
window.cursor_options.grab_mode = match window.cursor_options.grab_mode {
CursorGrabMode::None => CursorGrabMode::Locked,
CursorGrabMode::Locked | CursorGrabMode::Confined => CursorGrabMode::None,
};
}
}
Reactive windows
We can initialize our windows to be reactive instead of continuous:
use bevy::utils::Duration;
fn main() {
App::new()
.insert_resource(WinitSettings {
focused_mode: bevy::winit::UpdateMode::Continuous,
unfocused_mode: bevy::winit::UpdateMode::reactive_low_power(Duration::from_millis(10)),
})
}
When your window is in UpdateMode::reactive_low_power
it will only re-render when it receives input. This is great for saving CPU when the app is not doing anything.
UpdateMode::Continuous
is the default and renders each tick of your app.
This is quite an all or nothing type of setting so a crate like bevy_framepace
can help give us even more control.
bevy_framepace
works in a similar way to FixedUpdate
systems where it measures the time that passes and triggers its render pipeline manually based on the delta.
Taking a screenshot
To take screenshots we spawn a Screenshot
component on the render target we are interested in capturing.
To actually save them we can use the save_to_disk
observer.
use bevy::render::view::screenshot::{save_to_disk, Screenshot};
fn take_screenshot(
mut commands: Commands,
input: Res<ButtonInput<KeyCode>>,
primary_window: Query<Entity, With<PrimaryWindow>>,
) {
if input.just_pressed(KeyCode::Space) {
let path = "screenshot.png";
commands
.spawn(Screenshot::primary_window())
.observe(save_to_disk(path));
}
}