A Bevy plugin to easily create and manage windows that remember where they were.

Overview

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:

This means you can select the license you prefer!

You might also like...
Crossterm plugin for the bevy game engine
Crossterm plugin for the bevy game engine

What is bevy_crossterm? bevy_crossterm is a Bevy plugin that uses crossterm as a renderer. It provides custom components and events which allow users

A Bevy plugin to use Kira for game audio

Bevy Kira audio This bevy plugin is intended to try integrating Kira into Bevy. The end goal would be to replace or update bevy_audio, if Kira turns o

A prototype plugin providing a simple line drawing api for bevy.
A prototype plugin providing a simple line drawing api for bevy.

bevy_debug_lines A prototype plugin providing a simple line drawing api for bevy. See docs.rs for documentation. Expect breakage on master. Click on t

A sprite-sheet animation plugin for bevy
A sprite-sheet animation plugin for bevy

Benimator A sprite sheet animation plugin for bevy Features A SpriteSheetAnimation component to automatically update the indices of the TextureAtlasSp

A Bevy Engine plugin for making 2D paths, smooth animations with Bezier curves
A Bevy Engine plugin for making 2D paths, smooth animations with Bezier curves

bevy_pen_tool A Bevy Engine plugin for making 2D paths and smooth animations with Bezier curves TODO: Mesh-making functionality for building 2D shapes

A procedural sky plugin for bevy

bevy_atmosphere A procedural sky plugin for bevy Example use bevy::prelude::*; use bevy_atmosphere::*; fn main() { App::build() .insert_re

Generic cellular automaton plugin for bevy.
Generic cellular automaton plugin for bevy.

Bevy Cellular Automaton bevy_life is a generic plugin for cellular automaton. From the classic 2D Conway's game of life to WireWorld and 3D rules, the

Verlet physics plugin for bevy.
Verlet physics plugin for bevy.

bevy_verlet Simple Verlet points and sticks implementation for bevy. Features You can simply add a VerletPoint component on any entity with a Transfor

Bevy plugin for an AssetServer that can load embedded resources, or use other AssetServers based on the path.

Bevy-Embasset Embed your asset folder inside your binary. bevy-embasset adds support for loading assets embedded into the binary. Furthermore, it can

Comments
  • Fix link to respository

    Fix link to respository

    The link to the github repository was missing the final 's'. This makes the link on crates.io not work. I think this requires a new release to crates.io

    opened by FrTerstappen 4
Releases(v0.2.0)
  • v0.2.0(Aug 13, 2023)

    Breaking Changes

    • Default window state is changed from fullscreen to borderless fullscren
      • It's changed because fullscreen was panicking in Wayland

    New Features

    • Methods to create borderless fullscreen and sized fullscreen window states
      • Previously, creating those required creating another window state and modifying it
    Source code(tar.gz)
    Source code(zip)
  • v0.1.1(Aug 12, 2023)

    Chores

    • Repository URL in Cargo.toml is fixed
      • It was https://github.com/umut-sahin/bevy-persistent-window/ without the s, which was not correct
    Source code(tar.gz)
    Source code(zip)
  • v0.1.0(Aug 11, 2023)

Owner
Umut
METU Ceng\nProgramming since 2008
Umut
A Bevy helper to easily manage resources that need to persist across game sessions.

bevy-persistent A Bevy helper to easily manage resources that need to persist across game sessions. Background In games, there are a lot of resources

Umut 5 Mar 25, 2023
You have been an apprentice to a powerful sorcerer for as long as you can remember.

You have been an apprentice to a powerful sorcerer for as long as you can remember. However you dream of becoming a rancher, raising animals in your own pen.

null 3 Jun 22, 2022
Rust library to create a Good Game Easily

ggez What is this? ggez is a Rust library to create a Good Game Easily. The current version is 0.6.0-rc0. This is a RELEASE CANDIDATE version, which m

null 3.6k Jan 7, 2023
An attempt to create an easily customizable MMORPG game engine. Sky not included.

skyless Proof of concept of an easily customizable MMORPG game engine. Getting started Copy environment variables cp .env.example .env Start the engi

null 8 Nov 23, 2022
Bevy plugin helping with asset loading and organisation

Bevy asset loader This Bevy plugin reduces boilerplate when loading game assets. The crate offers the AssetCollection trait and can automatically load

Niklas Eicker 205 Jan 2, 2023
Bevy plugin to simulate and preview different types of Color Blindness.

Bevy Color Blindness Simulation Bevy plugin to simulate and preview different types of Color Blindness. This lets you ensure that your game is accessi

annie 29 Nov 22, 2022
Bevy plugin that does navmesh generation, pathfinding, and navigation for tilemaps

Bevy plugin that does navmesh generation, pathfinding, and navigation for tilemaps. Navmesh generation is available without Bevy dependency.

Seldom 13 Jan 31, 2023
A Bevy plugin for loading the LDtk 2D tile map format.

bevy_ldtk ( Tileset from "Cavernas" by Adam Saltsman ) A Bevy plugin for loading LDtk tile maps. Usage use bevy::prelude::*; use bevy_ldtk::*; fn mai

Katharos Technology 23 Jul 4, 2022
Inspector plugin for the bevy game engine

bevy-inspector-egui This crate provides the ability to annotate structs with a #[derive(Inspectable)], which opens a debug interface using egui where

Jakob Hellermann 517 Dec 31, 2022
A plugin for Egui integration into Bevy

bevy_egui This crate provides a Egui integration for the Bevy game engine. Features: Desktop and web (bevy_webgl2) platforms support Clipboard (web su

Vladyslav Batyrenko 453 Jan 3, 2023