bevy_ggrs

repository·main·Indexed 18 days ago

https://github.com/gschup/bevy_ggrs

A Bevy plugin for the GGRS P2P rollback networking library. It enables low-latency, deterministic rollback networking by managing state snapshots and a dedicated execution schedule (GgrsSchedule). The library provides various snapshot strategies (Copy, Clone, and Reflect) and handles entity reconciliation to maintain stable identities across rollbacks.

Tokens
14.6K
Snippets
41
Records
64
Agent score
62%

What's inside bevy_ggrs

  1. How entity identity and reconciliation work

    main

    Because Bevy Entity IDs are not stable across despawn/respawn cycles, bevy_ggrs uses specific components to maintain logical identity during rollbacks:

    • Rollback: A marker component. Add this to any entity that must be saved and rolled back. Adding this automatically assigns a RollbackId.
    • RollbackId: An immutable component representing the Entity ID at the time Rollback was first added. This is the stable key used in GgrsComponentSnapshot.
    • RollbackOrdered: A resource maintaining a stable, insertion-ordered list of all RollbackIds, used for deterministic checksums.

    Reconciliation Process: During LoadWorld, the EntitySnapshotPlugin compares the live entity set against the snapshot:

    • Exists in both: Maps current → snapshot IDs.
    • In snapshot only: Spawns a new entity with the same Rollback + RollbackId and maps new_id → old_id.
    • In world only: Despawns the entity.

    Mappings are stored in RollbackEntityMap and used by LoadWorldSystems::Mapping to fix up stale Entity references in components or resources.

  2. Determinism Warning for BoxGame

    main

    The BoxGame example uses f32 arithmetic (e.g., velocity integration, clamp_length_max). While deterministic on the same platform, this may produce different results across different CPU architectures or operating systems, leading to desyncs.

    Recommendation: If implementing a deterministic game, use fixed-point math or limit cross-platform play to the same architecture.

  3. How the GGRS rollback loop works

    main

    The GgrsPlugin manages the rollback loop by running the run_ggrs_schedules system (typically in PreUpdate). This system accumulates real-world delta time and polls the Session for remote input. When enough time has accumulated, it processes GgrsRequest objects by triggering specific Bevy schedules:

    GGRS Requestbevy_ggrs ScheduleAction
    SaveGameStateSaveWorldSnapshot the current world state
    LoadGameStateLoadWorldRestore the world to a previously saved frame
    AdvanceFrameAdvanceWorldSimulate one frame of game logic

    Normal Frame: One SaveGameState followed by one AdvanceFrame. Rollback Frame: One LoadGameState to rewind, followed by a sequence of AdvanceFrame + SaveGameState pairs to re-simulate up to the current frame.

  4. How bevy_ggrs handles rollbacks

    main

    Bevy GGRS is a plugin for the GGRS P2P rollback networking library. It manages game state advancement and rollbacks using a dedicated GgrsSchedule.

    To ensure rollbacks work correctly, you must:

    • Snapshotting: Explicitly register components and resources for snapshotting using .rollback_component_with_clone::<T>(). Only registered components will be reverted during a rollback.
    • Determinism: All game logic systems added to the GgrsSchedule must be deterministic. If the same inputs are applied to the same state, the resulting state must be identical across all clients.
    • Rollback Component: You can use the #[require(Rollback)] attribute on your player components to ensure the Rollback component is automatically added whenever the player is spawned.
  5. Be aware of Change Detection during rollback

    main
    Every snapshot restore triggers change detection on all restored components. Systems reacting to Changed<T> will fire after every rollback, which can lead to performance issues or unintended behavior (e.g., in transform propagation systems).
  6. Understand the checksum pipeline and desync detection

    main

    bevy_ggrs uses a checksum pipeline to detect desyncs between peers:

    1. Contribution: Every registered type contributes a ChecksumPart (a u128 component with ChecksumFlag<T>).
    2. Computation: During SaveWorldSystems::Checksum, each plugin computes its hash and upserts a ChecksumPart entity.
    3. Aggregation: ChecksumPlugin::update XORs all parts together into the Checksum resource.
    4. Transmission: run_ggrs_schedules reads the Checksum after SaveWorld and sends it to GGRS.

    If checksums diverge between peers, GGRS triggers GgrsEvent::DesyncDetected (for P2P) or SyncTestMismatch (for SyncTest).

  7. Avoid using Local<T> in rollback systems

    main
    Local<T> is per-system state that is not snapshotted. Using it inside GgrsSchedule will cause the local value to drift between the original simulation and resimulation. Use a Component or Resource registered for rollback instead.
  8. Understand the schedule order within a frame

    main

    The execution order of schedules within a single Bevy frame is critical for correct rollback behavior:

    1. PreUpdate: Runs run_ggrs_schedules.
    2. SaveWorld: Snapshots the current state.
      • SaveWorldSystems::Checksum: Computes ChecksumParts.
      • SaveWorldSystems::Snapshot: Writes to snapshot storage.
    3. LoadWorld: Restores the world to a rollback frame (if needed).
      • LoadWorldSystems::Entity: Reconciles entity sets and builds RollbackEntityMap.
      • LoadWorldSystems::EntityFlush.
      • LoadWorldSystems::Data: Restores component/resource values.
      • LoadWorldSystems::DataFlush.
      • LoadWorldSystems::Mapping: Remaps stale Entity references via MapEntities.
    4. AdvanceWorld: Runs game logic.
      • AdvanceWorldSystems::First: Pre-frame setup (e.g., GgrsTime update).
      • [ApplyDeferred]
      • AdvanceWorldSystems::Main: Runs your custom GgrsSchedule (game logic).
      • [ApplyDeferred]
      • AdvanceWorldSystems::Last: Post-frame cleanup (e.g., restoring Time<()>)
  9. Avoid using Bevy Events inside GgrsSchedule

    main

    Bevy's Events<T> resource is not snapshotted. Events fired during a frame that gets rolled back will not be re-fired during resimulation, and events from resimulated frames will not be visible to systems outside GgrsSchedule.

    Best Practices:

    • Do not use EventWriter or EventReader inside GgrsSchedule.
    • Use a component or resource to communicate state changes between rollback systems.
    • Only fire Bevy events from systems outside GgrsSchedule (e.g., in Update) based on snapshotted state.
  10. Quickstart: Set up a Bevy GGRS application

    main

    To implement rollback networking with bevy_ggrs, follow these core steps:

    1. Define your configuration: Create a type alias for GgrsConfig, specifying your input type (e.g., u8).
    2. Initialize the Plugin: Add GgrsPlugin::<GgrsConfig>::default() to your Bevy App.
    3. Set Frame Rate: Insert the RollbackFrameRate resource to define how many frames per second the rollback logic should run at.
    4. Register Components: Use .rollback_component_with_clone::<T>() to register components that need to be snapshotted for rollbacks. These components must implement Clone.
    5. Handle Inputs: Add systems to the ReadInputs schedule to provide local player inputs each frame.
    6. Implement Game Logic: Place your deterministic game logic systems into the GgrsSchedule instead of the standard Update schedule.
    7. Initialize Session: Insert a Session resource (e.g., Session::SyncTest) to start the networking session.
    use bevy::prelude::*;
    use bevy_ggrs::prelude::*;
    
    type GgrsConfig = bevy_ggrs::GgrsConfig<u8>; // replace u8 with your input type
    
    App::new()
        .add_plugins(GgrsPlugin::<GgrsConfig>::default())
        .insert_resource(RollbackFrameRate(60))
        // register components/resources for snapshotting
        .rollback_component_with_clone::<Transform>()
        // provide inputs each frame
        .add_systems(ReadInputs, read_local_inputs)
        // your game logic — must be deterministic!
        .add_systems(GgrsSchedule, move_players)
        .insert_resource(Session::SyncTest(session))
        .run();
    
    // #[require(Rollback)] ensures Rollback is always added with Player —
    // no need to include it manually in every spawn call.
    #[derive(Component)]
    #[require(Rollback)]
    struct Player;
    
    fn spawn_player(mut commands: Commands) {
        commands.spawn((Transform::default(), Player));
    }
  11. Register all components and resources for rollback

    main

    Only components and resources explicitly registered via rollback_component_with_* or rollback_resource_with_* are snapshotted. Unregistered state will not be restored on rollback, causing silent desyncs.

    If an entity is despawned and re-created during rollback, all of its components must be registered to ensure the resimulated entity is complete. You can use SyncTestSession with checksum_component_with_hash to detect these issues.