bevy-persistent-windows
A Bevy plugin to easily create and manage windows that remember where they were.
Background
When you're developing a game, thus frequently restarting it, you may (understandably) desire that the windows just stay where they were in the last run. Implementing this manually in every project you create is error-prone and time-consuming (trust me, I know). This plugin aims to make it as seamless as possible!
Installation
We'll be using bevy-persistent to store window states persistently, so let's add it first:
cargo add bevy-persistent --features all # you probably don't need all features, see installation section of bevy-persistent to learn more
Now let's add bevy-persistent-windows to make our windows persistent:
cargo add bevy-persistent-windows
Usage
Prelude
As mentioned before, we'll be using bevy-persistent to store the window state persistently, so lets prelude it first:
use bevy_persistent::prelude::*;
We need WindowState, PersistentWindowBundle and PersistentWindowsPlugin types to use the library, and they are exported from the prelude module:
use bevy_persistent_windows::prelude::*;
Setup
Let's start by creating an App
within main
:
let mut app = App::new();
We'll add the default plugins to this app, but we should edit the window plugin to avoid creating a default primary window:
let window_plugin = WindowPlugin { primary_window: None, ..Default::default() };
app.add_plugins(DefaultPlugins.set(window_plugin).build());
We need somewhere to store the window state, to restore the window later:
let state_directory = dirs::data_dir()
.expect("failed to get the platforms data directory")
.join("your-amazing-game")
.join("state");
Time to create the primary window:
app.world.spawn((
PrimaryWindow,
PersistentWindowBundle {
window: Window { title: "I am the primary window!".to_owned(), ..Default::default() },
state: Persistent::<WindowState>::builder()
.name("primary window state")
.format(StorageFormat::Toml)
.path(state_directory.join("primary-window.toml"))
.default(WindowState::windowed(1280, 720))
.revertible(true)
.revert_to_default_on_deserialization_errors(true)
.build()
.expect("failed to create the persistent primary window state"),
},
));
Feel free to spawn additional windows, without the PrimaryWindow
component of course!
Once, you're done, you can add PersistentWindowsPlugin
plugin to the app:
app.add_plugins(PersistentWindowsPlugin);
And run your game:
app.run();
You'll see a 1280x720
window appear in the center of your best monitor, move it around, resize, and play with it. Now close the application, and run it again. You'll see that the window will open in the exact same monitor, with the exact same resolution, and the exact same position!
See examples/setup.rs for the full example!
Controlling
You may wish to control the persistent windows programmatically. You can edit the window itself, but if you want your changes to persist, you should modify the window state directly!
Say you want to toggle fullscreen when space bar is pressed, you can add this system to your app:
fn fullscreen_toggle(
keyboard_input: Res<Input<KeyCode>>,
mut query: Query<&mut Persistent<WindowState>, With<PrimaryWindow>>,
) {
if keyboard_input.just_pressed(KeyCode::Space) {
let mut primary_window_state = query.get_single_mut().unwrap();
if primary_window_state.mode == WindowMode::Windowed {
primary_window_state.mode = WindowMode::Fullscreen;
} else {
primary_window_state.mode = WindowMode::Windowed;
}
primary_window_state.persist().ok();
}
}
Or if you want to move the window with arrow keys, and resize it with ctrl + arrow keys, you can use this system:
fn movement(
time: Res<Time>,
keyboard_input: Res<Input<KeyCode>>,
mut query: Query<&mut Persistent<WindowState>, With<PrimaryWindow>>,
) {
let mut position_change = (0.0, 0.0);
let mut resolution_change = (0.0, 0.0);
let change = if keyboard_input.pressed(KeyCode::ControlLeft) {
&mut resolution_change
} else {
&mut position_change
};
if keyboard_input.pressed(KeyCode::Up) {
change.1 -= 3.0 * time.delta().as_millis() as f32;
}
if keyboard_input.pressed(KeyCode::Left) {
change.0 -= 3.0 * time.delta().as_millis() as f32;
}
if keyboard_input.pressed(KeyCode::Down) {
change.1 += 3.0 * time.delta().as_millis() as f32;
}
if keyboard_input.pressed(KeyCode::Right) {
change.0 += 3.0 * time.delta().as_millis() as f32;
}
if position_change == (0.0, 0.0) && resolution_change == (0.0, 0.0) {
return;
}
let mut primary_window_state = query.get_single_mut().unwrap();
if let Some(resolution) = &mut primary_window_state.resolution {
resolution.0 = ((resolution.0 as f32) + (resolution_change.0)) as u32;
resolution.1 = ((resolution.1 as f32) + (resolution_change.1)) as u32;
}
if let Some(position) = &mut primary_window_state.position {
position.0 += position_change.0 as i32;
position.1 += position_change.1 as i32;
}
}
(ps: anyone wants to write a snake clone using persistent windows?)
See examples/controlling.rs for the full example!
Spawning
When you want to spawn additional windows, you can just spawn a new PersistentWindowBundle, just like you did in the setup:
fn spawn_persistent_window(
mut commands: Commands,
persistent_windows: Query<(), (With<Persistent<WindowState>>, With<Window>)>,
) {
let state_directory = dirs::data_dir()
.expect("failed to get the platforms data directory")
.join("your-amazing-game")
.join("state");
let persistent_window_count = persistent_windows.iter().len();
commands.spawn((
PersistentWindowBundle {
window: Window {
title: format!("I am #{}", persistent_window_count),
..Default::default()
},
state: Persistent::<WindowState>::builder()
.name(format!("window #{} state", persistent_window_count))
.format(StorageFormat::Toml)
.path(state_directory.join(format!("window-{}.toml", persistent_window_count)))
.default(WindowState::windowed(400, 400))
.revertible(true)
.revert_to_default_on_deserialization_errors(true)
.build()
.unwrap_or_else(|error| {
panic!(
"failed to create the persistent window #{} state: {}",
persistent_window_count, error
)
}),
},
));
}
When this system runs, it'll create a 400x400
persistent window in the center of your monitor.
See examples/spawning.rs for the full example!
Examples
You can run all the mentioned examples using:
cargo run --release --example name-of-the-example
Limitations
- If you're a psychopath who like to put your window half in one monitor and half in another, I have bad news. Bevy clips the windows to the monitor they're in, so the window state cannot be restored entirely.
- Best monitor cannot be decided for persistent windows that are spawned after application starts running. This is because WinitPlugin removes the EventLoop from the world before application starts running and without the event loop, I couldn't find a way to get the available monitors.
Please let me know if you know ways to get around these limitations!
License
bevy-persistent-windows is free, open source and permissively licensed, just like Bevy!
All code in this repository is dual-licensed under either:
- MIT License (LICENSE-MIT or https://opensource.org/licenses/MIT)
- Apache License, Version 2.0 (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)
This means you can select the license you prefer!