Backdash Rollback Netcode

repository·master·Indexed 41 days ago

https://github.com/delta3-studio/backdash

A high-performance, asynchronous implementation of Rollback Netcode for .NET games. Inspired by GGPO, it provides a zero-latency feel through input prediction and speculative execution. The library requires game engines to support full determinism, state serializability, and headless simulation. It includes tools for P2P connectivity via UDP hole punching and a developer guide for implementing the INetcodeSessionHandler interface.

Tokens
7.6K
Snippets
18
Records
30
Agent score
52%

What's inside Backdash

  1. Overview of Backdash Rollback Netcode

    master

    Backdash is a highly configurable and extensible implementation of Rollback Netcode with full asynchronous IO. It is heavily inspired by GGPO.

    Unlike traditional networking that adds input delay to account for transmission time, Backdash uses input prediction and speculative execution. This provides the illusion of zero-latency, allowing players to use the same timings and muscle memory online as they do offline.

  2. What is Backdash and how does it work?

    master

    Backdash is a multiplayer network SDK designed to implement Rollback Netcode in games using C#, F#, or any .NET dialect (such as Godot, MonoGame, or Stride3D).

    Core Concept: Rollback Netcode

    Unlike traditional delay-based networking that waits for all player inputs to arrive before advancing a frame, rollback networking allows the local game to proceed immediately using local inputs. If remote inputs are missing, the SDK predicts them based on previous behavior. When the actual remote inputs arrive, if they differ from the prediction, Backdash triggers a re-simulation from the point of divergence to the current frame to ensure all players remain synchronized.

    Integration Requirements

    To use Backdash, your game engine must be fully deterministic. You are responsible for implementing three core functionalities that Backdash will orchestrate:

    1. Save State: The ability to capture and store the current game state.
    2. Load State: The ability to restore the game to a previously saved state.
    3. Execute Frame: The ability to run a single frame of game logic without rendering the output (used during re-simulations).
  3. Core Concepts: Game State and Game Inputs

    master

    Backdash relies on two fundamental abstractions to manage synchronization:

    • Game State: A complete description of everything in your game at a specific moment (e.g., positions of entities, health values, scores, bullet locations). This must be encapsulated and serializable.
    • Game Inputs: The set of variables that modify the game state (e.g., button presses, joystick movements, or even environmental variables like the current time of day used for simulation calculations).

    Note: Elements like audio renderers, video renderers, or visual-only special effects engines should be excluded from the Game State as they do not affect the outcome of the game simulation.

  4. Tune frame delay and speculative execution

    master

    Backdash uses a combination of frame delay and speculative execution to manage network latency.

    • Frame Delay: The number of frames you choose to delay input. Increasing this can reduce the frequency and visual impact of rollbacks (glitching), but adds perceived input lag.
    • Speculative Execution: If the network latency exceeds your specified frame delay, Backdash uses speculative execution to predict and hide the remaining latency.

    Tuning Recommendations

    • Fighting Games: Require high precision. Keep frame delay low (e.g., 1 frame) to avoid affecting timing-sensitive mechanics.
    • Board/Puzzle Games: Can tolerate higher frame delay (e.g., 4-5 frames) to provide a smoother experience with fewer rollbacks.
    • Visual Glitches: If short-duration visual effects (e.g., a 2-frame screen flash) are being 'consumed' or hidden by rollbacks, increase the frame delay or redesign the renderer to delay the effect until after the potential rollback window.
  5. How the SpaceWar Lobby and NAT Traversal works

    master

    The SpaceWar sample demonstrates P2P connectivity over the internet using UDP hole punching.

    Core Workflow

    1. Middle Server: A central server (LobbyServer) acts as a signaling mechanism. All clients connect to this server, which is known to all participants.
    2. Signaling: The server captures the IP address and port of each client and shares this information with other peers in the lobby.
    3. Authentication: When a client joins the lobby, the server issues a Guid/UUID token used for basic authentication.
    4. UDP Hole Punching: Once logged in, clients send a UDP packet containing their token to the server. The server uses the packet header metadata to track the client's public IP and Port.
    5. Local Network Mitigation: Since UDP hole punching often fails for clients behind the same NAT, the server also tracks local IP and port information to detect if peers are on the same local network.

    Communication Protocols

    • HTTP: Used for lobby management and retrieving peer information via HTTP pooling.
    • UDP: Used for the actual hole punching and peer-to-peer signaling.
  6. Isolate Game State from Non-Game State

    master

    To ensure efficient save/load operations and compatibility with rollback, you must separate game state (data required to compute the next frame) from non-game state (renderers, audio, textures, look-up tables, etc.).

    A recommended pattern is to encapsulate all game state into a single class or record type. This makes implementing save and load callbacks trivial and prevents unnecessary data from being serialized.

  7. Ensure Deterministic Game State Advancement

    master

    For rollback to work, the next game state must be computed solely from the current state and inputs. Avoid these common pitfalls:

    • Random Number Generators: Use a deterministic RNG. The seed must be identical for all players at frame 0, and the RNG's internal state must be part of your saved game state. Backdash provides a deterministic RNG via session.Random.
    • External Time Sources: Avoid using system clock or real-world time in state calculations, as clocks are never perfectly in sync across machines. If time is needed, include the time as part of the frame input.
    • Dangling References: When implementing save and load, ensure you are copying values rather than object references. Rebase pointers correctly during the load process.
    • Static/Hidden State: Avoid static variables in C# or C. All state must be part of the serializable game state. If you must use a global static, you must manually sync it during the LoadState callback.
  8. Separate Updating Game State from Rendering

    master

    Backdash may call your advance frame callback multiple times during a rollback. To prevent visual or audio glitches, you must separate game state updates from rendering.

    Your game loop should be structured so that the game state is determined solely by inputs, and rendering is driven by the resulting state. This allows you to advance the game state forward using inputs without triggering side effects like rendering or sound playback.

       bool finished = false;
       GameState state;
       Inputs inputs;
    
       do {
          GetControllerInputs(ref inputs);
          finished = AdvanceGameState(inputs, state);
          if (!finished) {
             RenderCurrentFrame(gamestate);
          }
       } while (!finished);
  9. Requirements for Synchronization and Rollback

    master

    Backdash synchronizes players by sending inputs to each other and having each player step their local simulation forward. For this to work correctly, your game engine must satisfy these three criteria:

    1. Full Determinism: For any given game state and set of inputs, advancing the simulation by exactly one frame must result in identical game states across all players.
    2. Encapsulation and Serializability: The game state must be fully encapsulated and capable of being serialized.
    3. Headless Simulation Capability: The engine must be able to load, save, and execute a single simulation frame without rendering the result. This capability is essential for implementing rollbacks.
  10. Implement state Save and Load handlers

    master

    Backdash requires you to implement SaveState and LoadState in your INetcodeSessionHandler to manage rollbacks.

    Using BinaryBufferWriter/Reader (Default): Use the provided BinaryBufferWriter in SaveState to write state members, and BinaryBufferReader in LoadState to read them. ⚠️ IMPORTANT: You must read and write members in the EXACT SAME ORDER and ensure the same data sizes (e.g., for arrays/lists).

    Using Custom Serializers (e.g., MemoryPack): You can bypass manual member writing by accessing the underlying buffer. Use writer.Buffer in SaveState and reader.Buffer in LoadState to integrate with third-party serialization libraries.

    // Example using manual binary writing
    public void SaveState(in Frame frame, ref readonly BinaryBufferWriter writer)
    {
        writer.Write(in currentGameState.Value1);
        writer.Write(in currentGameState.Value2);
    }
    
    public void LoadState(in Frame frame, ref readonly BinaryBufferReader reader)
    {
        reader.Read(ref currentGameState.Value1);
        reader.Read(ref currentGameState.Value2);
    }
    
    // Example using MemoryPack
    public void SaveState(in Frame frame, ref readonly BinaryBufferWriter writer)
    {
        MemoryPackSerializer.Serialize(writer.Buffer, currentGameState);
    }
    
    public void LoadState(in Frame frame, ref readonly BinaryBufferReader reader)
    {
        MemoryPackSerializer.Deserialize(reader.Buffer, ref currentGameState!);
    }
  11. Encode game inputs using Enum Flags

    master

    To minimize network bandwidth, it is highly recommended to encode game inputs using a C# Enum decorated with the [Flags] attribute. This allows you to represent a large number of digital inputs (buttons) within a very low byte count (e.g., using a short for up to 16 inputs). Backdash automatically handles the serialization of enums and primitive types.

    [Flags]
    public enum PadButtonInputs : short
    {
        None = 0,
        Select = 1 << 0,
        Up = 1 << 1,
        Down = 1 << 2,
        Left = 1 << 3,
        Right = 1 << 4,
        X = 1 << 5,
        Y = 1 << 6,
        A = 1 << 7,
        B = 1 << 8,
        LeftBumper = 1 << 9,
        RightBumper = 1 << 10,
        LeftTrigger = 1 << 11,
        RightTrigger = 1 << 12,
        LeftStickButton = 1 << 13,
        RightStickButton = 1 << 14,
    }
  12. Implement a custom input serializer

    master

    While Backdash automatically serializes primitives and enums, you must implement IBinarySerializer<TInput> for complex input types that require Endianess conversion support. The most efficient way to do this is by deriving from BinarySerializer<T>.

    When implementing, you must override Serialize using a BinaryRawBufferWriter and Deserialize using a BinaryBufferReader to manually handle each field of your input structure.

    // 1. Define your complex input structure
    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    public record struct MyPadInputs
    {
        public PadButtons Buttons;
        public byte LeftTrigger;
        public byte RightTrigger;
        public Axis LeftAxis;
        public Axis RightAxis;
    }
    
    // 2. Implement the serializer
    public class MyPadInputsBinarySerializer : BinarySerializer<MyPadInputs>
    {
        protected override void Serialize(in BinaryRawBufferWriter binaryWriter, in MyPadInputs data)
        {
            binaryWriter.Write((short)data.Buttons);
            binaryWriter.Write(data.LeftTrigger);
            binaryWriter.Write(data.RightTrigger);
            binaryWriter.Write(data.LeftAxis.X);
            binaryWriter.Write(data.LeftAxis.Y);
            binaryWriter.Write(data.RightAxis.X);
            binaryWriter.Write(data.RightAxis.Y);
        }
    
        protected override void Deserialize(in BinaryBufferReader binaryReader, ref MyPadInputs result)
        {
            result.Buttons = (PadButtons)binaryReader.ReadShort();
            result.LeftTrigger = binaryReader.ReadByte();
            result.RightTrigger = binaryReader.ReadByte();
            result.LeftAxis.X = binaryReader.ReadSByte();
            result.LeftAxis.Y = binaryReader.ReadSByte();
            result.RightAxis.X = binaryReader.ReadSByte();
            result.RightAxis.Y = binaryReader.ReadSByte();
        }
    }
    
    // 3. Register the serializer with RollbackNetcode
    MyPadInputsBinarySerializer mySerializer = new();
    var session = RollbackNetcode.WithInputType(t => t.Custom(mySerializer));