Snapshot System Implementation
This document covers the technical implementation details of the Rummage snapshot system.
Plugin Structure
The snapshot system is implemented as a Bevy plugin that can be added to the application:
#![allow(unused)] fn main() { pub struct SnapshotPlugin; impl Plugin for SnapshotPlugin { fn build(&self, app: &mut App) { app // Register resources .init_resource::<PendingSnapshots>() .init_resource::<SnapshotConfig>() // Register snapshot events .add_event::<SnapshotEvent>() .add_event::<SnapshotProcessedEvent>() // Add snapshot systems to the appropriate schedule .add_systems(Update, ( handle_snapshot_events, process_pending_snapshots, trigger_snapshot_on_turn_change, trigger_snapshot_on_phase_change, ).chain()); } } }
Core Components
Configuration
The snapshot system is configured through a resource:
#![allow(unused)] fn main() { #[derive(Resource)] pub struct SnapshotConfig { /// Whether to automatically create snapshots on turn changes pub auto_snapshot_on_turn: bool, /// Whether to automatically create snapshots on phase changes pub auto_snapshot_on_phase: bool, /// Maximum number of snapshots to process per frame pub max_snapshots_per_frame: usize, /// Maximum number of snapshots to keep in history pub max_history_size: usize, /// Whether to compress snapshots pub use_compression: bool, } impl Default for SnapshotConfig { fn default() -> Self { Self { auto_snapshot_on_turn: true, auto_snapshot_on_phase: false, max_snapshots_per_frame: 1, max_history_size: 100, use_compression: true, } } } }
Event Types
The system communicates through events:
#![allow(unused)] fn main() { /// Events for snapshot operations #[derive(Event)] pub enum SnapshotEvent { /// Take a new snapshot of the current state Take, /// Apply a specific snapshot Apply(Uuid), /// Save the current snapshot to disk Save(String), /// Load a snapshot from disk Load(String), } /// Event fired when a snapshot has been processed #[derive(Event)] pub struct SnapshotProcessedEvent { /// The ID of the processed snapshot pub id: Uuid, /// Whether processing succeeded pub success: bool, } }
Creating Snapshots
The core snapshot creation logic:
#![allow(unused)] fn main() { fn create_game_snapshot( world: &World, game_state: &GameState, ) -> GameSnapshot { // Create a new empty snapshot let mut snapshot = GameSnapshot { id: Uuid::new_v4(), turn: game_state.turn, phase: game_state.phase.clone(), active_player: game_state.active_player, game_data: HashMap::new(), timestamp: world.resource::<Time>().elapsed_seconds(), }; // Find all snapshotable entities let mut snapshotable_query = world.query_filtered::<Entity, With<Snapshotable>>(); // For each snapshotable entity, serialize its components for entity in snapshotable_query.iter(world) { serialize_entity_to_snapshot(world, entity, &mut snapshot); } snapshot } fn serialize_entity_to_snapshot( world: &World, entity: Entity, snapshot: &mut GameSnapshot, ) { // Get all component types registered for this entity let entity_components = world.entity(entity).archetype().components(); // Create a buffer to store the entity's serialized components let mut entity_data = Vec::new(); // Write the entity ID let entity_id = entity.to_bits(); entity_data.extend_from_slice(&entity_id.to_le_bytes()); // For each component type for component_id in entity_components.iter() { // Skip certain component types that don't need to be serialized if should_skip_component(component_id) { continue; } // Get the component storage if let Some(component_info) = world.components().get_info(component_id) { // Get the component data for this entity if let Some(component_data) = component_info.get_component(world, entity) { // Write the component ID entity_data.extend_from_slice(&component_id.to_le_bytes()); // Write the component size let size = component_data.len(); entity_data.extend_from_slice(&size.to_le_bytes()); // Write the component data entity_data.extend_from_slice(component_data); } } } // Add the entity's data to the snapshot snapshot.game_data.insert(entity.index().to_string(), entity_data); } }
Automatic Snapshot Triggering
Snapshots can be automatically triggered by game events:
#![allow(unused)] fn main() { fn trigger_snapshot_on_turn_change( mut turn_events: EventReader<TurnChangeEvent>, mut snapshot_events: EventWriter<SnapshotEvent>, config: Res<SnapshotConfig>, ) { if !config.auto_snapshot_on_turn { return; } for _ in turn_events.iter() { // Create a new snapshot event snapshot_events.send(SnapshotEvent::Take); } } fn trigger_snapshot_on_phase_change( mut phase_events: EventReader<PhaseChangeEvent>, mut snapshot_events: EventWriter<SnapshotEvent>, config: Res<SnapshotConfig>, ) { if !config.auto_snapshot_on_phase { return; } for _ in phase_events.iter() { // Create a new snapshot event snapshot_events.send(SnapshotEvent::Take); } } }
Processing Snapshots
The process_pending_snapshots
system handles snapshots in the pending queue:
#![allow(unused)] fn main() { /// Process any pending snapshots in the queue pub fn process_pending_snapshots( mut commands: Commands, mut pending: ResMut<PendingSnapshots>, mut processed_events: EventWriter<SnapshotProcessedEvent>, config: Res<SnapshotConfig>, ) { // Skip if processing is paused if pending.paused { return; } // Process up to config.max_snapshots_per_frame let to_process = pending.queue.len().min(config.max_snapshots_per_frame); for _ in 0..to_process { if let Some(snapshot) = pending.queue.pop_front() { // Apply the snapshot to the game state apply_snapshot(&mut commands, &snapshot); // Notify that a snapshot was processed processed_events.send(SnapshotProcessedEvent { id: snapshot.id, success: true, }); } } } }
Applying Snapshots
To restore a game state from a snapshot:
#![allow(unused)] fn main() { fn apply_snapshot( commands: &mut Commands, snapshot: &GameSnapshot, ) { // Clear existing entities that should be replaced by the snapshot clear_snapshotable_entities(commands); // Restore global game state restore_game_state(commands, snapshot); // For each entity in the snapshot for (entity_key, entity_data) in &snapshot.game_data { // Create a new entity let entity = commands.spawn_empty().id(); // Deserialize and add components deserialize_entity_components(commands, entity, entity_data); } } fn deserialize_entity_components( commands: &mut Commands, entity: Entity, entity_data: &[u8], ) { let mut offset = 8; // Skip the entity ID // While there's more data to read while offset < entity_data.len() { // Read the component ID let component_id_bytes = &entity_data[offset..offset+8]; let component_id = u64::from_le_bytes(component_id_bytes.try_into().unwrap()); offset += 8; // Read the component size let size_bytes = &entity_data[offset..offset+8]; let size = usize::from_le_bytes(size_bytes.try_into().unwrap()); offset += 8; // Read the component data let component_data = &entity_data[offset..offset+size]; offset += size; // Deserialize and add the component add_component_from_bytes(commands, entity, component_id, component_data); } } }
Snapshot Storage
The system supports saving snapshots to disk and loading them later:
#![allow(unused)] fn main() { fn save_snapshot_to_disk( snapshot: &GameSnapshot, path: &str, ) -> Result<(), std::io::Error> { // Serialize the snapshot let serialized = bincode::serialize(snapshot) .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; // Compress if configured let final_data = if snapshot.use_compression { // Apply compression let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); encoder.write_all(&serialized)?; encoder.finish()? } else { serialized }; // Write to file std::fs::write(path, final_data)?; Ok(()) } fn load_snapshot_from_disk( path: &str, ) -> Result<GameSnapshot, std::io::Error> { // Read file let data = std::fs::read(path)?; // Check for compression let serialized = if is_compressed(&data) { // Decompress let mut decoder = flate2::read::GzDecoder::new(&data[..]); let mut decompressed = Vec::new(); decoder.read_to_end(&mut decompressed)?; decompressed } else { data }; // Deserialize let snapshot = bincode::deserialize(&serialized) .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; Ok(snapshot) } }
Performance Considerations
The snapshot system is designed with performance in mind:
- Selective Serialization: Only necessary components are included
- Batched Processing: Limits how many snapshots are processed per frame
- Compression Options: Configurable to balance size vs. speed
- Marker Components: Only entities explicitly marked are included
- Queue Management: Background processing to minimize frame time impact
Integration Points
The snapshot system integrates with other systems through:
- Events: For triggering and receiving snapshot operations
- Component Markers: To specify what entities should be included
- Configuration: To control behavior based on game requirements
- Plugins: To integrate with other game systems
These integration points provide flexibility while maintaining clean separation of concerns.
Next Steps
- Integration with Networking: How snapshots work with the networking system
- Testing: How to test snapshot functionality
- API Reference: Complete reference documentation