Snapshot Integration with Networking
This document explains how the snapshot system integrates with Rummage's networking capabilities to enable multiplayer gameplay.
Overview
The snapshot system forms a critical part of Rummage's networking architecture, providing:
- State Synchronization: Ensuring all clients have the same game state
- Rollback Capability: Allowing recovery from network disruptions
- Deterministic Execution: Working with deterministic systems for consistent gameplay
- Hidden Information Management: Handling information that should be hidden from certain players
Integration Architecture
Core Components
The networking integration uses these key components:
#![allow(unused)] fn main() { /// Plugin that integrates the snapshot system with networking pub struct NetworkSnapshotPlugin; /// Resource that tracks network-specific snapshot configuration #[derive(Resource)] pub struct NetworkSnapshotConfig { /// Frequency of network snapshot updates (in seconds) pub sync_frequency: f32, /// Maximum size of a snapshot packet (in bytes) pub max_packet_size: usize, /// Whether to compress network snapshots pub compress_network_snapshots: bool, /// Number of snapshots to keep for rollback purposes pub rollback_history_size: usize, } /// Component marking entities with network-specific snapshot requirements #[derive(Component)] pub struct NetworkSnapshotable { /// Which player IDs can see this entity pub visible_to: Vec<u64>, /// Priority for synchronization (higher values sync first) pub sync_priority: u8, } }
Plugin Implementation
The integration plugin adds networking-specific systems:
#![allow(unused)] fn main() { impl Plugin for NetworkSnapshotPlugin { fn build(&self, app: &mut App) { app // Register resources .init_resource::<NetworkSnapshotConfig>() .init_resource::<PendingNetworkSnapshots>() // Register custom events .add_event::<NetworkSnapshotEvent>() // Add networking-specific systems .add_systems(Update, ( sync_game_state_with_clients, handle_network_snapshot_events, process_incoming_snapshots, ).chain()) // Add systems to the replicon client and server sets .add_systems(RepliconClientSet::Receive, receive_network_snapshot) .add_systems(RepliconServerSet::Send, send_network_snapshots); } } }
State Synchronization
The core of the networking integration is the state synchronization system:
#![allow(unused)] fn main() { fn sync_game_state_with_clients( mut commands: Commands, time: Res<Time>, mut last_sync: Local<f32>, config: Res<NetworkSnapshotConfig>, game_state: Res<GameState>, client_info: Res<ClientRegistry>, world: &World, mut network_events: EventWriter<NetworkSnapshotEvent>, ) { // Check if it's time for a sync if time.elapsed_seconds() - *last_sync < config.sync_frequency { return; } *last_sync = time.elapsed_seconds(); // Create a base snapshot of the current state let base_snapshot = create_game_snapshot(world, &game_state); // For each connected client, create a tailored snapshot for client_id in client_info.connected_clients() { let client_snapshot = create_client_specific_snapshot( &base_snapshot, client_id, &client_info ); // Send the snapshot to the client network_events.send(NetworkSnapshotEvent::SendTo( client_id, client_snapshot )); } } }
Client-Specific Snapshots
The system creates tailored snapshots for each client to manage hidden information:
#![allow(unused)] fn main() { fn create_client_specific_snapshot( base_snapshot: &GameSnapshot, client_id: u64, client_info: &ClientRegistry, ) -> GameSnapshot { // Clone the base snapshot let mut client_snapshot = base_snapshot.clone(); // Filter out data the client shouldn't see client_snapshot.game_data.retain(|entity_key, _| { // Check if this entity is visible to the client let entity = Entity::from_bits( u64::from_str(entity_key).unwrap_or_default() ); is_entity_visible_to_client(entity, client_id, client_info) }); // Modify certain data to hide information for (_, entity_data) in client_snapshot.game_data.iter_mut() { sanitize_hidden_information(entity_data, client_id, client_info); } client_snapshot } fn is_entity_visible_to_client( entity: Entity, client_id: u64, client_info: &ClientRegistry, ) -> bool { // Logic to determine if an entity should be visible to a client // ... } fn sanitize_hidden_information( entity_data: &mut Vec<u8>, client_id: u64, client_info: &ClientRegistry, ) { // Logic to modify entity data to hide sensitive information // ... } }
Network Data Transfer
The system handles sending and receiving snapshots over the network:
#![allow(unused)] fn main() { fn send_network_snapshots( mut server: ResMut<RenetServer>, mut pending: ResMut<PendingNetworkSnapshots>, config: Res<NetworkSnapshotConfig>, ) { // Process all pending network snapshots for (client_id, snapshot) in pending.outgoing.drain(..) { // Serialize the snapshot let serialized = bincode::serialize(&snapshot) .unwrap_or_default(); // Compress if configured let final_data = if config.compress_network_snapshots { // Compression logic... Vec::new() } else { serialized }; // Send to client server.send_message( client_id, NetworkChannel::StateSync as u8, final_data ); } } fn receive_network_snapshot( mut client: ResMut<RenetClient>, mut snapshot_events: EventWriter<SnapshotEvent>, ) { // Check for incoming snapshot messages while let Some(message) = client.receive_message(NetworkChannel::StateSync as u8) { // Decompress if needed let serialized = if is_compressed(&message) { // Decompression logic... Vec::new() } else { message }; // Deserialize the snapshot if let Ok(snapshot) = bincode::deserialize::<GameSnapshot>(&serialized) { // Apply the received snapshot snapshot_events.send(SnapshotEvent::Apply(snapshot.id)); } } } }
Rollback System
The integration includes a rollback system for handling network disruptions:
/// Plugin that provides rollback functionality for network gameplay pub struct RollbackPlugin; impl Plugin for RollbackPlugin { fn build(&self, app: &mut App) { app .init_resource::<RollbackHistory>() .add_event::<RollbackEvent>() .add_systems(Update, ( maintain_rollback_history, handle_rollback_events, ).chain()); } } /// Resource that maintains a history of snapshots for rollback #[derive(Resource, Default)] pub struct RollbackHistory { /// Historical snapshots indexed by turn and phase pub history: HashMap<(u32, Phase), GameSnapshot>, } /// Event requesting a rollback to a previous state #[derive(Event)] pub struct RollbackEvent { /// The turn to roll back to pub turn: u32, /// The phase within the turn pub phase: Option<Phase>, } fn maintain_rollback_history( mut history: ResMut<RollbackHistory>, mut snapshot_events: EventReader<SnapshotProcessedEvent>, snapshots: Res<SnapshotRegistry>, config: Res<NetworkSnapshotConfig>, ) { // For each new snapshot, add it to the history for event in snapshot_events.iter() { if let Some(snapshot) = snapshots.get(event.id) { history.history.insert( (snapshot.turn, snapshot.phase.clone()), snapshot.clone() ); } } // Prune history to maintain size limits if history.history.len() > config.rollback_history_size { // Pruning logic... } } fn handle_rollback_events( mut rollback_events: EventReader<RollbackEvent>, history: Res<RollbackHistory>, mut snapshot_events: EventWriter<SnapshotEvent>, ) { for event in rollback_events.iter() { // Find the snapshot to roll back to let target_snapshot = if let Some(phase) = &event.phase { // Find specific phase history.history.get(&(event.turn, phase.clone())) } else { // Find any snapshot from this turn history.history.iter() .find(|((turn, _), _)| *turn == event.turn) .map(|(_, snapshot)| snapshot) }; // If found, apply the snapshot if let Some(snapshot) = target_snapshot { snapshot_events.send(SnapshotEvent::Apply(snapshot.id)); } } }
Deterministic Random Number Generator
The integration includes special handling for random number generation to ensure deterministic gameplay:
#![allow(unused)] fn main() { /// Plugin that integrates deterministic RNG with snapshots for networking pub struct DeterministicRNGPlugin; impl Plugin for DeterministicRNGPlugin { fn build(&self, app: &mut App) { app .init_resource::<NetworkedRngState>() .add_systems(Update, ( capture_rng_in_snapshot, restore_rng_from_snapshot, ).chain()); } } /// Resource that tracks the state of the deterministic RNG #[derive(Resource, Serialize, Deserialize)] pub struct NetworkedRngState { /// The current seed pub seed: u64, /// The number of times the RNG has been used pub usage_count: u64, } fn capture_rng_in_snapshot( rng_state: Res<NetworkedRngState>, mut create_snapshot: EventReader<SnapshotEvent>, mut snapshots: ResMut<SnapshotRegistry>, ) { for event in create_snapshot.iter() { if let SnapshotEvent::Take = event { // Find the most recent snapshot if let Some(snapshot) = snapshots.most_recent() { // Add RNG state to the snapshot let rng_data = bincode::serialize(&*rng_state).unwrap_or_default(); snapshot.game_data.insert("rng_state".to_string(), rng_data); } } } } fn restore_rng_from_snapshot( mut rng_state: ResMut<NetworkedRngState>, mut apply_snapshot: EventReader<SnapshotEvent>, snapshots: Res<SnapshotRegistry>, ) { for event in apply_snapshot.iter() { if let SnapshotEvent::Apply(id) = event { // Find the snapshot if let Some(snapshot) = snapshots.get(*id) { // Restore RNG state from snapshot if let Some(rng_data) = snapshot.game_data.get("rng_state") { if let Ok(state) = bincode::deserialize::<NetworkedRngState>(rng_data) { *rng_state = state; } } } } } } }
Testing Network Integration
Testing the networking integration with snapshots:
#![allow(unused)] fn main() { #[test] fn test_network_snapshot_synchronization() { // Set up a test server and client let mut app = App::new(); app.add_plugins(( MinimalPlugins, RepliconServerPlugin, SnapshotPlugin, NetworkSnapshotPlugin, )); // Create a test client let client_id = 1; let mut client_registry = ClientRegistry::default(); client_registry.register_client(client_id); app.insert_resource(client_registry); // Set up game state setup_test_game_state(&mut app); // Trigger a snapshot app.world.send_event(SnapshotEvent::Take); app.update(); // Verify that a network snapshot was created let network_events = app.world.resource::<Events<NetworkSnapshotEvent>>(); let mut reader = network_events.get_reader(); let has_snapshot_event = reader.iter(&network_events).any(|event| { matches!(event, NetworkSnapshotEvent::SendTo(id, _) if *id == client_id) }); assert!(has_snapshot_event, "Should create a network snapshot for the client"); } }
Best Practices
When working with the snapshot and networking integration:
- Minimize Snapshot Size: Use the
NetworkSnapshotable
component to control what gets synchronized - Handle Frequent Updates: Be mindful of performance impact for frequently changing components
- Test Network Conditions: Use simulated network conditions to test behavior under varying latency
- Secure Hidden Information: Carefully audit what information is sent to each client
- Handle Reconnections: Ensure clients that reconnect receive a complete state update
- Monitor Bandwidth: Keep track of snapshot sizes and network usage
- Implement Fallbacks: Have strategies for when snapshot synchronization fails
Next Steps
- Testing: How to test snapshot functionality, including network integration
- API Reference: Complete reference documentation for the snapshot system