A save/load framework for Bevy game engine.

Overview

💾 Moonshine Save

A save/load framework for Bevy game engine.

Overview

In Bevy, it is possible to serialize and deserialize a World using a DynamicScene (see example for details). While this is useful for scene management and editing, it has some problems when using it as-is for saving or loading game state.

The main issue is that in most common applications, the saved game data is a very minimal subset of the actual scene. Visual and aesthetic elements such as transforms, scene hierarchy, camera, or UI components are typically added to the scene during game start or entity initialization.

This crate aims to solve this issue by providing a framework and a collection of systems for selectively saving and loading a world to disk.

Usage

Save

In order to save game state, start by marking entities which must be saved using the Save marker. This is a component which can be added to bundles or inserted into entities like any other component:

// Saved components must derive `Reflect`
#[derive(Component, Default, Reflect)]
#[reflect(Component)]
struct Player;

#[derive(Component, Default, Reflect)]
#[reflect(Component)]
struct Level(u32);

#[derive(Bundle)]
struct PlayerBundle {
    player: Player,
    level: Level,
    name: Name,
    save: Save,
}

⚠️ Warning:
Components which are to be saved must derive Reflect. Otherwise, they are not saved.

Add SavePlugin and register your serialized components:

app.add_plugin(SavePlugin)
    .register_type::<Player>()
    .register_type::<Level>();

Finally, to invoke the save process, you must add a save pipeline. The default save pipeline is save_into_file:

app.add_system(save_into_file("saved.ron"));

When used on its own, save_into_file would save the world state on every application update. This is often undesirable because you typically want save to happen at specific times. To do this, you can combine save_into_file with run_if:

app.add_system(save_into_file("saved.ron").run_if(should_save));

fn should_save( /* ... */ ) -> bool {
    todo!()
}

Load

Before loading, mark your visual and aesthetic entities with Unload. Similar to Save, this is a marker which can be added to bundles or inserted into entities like a regular component. Any entity with Unload is despawned recursively prior to load.

#[derive(Bundle)]
struct PlayerSpriteBundle {
    /* ... */
    unload: Unload,
}

You should try to design your game logic to keep saved data separate from game visuals. This can be done by using systems which spawn visuals for saved game data:

#[derive(Component)] // <-- Does not derive Reflect, not saved!
struct PlayerSprite(Entity);

#[derive(Bundle)]
struct PlayerSpriteBundle {
    sprite: SpriteBundle,
    unload: Unload,
}

impl PlayerSpriteBundle {
    fn new() -> Self {
        todo!("create sprite bundle")
    }
}

fn spawn_player_visuals(query: Query<Entity, Added<Player>>, mut commands: Commands) {
    for entity in query.iter() {
        let sprite = PlayerSprite(commands.spawn(PlayerSpriteBundle::new()).id());
        commands.entity(entity).insert(sprite);
    }
}

Any saved components which reference entities must implement FromLoaded and be invoked during post load using component_from_loaded:

#[derive(Component, Default, Reflect)]
#[reflect(Component)]
struct PlayerWeapon(Option<Entity>);

impl FromLoaded for PlayerWeapon {
    fn from_loaded(old: Self, loaded: &Loaded) -> Self {
        Self(Option::from_loaded(loaded))
    }
}

...

app.add_system(component_from_loaded::<PlayerWeapon>());

Make sure LoadPlugin is added and your types are registered:

app.add_plugin(LoadPlugin)
    .register_type::<Player>()
    .register_type::<Level>();

Finally, to invoke the load process, you must add a load pipeline. The default load pipeline is load_from_file:

app.add_system(load_from_file("saved.ron"));

Similar to save_into_file, you typically want to use load_from_file with run_if:

app.add_system(load_from_file("saved.ron").run_if(should_load));

fn should_load( /* ... */ ) -> bool {
    todo!()
}

Example

See examples/army.rs for a minimal application which demonstrates how to save/load game state in detail.

Bevy Components

Some built-in Bevy components reference entities, most notably Parent and Children. Currently, this crate does not implement FromLoaded for these components. The rationale for this is that these components are often used for game aesthetics, rather than saved game data.

Ideally, your saved game data should be completely separate from the aesthetic elements.

Dynamic Save File Path

In the examples provided, the save file path is often static (i.e. known at compile time). However, in some applications, it may be necessary to save into a path selected at runtime. To solve this, you have to create a custom save pipeline.

Start by creating a mechanism to trigger the save request. You can use a Resource for this:

// Save request with a dynamic path
#[derive(Resource)]
struct SaveRequest {
    path: PathBuf
}

// Run criteria used to trigger the save pipeline
fn should_save(request: Option<Res<SaveRequest>>) -> bool {
    request.is_some()
}

// Finish the save pipeline by removing the request
fn remove_save_request(world: &mut World) {
    world.remove_resource::<SaveRequest>().unwrap();
}

Then implement the system responsible for handling the save request to write the saved data into the correct path:

fn into_dynamic_file(
    In(saved): In<Saved>,
    type_registry: Res<AppTypeRegistry>,
    request: Res<SaveRequest>
) -> Result<Saved, Error> {
    let data = saved.scene.serialize_ron(&type_registry)?;
    std::fs::write(&request.path, data.as_bytes())?;
    info!("saved into file: {path:?}");
    Ok(saved)
}

The example above is based on into_file.

Finally, define your save pipeline and register your systems:

fn save_into_dynamic_file() -> SystemConfig {
    save::<With<Save>>
        .pipe(into_dynamic_file)
        .pipe(finish)
        .in_set(SaveSet::Save)
}
app.add_systems(
    (save_into_dynamic_file(), remove_save_request)
        .chain()
        .distributive_run_if(should_save),
);

Configuration

This crate is designed to be modular and fully configurable. The default save/load pipelines (save_into_file and load_from_file) are composed of sub-systems which can be used individually in any desirable configuration with other systems. You may refer to their implementation for details on how this can be done.

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

Concise Reference Book for the Bevy Game Engine

Unofficial Bevy Cheat Book Click here to read the book! Concise reference to programming in the Bevy game engine. Covers useful syntax, features, prog

Proof-of-concept of getting OpenXR rendering support for Bevy game engine using gfx-rs abstractions
Proof-of-concept of getting OpenXR rendering support for Bevy game engine using gfx-rs abstractions

Introduction Proof-of-concept of getting OpenXR rendering support for Bevy game engine using gfx-rs abstractions. (hand interaction with boxes missing

A physics lib for the bevy game engine based on physme

physimple Physimple aims to be the simplest(and capable) physics engine(currently for bevy) WARNING Beware for breaking changes with each update for n

An immediate mode 2D drawing API for Bevy game engine

Bevy Canvas API prototype An unofficial immediate mode 2D drawing API for Bevy game engine. Check the documentation or the examples to know how to use

Simple RUST game with the Bevy Engine

Simple RUST Game using the Bevy Engine YouTube videos for this code base: Episode 1 - Rust Game Development tutorial from Scratch with Bevy Engine Epi

A vdom-free reactive UI lib for the bevy game engine

ui4 ui4 is my fourth major attempt at making a UI dataflow library for the Bevy game engine. More specifically, it's a vdom-less UI library which uses

Hanabi — a particle system plugin for the Bevy game engine.

Hanabi — a particle system plugin for the Bevy game engine

Brine is my attempt at writing a Minecraft client in Rust using the Bevy game engine.
Brine is my attempt at writing a Minecraft client in Rust using the Bevy game engine.

Brine Brine is my attempt at writing a Minecraft client in Rust using the Bevy game engine. It's EXTREMELY work-in-progress. The thing that makes Brin

Owner
null
Minecraft-esque voxel engine prototype made with the bevy game engine. Pending bevy 0.6 release to undergo a full rewrite.

vx_bevy A voxel engine prototype made using the Bevy game engine. Goals and features Very basic worldgen Animated chunk loading (ala cube world) Optim

Lucas Arriesse 125 Dec 31, 2022
Bevy Simple Portals is a Bevy game engine plugin aimed to create portals.

Portals for Bevy Bevy Simple Portals is a Bevy game engine plugin aimed to create portals. Those portals are (for now) purely visual and can be used t

Sélène Amanita 11 May 28, 2023
2d Endless Runner Game made with Bevy Game Engine

Cute-runner A 2d Endless Runner Game made with Bevy Game Engine. Table of contents Project Infos Usage Screenshots Disclaimer Project Infos Date: Sept

JoaoMarinho 2 Jul 15, 2022
A game of snake written in Rust using the Bevy game engine, targeting WebGL2

Snake using the Bevy Game Engine Prerequisites cargo install cargo-make Build and serve WASM version Set your local ip address in Makefile.toml (loca

Michael Dorst 0 Dec 26, 2021
A game made in one week for the Bevy engine's first game jam

¿Quien es el MechaBurro? An entry for the first Bevy game jam following the theme of "Unfair Advantage." It was made in one week using the wonderful B

mike 20 Dec 23, 2022
A Client/Server game networking plugin using QUIC, for the Bevy game engine.

Bevy Quinnet A Client/Server game networking plugin using QUIC, for the Bevy game engine. Bevy Quinnet QUIC as a game networking protocol Features Roa

Gilles Henaux 65 Feb 20, 2023
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

Johnny Tidemand Vestergaard 9 Aug 4, 2022
A Win32 GUI program which modifies save files from the game Freelancer (2003).

FL Save Convert A Win32 GUI program which modifies save files from the game Freelancer (2003). System Dependencies Your system will need the latest Mi

Devin Mutlu 3 Nov 15, 2022
Basic first-person fly camera for the Bevy game engine

bevy_flycam A basic first-person fly camera for Bevy 0.4 Controls WASD to move horizontally SPACE to ascend LSHIFT to descend ESC to grab/release curs

Spencer Burris 85 Dec 23, 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