| Crates.io | moonshine-save |
| lib.rs | moonshine-save |
| version | 0.5.2 |
| created_at | 2023-03-08 05:48:46.269261+00 |
| updated_at | 2025-07-16 03:20:52.539053+00 |
| description | Save/Load framework for Bevy |
| homepage | https://github.com/Zeenobit/moonshine_save |
| repository | https://github.com/Zeenobit/moonshine_save |
| max_upload_size | |
| id | 804260 |
| size | 233,990 |
A save/load framework for Bevy game engine.
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 is problematic when used for saving/loading the game state.
The main issue is that in most common applications, the saved game data is a very minimal subset of the whole 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 for selectively saving and loading a world.
This crate may be used separately, but is also included as part of 🍸 Moonshine Core.
use bevy::prelude::*;
use moonshine_save::prelude::*;
#[derive(Component, Default, Reflect)] // <-- Saved Components must derive `Reflect`
#[reflect(Component)]
#[require(Save)] // <-- Mark this Entity to be saved
pub struct MyComponent;
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins)
// Register saved components:
.register_type::<MyComponent>()
// Register default save/load observers:
.add_observer(save_on_default_event)
.add_observer(load_on_default_event);
/* ... */
}
fn save(mut commands: Commands) {
// Save default entities (with `Save` component) into a file
commands.trigger_save(SaveWorld::default_into_file("world.ron"));
}
fn load(mut commands: Commands) {
// Unload default entities (with `Unload` component) and load the world from a file
commands.trigger_load(LoadWorld::default_from_file("world.ron"));
}
The main design goal of this crate is to use concepts inspired from MVC (Model-View-Controller) architecture to separate the aesthetic elements of the game (the game "view") from its logical and saved state (the game "model"). This allows the application to treat the saved data as the singular source of truth for the entire game state.
To use this crate as intended, you should design your game logic with this separation in mind:
[!TIP] See 👁️ Moonshine View for a generic implementation of this pattern.
For example, suppose we want to represent a player character in a game.
Various components are used to store the logical state of the player, such as Health, Inventory, or Weapon.
Each player is represented using a 2D sprite, which presents the current visual state of the player.
Traditionally, we might have used a single entity (or a hierarchy) to reppresent the player. This entity would carry all the logical components, such as Health, in addition to its visual data, such as Sprite:
use bevy::prelude::*;
#[derive(Component)]
#[require(Health, Inventory, Weapon, Sprite)] // <-- Model + View
struct Player;
#[derive(Component, Default)]
struct Health;
#[derive(Component, Default)]
struct Inventory;
#[derive(Component, Default)]
struct Weapon;
An arguably better approach would be to store this data in a completely separate entity:
use bevy::prelude::*;
use moonshine_save::prelude::*;
#[derive(Component)]
#[require(Health, Inventory, Weapon)] // <-- Model
struct Player;
#[derive(Component, Default)]
struct Health;
#[derive(Component, Default)]
struct Inventory;
#[derive(Component, Default)]
struct Weapon;
#[derive(Component)]
#[require(Sprite)] // <-- View
struct PlayerView {
player: Entity
}
// Spawn `PlayerView` and associate it with the `Player` entity:
fn on_player_added(trigger: Trigger<OnAdd, Player>, mut commands: Commands) {
let player = trigger.target();
commands.spawn(PlayerView { player });
}
This approach may seem verbose at first, but it has several advantages:
Ultimately, it is up to you to decide if the additional complexity of this separation is beneficial to your project or not. This crate is not intended to be a general purpose save solution by default.
However, you can also extend the save/load pipeline by processing the saved or loaded data to suit your needs. See crate documentation for full details.
To save the game state, start by marking entities which must be saved using Save.
It is best to use this component as a requirement for your saved components:
use bevy::prelude::*;
use moonshine_save::prelude::*;
#[derive(Component, Default, Reflect)] // <-- Saved Components must derive `Reflect`
#[reflect(Component)]
#[require(Name, Level, Save)] // <-- Add Save as requirement
struct Player;
#[derive(Component, Default, Reflect)]
#[reflect(Component)]
struct Level(u32);
Using Save as a requirement ensures it is inserted automatically during the load process, since Save itself is never serialized (due to efficiency). However, you can insert the Save component manually if needed.
Note that Save marks the whole entity for saving. So you do NOT need it on every saved component.
Register your saved component/resource types and add a save event observer:
use bevy::prelude::*;
use moonshine_save::prelude::*;
#[derive(Component, Default, Reflect)]
#[reflect(Component)]
struct Level(u32);
let mut app = App::new();
app.register_type::<Level>()
.add_observer(save_on_default_event);
save_on_default_event is a default observer which saves all entities marked with Save component when a SaveWorld event is triggered.
Alternatively, you can use save_on with a custom SaveEvent for specialized save pipelines. See documentation for details.
To trigger a save, use trigger_save via Commands or World:
use bevy::prelude::*;
use moonshine_save::prelude::*;
fn request_save(mut commands: Commands) {
commands.trigger_save(SaveWorld::default_into_file("saved.ron"));
}
SaveWorld is a generic SaveEvent which allows you to:
See documentation for full details and examples.
Before loading, mark your visual and aesthetic entities ("view" entities) with Unload.
[!TIP] 👁️ Moonshine View does this automatically for all "view entities".
Similar to Save, this is a marker which can be added to bundles or inserted into entities like a regular component.
Any entity marked with Unload is despawned recursively before loading begins.
use bevy::prelude::*;
use moonshine_save::prelude::*;
#[derive(Component)]
#[require(Unload)] // <-- Mark this entity to be unloaded before loading
struct PlayerView;
You should design your game logic to keep saved data separate from game visuals.
Any saved components which reference entities must also derive MapEntities:
use bevy::prelude::*;
use moonshine_save::prelude::*;
#[derive(Component, MapEntities, Reflect)]
#[reflect(Component, MapEntities)] // <-- Derive and reflect MapEntities
struct PlayerWeapon(Entity);
Register your saved component/resource types and add a load event observer:
use bevy::prelude::*;
use moonshine_save::prelude::*;
let mut app = App::new();
app.add_observer(load_on_default_event);
load_on_default_event is a default observer which unloads all entities marked with [Unload] component and loads the saved without any further processing.
Alternatively, you can use load_on with a custom LoadEvent for specialized load pipelines. See documentation for details.
To trigger a load, use trigger_load via Commands or World:
use bevy::prelude::*;
use moonshine_save::prelude::*;
fn request_load(mut commands: Commands) {
commands.trigger_load(LoadWorld::default_from_file("saved.ron"));
}
LoadWorld is a generic LoadEvent which allows you to:
See documentation for full details and examples.
See examples/army.rs for a minimal application which demonstrates how to save/load game state in detail.
This crate does not support backwards compatibility, versioning, or validation.
This is because supporting these should be trivial using Required Components and Component Hooks.
Here is a simple example of how to "upgrade" a component from saved data:
use bevy::prelude::*;
use moonshine_save::prelude::*;
use bevy::ecs::component::HookContext;
use bevy::ecs::world::DeferredWorld;
#[derive(Component, Default, Reflect)]
#[reflect(Component)]
struct Old;
#[derive(Component, Default, Reflect)]
#[reflect(Component)]
#[component(on_insert = Self::upgrade)] // <-- Upgrade on insert
struct New;
impl New {
fn upgrade(mut world: DeferredWorld, ctx: HookContext) {
let entity = ctx.entity;
if world.entity(entity).contains::<Old>() {
world.commands().queue(move |world: &mut World| {
world.entity_mut(entity).insert(New).remove::<Old>();
})
}
}
}
You can also create specialized validator components to ensure validity:
use bevy::prelude::*;
use moonshine_save::prelude::*;
use bevy::ecs::component::HookContext;
use bevy::ecs::world::DeferredWorld;
#[derive(Component, Default, Reflect)]
#[reflect(Component)]
#[require(ValidNew)] // <-- Require validation
struct New;
#[derive(Component, Default, Reflect)]
#[reflect(Component)]
#[component(on_insert = Self::validate)] // <-- Validate on insert
struct ValidNew;
impl ValidNew {
fn validate(mut world: DeferredWorld, ctx: HookContext) {
// ...
}
}
save, save_default, save_all, load)SavePlugin and LoadPluginSaveWorld and LoadWorld events
save_on_default_event and load_on_default_event are equivalent to the old save_default and load pipelinessave_on and load_on for custom events/filtersSave component is added as a required component.
Save component to all saved entities.Save to at least one of the saved components on a saved entity ensures it is inserted automatically on load.PostSave and PostLoad should be refactored into observers
Trigger<OnSave> and Trigger<OnLoad> to access the Saved and Loaded data, or handle any errorsPlease post an issue for any bugs, questions, or suggestions.
You may also contact me on the official Bevy Discord server as @Zeenobit.