Persistent Storage with bevy_persistent
This guide provides an overview of using bevy_persistent
for data persistence in Rummage.
Introduction
bevy_persistent
is a Bevy crate that makes it easy to save and load data across application sessions. It enables robust persistence for:
- User settings
- Game saves
- Deck collections
- Game state snapshots
- Player profiles
- Achievement data
Getting Started
Adding the Dependency
First, add bevy_persistent
to your Cargo.toml
:
[dependencies]
bevy_persistent = "0.4.0" # Use the latest compatible version
Basic Usage
The basic pattern for using bevy_persistent
is:
use bevy::prelude::*; use bevy_persistent::prelude::*; use serde::{Deserialize, Serialize}; // Define a persistent resource #[derive(Resource, Serialize, Deserialize, Default)] struct GameSettings { volume: f32, fullscreen: bool, resolution: (u32, u32), } fn main() { App::new() .add_plugins(DefaultPlugins) // Initialize persistent resource .insert_resource( Persistent::<GameSettings>::builder() .name("settings") .format(StorageFormat::Ron) .path("user://settings.ron") .default(GameSettings { volume: 0.5, fullscreen: false, resolution: (1920, 1080), }) .build() ) .add_systems(Startup, load_settings) .add_systems(Update, save_settings_on_change) .run(); } // Load settings at startup fn load_settings(mut settings: ResMut<Persistent<GameSettings>>) { if let Err(err) = settings.load() { error!("Failed to load settings: {}", err); } else { info!("Settings loaded successfully"); } } // Save settings when they change fn save_settings_on_change(settings: Res<Persistent<GameSettings>>) { if settings.is_changed() { if let Err(err) = settings.save() { error!("Failed to save settings: {}", err); } else { info!("Settings saved successfully"); } } }
Key Features
Builder Pattern
The library uses a builder pattern for constructing persistent resources:
#![allow(unused)] fn main() { Persistent::<T>::builder() .name("resource_name") // Human-readable name .format(StorageFormat::Ron) // Serialization format .path("user://file.ron") // Storage path .default(T::default()) // Default value .build() }
Storage Formats
bevy_persistent
supports multiple storage formats:
- Ron: Human-readable Rusty Object Notation (good for configs)
- Json: Standard JSON format (good for interoperability)
- Bincode: Efficient binary format (good for large data)
- Toml: Config-friendly format (good for settings)
- Yaml: Human-readable structured format
Storage Paths
Paths can use special prefixes:
user://
: User-specific data directoryconfig://
: Configuration directorycache://
: Cache directoryassets://
: Assets directory
For example:
#![allow(unused)] fn main() { .path("user://saves/profile1.save") }
Hot Reloading
During development, you can enable hot reloading of persistent resources:
#![allow(unused)] fn main() { fn hot_reload_system(mut settings: ResMut<Persistent<GameSettings>>) { if settings.was_modified_on_disk() { if let Err(err) = settings.load() { error!("Failed to hot reload settings: {}", err); } else { info!("Hot reloaded settings from disk"); } } } }
Error Handling
The library provides comprehensive error handling:
#![allow(unused)] fn main() { match settings.load() { Ok(()) => info!("Loaded successfully"), Err(PersistentError::Io(err)) => error!("I/O error: {}", err), Err(PersistentError::Deserialize(err)) => error!("Deserialization error: {}", err), Err(err) => error!("Other error: {}", err), } }
Best Practices
Atomicity
To ensure atomic updates (all-or-nothing):
#![allow(unused)] fn main() { // Make multiple changes in a transaction-like manner fn update_settings(mut settings: ResMut<Persistent<GameSettings>>) { // Make changes settings.volume = 0.8; settings.fullscreen = true; settings.resolution = (3840, 2160); // Save all changes at once if let Err(err) = settings.save() { error!("Failed to save settings: {}", err); // Optionally revert changes on error } } }
Versioning
For schema changes, use serde's versioning support:
#![allow(unused)] fn main() { #[derive(Resource, Serialize, Deserialize)] struct GameSettings { // Add version field for schema migration #[serde(default = "default_version")] version: u32, // Original fields volume: f32, fullscreen: bool, // New fields with defaults #[serde(default)] resolution: (u32, u32), } fn default_version() -> u32 { 1 } }
Resource Granularity
Choose the right granularity for persistent resources:
- Too coarse: One resource for all settings makes recovery harder
- Too fine: Too many small resources increases I/O overhead
Good balance:
#![allow(unused)] fn main() { // Audio settings in one resource #[derive(Resource, Serialize, Deserialize, Default)] struct AudioSettings { /* ... */ } // Video settings in another #[derive(Resource, Serialize, Deserialize, Default)] struct VideoSettings { /* ... */ } // Controls in another #[derive(Resource, Serialize, Deserialize, Default)] struct ControlSettings { /* ... */ } }
Change Detection
For efficient saving, only save when something has actually changed:
#![allow(unused)] fn main() { fn save_if_changed( settings: Res<Persistent<GameSettings>>, time: Res<Time>, mut last_save: Local<f64>, ) { // Only check if resource changed if settings.is_changed() { let now = time.elapsed_seconds_f64(); // Don't save too frequently (debounce) if now - *last_save > 5.0 { if let Err(err) = settings.save() { error!("Failed to save: {}", err); } else { *last_save = now; } } } } }
Use Cases in Rummage
Deck Database
For the deck database, use bevy_persistent
to store user decks:
#![allow(unused)] fn main() { // See: docs/card_systems/deck_database/persistent_storage.md }
Game State Snapshots
For game state snapshots and rollback functionality:
#![allow(unused)] fn main() { // See: docs/core_systems/snapshot/bevy_persistent_integration.md }
User Settings
For user preferences and settings:
#![allow(unused)] fn main() { #[derive(Resource, Serialize, Deserialize, Default)] struct UserPreferences { username: String, card_style: CardStyle, audio_volume: f32, enable_animations: bool, enable_auto_tap: bool, enable_hints: bool, } // Initialize in plugin fn build_user_settings(app: &mut App) { let preferences = Persistent::<UserPreferences>::builder() .name("user_preferences") .format(StorageFormat::Ron) .path("user://preferences.ron") .default(UserPreferences::default()) .build(); app.insert_resource(preferences) .add_systems(Startup, load_user_preferences) .add_systems(Update, save_preferences_on_change); } }
Troubleshooting
Common Issues
- File Not Found: Check if the directory exists and has write permissions
- Serialization Errors: Make sure all fields are serializable
- Path Resolution: Use the correct path prefix for different platforms
Debugging Tips
Enable logging to debug storage issues:
#![allow(unused)] fn main() { // Enable debug logs for bevy_persistent fn setup_logging() { env_logger::Builder::from_default_env() .filter_module("bevy_persistent", log::LevelFilter::Debug) .init(); } }