mike chambers | about

Managing Window Size in Bevy

Sunday, October 30, 2022

Continuing my posts as I learn about the Bevy game engine for Rust, this post covers the basics of working with windows in Bevy, including how to set the initial size, and handle window resize events.

Setting The Initial Window Size

In order to set the inital window size, you need to specify the settings via a WindowDescriptor resource added before DefaultPlugins are added.

Here is some example init code that sets the window size to 400 x 400 and sets the title of the window:

fn main() {
    App::new()
        //add a WindowDescriptor resource. This will be used to set initial
        //height / width of window, as well as keep track of information and
        //data about the window
        .insert_resource(
            WindowDescriptor {
                width:400.0,
                height:400.0,
                title:"Window Resize Example".to_string(),
                ..default()
            }
        )
        .insert_resource(ClearColor(Color::ANTIQUE_WHITE))
        .add_plugins(DefaultPlugins)
        //initial setup
        .add_startup_system(setup)
        .run();
}

Again, note that you must specify the WindowDescriptor with the settings before you add the DefaultPlugins.

Handling Window Resizing

The WindowDescriptor is added as a regular resource which means you can access it later within your systems. However, if the window is resized, the WindowDescriptor resource will not automatically be updated with the new window settings. For that, we need to listen for the WindowResized event, and update the WindowDescriptor resource with the updated settings.

Here is the system we use to listen for the WindowResize event and update the WindowDescriptor resource properties:

fn on_window_resize(
    mut window: ResMut<WindowDescriptor>,
    mut resize_events: EventReader<WindowResized>) {

    for e in resize_events.iter() {
        window.height = e.height;
        window.width = e.width;
    } 
}

We also request a mutable instance of the WindowDescriptor resource, and then update its height / width with the new window size which are passed in via the WindowResized event.

Now when we access the WindowDescriptor in other systems, it should always have the correct window dimensions.

use bevy::prelude::*;
use bevy::window::WindowResized;

//Component to tag a marker
#[derive(Component)]
struct Marker;

fn main() {
    App::new()
        //add a WindowDescriptor resource. This will be used to set initial
        //height / width of window, as well as keep track of information and
        //data about the window
        .insert_resource(
            WindowDescriptor {
                width:400.0,
                height:400.0,
                title:"Window Resize Example".to_string(),
                ..default()
            }
        )
        .insert_resource(ClearColor(Color::ANTIQUE_WHITE))
        .add_plugins(DefaultPlugins)
        //initial setup
        .add_startup_system(setup)
        //window resize event listener
        .add_system(on_window_resize)
        //update position of marker, and make sure it runs after the window resize
        //event is handled
        .add_system(update_marker.after(on_window_resize))
        .run();
}

//help function that finds the bottom right coordinates of the window
fn find_bottom_right(window : &WindowDescriptor) -> Vec3 {
    Vec3::new(window.width / 2.0, window.height / -2.0, 0.0)
}

fn setup(mut commands:Commands, window:Res<WindowDescriptor>) {
    commands.spawn_bundle(Camera2dBundle::default());
   
    //Added a marker that sits in the bottom right of the window
    commands.spawn()
        .insert(Marker)
        .insert_bundle(SpriteBundle {
            sprite:Sprite {
                color: Color::MAROON,
                custom_size:Some(Vec2::new(40.0, 40.0)),

                //Since we are anchoring to bottom right, lets set anchor
                //point also to bottom right, so we dont have to offset the
                //coordinates
                anchor:bevy::sprite::Anchor::BottomRight,
                ..default()
            },
            transform:Transform { 
                translation: find_bottom_right(&window),
                ..default()
            },
            ..default()
        });
}

//system that updates the position of the marker
fn update_marker(
        mut query:Query<&mut Transform, With<Marker>>,
        window:Res<WindowDescriptor>
    ) {

    //If window size hasn't changed, then we can just exit out
    if !window.is_changed() {
        return;
    }

    //window resource has changed, so lets update the position
    for mut transform in &mut query {
        transform.translation = find_bottom_right(&window);
    }
}

//listener for window resize event
//we store the window info in the WindowDescriptor resource
fn on_window_resize(
    mut window: ResMut<WindowDescriptor>,
    mut resize_events: EventReader<WindowResized>) {

    for e in resize_events.iter() {
        window.height = e.height;
        window.width = e.width;
    } 
}

You can grab the project here.

First, pay attention to how we specify the update_marker system:

//update position of marker, and make sure it run after the window resize
//event is handled
.add_system(update_marker.after(on_window_resize))

We explicitly tell it to only run once on_window_resize has been handled. This is important as the systems runs in parallel and this makes sure the WindowDescriptor will be updated before we update the marker position.

Next, note how in the update_marker system, we first check to see if the WindowDescriptor resource has changed. If it hasn’t then we just exit out since we don’t need to update anything.

In the event listener, we are passed in a collection of events, and have to loop through them. This is to handle instances where an even may be broadcast multiple times in a frame, although in my testing I only ever seen one WindowResized event sent once per frame.

Finally, keep in mind there can be issues in some cases when using events within Bevy, which may require you to manually clear events. You can read more on some of the issues here.

twitter github flickr behance