Try Live
Add Docs
Rankings
Pricing
Enterprise
Docs
Install
Theme
Install
Docs
Pricing
Enterprise
More...
More...
Try Live
Rankings
Create API Key
Add Docs
Backdash
https://github.com/delta3-studio/backdash
Admin
Backdash is a highly configurable and extensible implementation of Rollback Netcode for .NET games,
...
Tokens:
17,190
Snippets:
110
Trust Score:
7
Update:
1 month ago
Context
Skills
Chat
Benchmark
81.1
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# Backdash Backdash is a highly configurable and extensible implementation of Rollback Netcode for .NET games. It provides multiplayer networking that uses input prediction and speculative execution to achieve zero-latency game feel, eliminating the sluggish responsiveness of traditional delay-based networking. Originally inspired by GGPO, Backdash is designed to work seamlessly with any .NET-based game engine including Godot, Monogame, and Stride3D. Rollback networking allows games to execute locally with predicted inputs while simultaneously receiving remote player inputs. When predictions are incorrect, the game state rolls back and re-simulates to the current frame. Backdash handles all rollback algorithms, low-level networking, and state synchronization automatically—developers only need to implement game state save/load functions and advance frame logic. This makes integrating professional-grade netcode into fighting games, competitive multiplayer games, and other latency-sensitive applications straightforward. ## RollbackNetcode.WithInputType - Create Session Builder The entry point for creating netcode sessions. This static method initializes a session builder configured with your game's input type, which must be an unmanaged value type (enum, struct, or primitive). ```csharp using Backdash; using Backdash.Core; using System.Net; // Define your game input as a flags enum for compact network transmission [Flags] public enum GameInput : short { None = 0, Up = 1 << 0, Down = 1 << 1, Left = 1 << 2, Right = 1 << 3, Punch = 1 << 4, Kick = 1 << 5, Block = 1 << 6, } // Create a session builder with enum input type (automatic serialization) var session = RollbackNetcode .WithInputType<GameInput>() .WithPort(9001) .WithPlayerCount(2) .WithInputDelayFrames(2) .WithLogLevel(LogLevel.Information) .Build(); // Alternative: Use integer input type var intSession = RollbackNetcode .WithInputType(t => t.Integer<uint>()) .WithPort(9002) .Build(); // Alternative: Use raw struct input type (no endianness conversion) var structSession = RollbackNetcode .WithInputType(t => t.Struct<MyCustomInput>()) .Build(); ``` ## NetcodeSessionBuilder - Configure Session Options The fluent builder API allows configuring all aspects of the netcode session including network settings, frame rate, prediction limits, and custom services. Sessions can be built for remote multiplayer, local play, spectating, sync testing, or replay. ```csharp using Backdash; using Backdash.Core; using Backdash.Network; using System.Net; // Full session configuration example var session = RollbackNetcode .WithInputType<GameInput>() // Network configuration .WithPort(9001) .WithPlayerCount(2) .WithInputDelayFrames(2) .WithFrameRate(60) // Prediction and state management .Configure(options => { options.PredictionFrames = 16; // Max frames to predict ahead options.StateSizeHint = 1024; // Hint for state buffer allocation options.InputQueueLength = 128; // Input queue size }) // Protocol settings .ConfigureProtocol(protocol => { protocol.NumberOfSyncRoundTrips = 10; protocol.DisconnectTimeout = TimeSpan.FromSeconds(5); protocol.DisconnectNotifyStart = TimeSpan.FromSeconds(3); }) // Logging configuration .WithLogLevel(LogLevel.Warning) .WithFileLogWriter("logs/game_session.log", append: false) // Deterministic random seed (ensures same random across all peers) .WithInitialRandomSeed(42) // Enable stats collection .WithPackageStats() .WithPlayerStats() // Build for remote multiplayer .ForRemote() .Build(); ``` ## NetcodePlayer - Player Management NetcodePlayer represents a participant in the session. Players can be local (on this machine), remote (over network), or spectators. Each player has a unique ID and index assigned when added to the session. ```csharp using Backdash; using System.Net; // Create local player (controlled by this machine) var localPlayer = NetcodePlayer.CreateLocal(); // Create remote player with IP endpoint var remoteEndpoint = new IPEndPoint(IPAddress.Parse("192.168.1.100"), 8001); var remotePlayer = NetcodePlayer.CreateRemote(remoteEndpoint); // Create remote player on localhost (for testing) var localRemote = NetcodePlayer.CreateRemote(port: 8002); // Create spectator var spectator = NetcodePlayer.CreateSpectator(new IPEndPoint(IPAddress.Loopback, 8003)); // Add custom identifier for game logic localPlayer.CustomId = 1; remotePlayer.CustomId = 2; // Build session with players var session = RollbackNetcode .WithInputType<GameInput>() .WithPort(9001) .WithPlayers(localPlayer, remotePlayer) .ForRemote() .Build(); // Query players from session if (session.TryGetLocalPlayer(out var local)) Console.WriteLine($"Local player index: {local.Index}"); if (session.TryGetPlayerByCustomId(2, out var found)) Console.WriteLine($"Found player with CustomId 2: {found.Number}"); // Check player type bool isLocal = localPlayer.IsLocal(); // true bool isRemote = remotePlayer.IsRemote(); // true bool isSpectator = spectator.IsSpectator(); // true ``` ## INetcodeSessionHandler - Session Callbacks The handler interface defines callbacks that Backdash invokes during the session lifecycle. Implementations must provide deterministic state save/load and frame advance logic for rollback to function correctly. ```csharp using Backdash; using Backdash.Serialization; using Backdash.Serialization.Numerics; using System.Numerics; public class GameSessionHandler : INetcodeSessionHandler { private readonly INetcodeSession<GameInput> session; private GameState state = new(); public GameSessionHandler(INetcodeSession<GameInput> session) { this.session = session; } // Called when all peers are synchronized and game can start public void OnSessionStart() { Console.WriteLine("Session started - all players synchronized!"); state = GameState.CreateInitial(); } // Called when session is closing public void OnSessionClose() { Console.WriteLine("Session closed"); } // Save game state to binary buffer (called every frame for rollback support) public void SaveState(Frame frame, ref readonly BinaryBufferWriter writer) { // Write each state field - ORDER MATTERS for LoadState writer.Write(state.PlayerPositions); // Vector2[] writer.Write(state.PlayerHealth); // int[] writer.Write(state.Score); // int writer.Write(state.RandomSeed); // uint writer.Write(state.FrameCount); // int } // Restore game state from binary buffer (called during rollback) public void LoadState(Frame frame, ref readonly BinaryBufferReader reader) { // Read in SAME ORDER as SaveState state.PlayerPositions = reader.ReadArray<Vector2>(2); state.PlayerHealth = reader.ReadArray<int>(2); state.Score = reader.ReadInt32(); state.RandomSeed = reader.ReadUInt32(); state.FrameCount = reader.ReadInt32(); } // Called during rollback to re-simulate frames public void AdvanceFrame() { session.SynchronizeInputs(); var inputs = session.CurrentSynchronizedInputs; UpdateGameState(inputs[0].Input, inputs[1].Input); session.AdvanceFrame(); } // Called when local client is ahead and should slow down public void TimeSync(FrameSpan framesAhead) { // Sleep to let remote catch up Thread.Sleep(framesAhead.Duration()); } // Handle network events (connection, sync progress, disconnects) public void OnPeerEvent(NetcodePlayer player, PeerEventInfo evt) { switch (evt.Type) { case PeerEvent.Connected: Console.WriteLine($"Player {player.Number} connected"); break; case PeerEvent.Synchronizing: var progress = evt.Synchronizing.CurrentStep / (float)evt.Synchronizing.TotalSteps; Console.WriteLine($"Syncing: {progress:P0}"); break; case PeerEvent.Synchronized: Console.WriteLine($"Player {player.Number} synchronized (ping: {evt.Synchronized.Ping.TotalMilliseconds}ms)"); break; case PeerEvent.ConnectionInterrupted: Console.WriteLine($"Connection interrupted, timeout in {evt.ConnectionInterrupted.DisconnectTimeout}"); break; case PeerEvent.Disconnected: Console.WriteLine($"Player {player.Number} disconnected"); break; } } private void UpdateGameState(GameInput input1, GameInput input2) { // Game logic implementation state.FrameCount++; } } public class GameState { public Vector2[] PlayerPositions = new Vector2[2]; public int[] PlayerHealth = new int[2]; public int Score; public uint RandomSeed; public int FrameCount; public static GameState CreateInitial() => new() { PlayerPositions = [Vector2.Zero, new(100, 0)], PlayerHealth = [100, 100], }; } ``` ## INetcodeSession Game Loop Integration The session manages synchronization of inputs between local and remote players. Call BeginFrame at frame start, add local inputs, synchronize all inputs, update game state, then call AdvanceFrame. ```csharp using Backdash; public class GameLoop { private readonly INetcodeSession<GameInput> session; private readonly NetcodePlayer localPlayer; private GameState gameState; public GameLoop(INetcodeSession<GameInput> session) { this.session = session; session.TryGetLocalPlayer(out localPlayer!); gameState = GameState.CreateInitial(); } public void Update() { // 1. Signal frame start session.BeginFrame(); // 2. Read and add local input var localInput = ReadLocalInput(); var result = session.AddLocalInput(localPlayer, localInput); if (result != ResultCode.Ok) { // Handle error (prediction threshold, not synchronized, etc.) Console.WriteLine($"AddLocalInput failed: {result}"); return; } // 3. Synchronize inputs from all players result = session.SynchronizeInputs(); if (result != ResultCode.Ok) { // Don't advance game state if sync fails Console.WriteLine($"SynchronizeInputs failed: {result}"); return; } // 4. Get synchronized inputs for all players var inputs = session.CurrentSynchronizedInputs; // 5. Update game state deterministically for (int i = 0; i < inputs.Length; i++) { var syncInput = inputs[i]; if (syncInput.IsConnected) { ApplyInput(i, syncInput.Input); } } // 6. Use deterministic random (synced across all peers) var random = session.Random; int randomValue = random.NextInt(0, 100); // 7. Signal frame end session.AdvanceFrame(); } private GameInput ReadLocalInput() { GameInput input = GameInput.None; // Read from controller/keyboard if (IsKeyPressed(Key.Up)) input |= GameInput.Up; if (IsKeyPressed(Key.Down)) input |= GameInput.Down; if (IsKeyPressed(Key.Left)) input |= GameInput.Left; if (IsKeyPressed(Key.Right)) input |= GameInput.Right; return input; } private void ApplyInput(int playerIndex, GameInput input) { // Apply input to game state deterministically } private bool IsKeyPressed(Key key) => false; // Placeholder private enum Key { Up, Down, Left, Right } } ``` ## BinaryBufferWriter and BinaryBufferReader - State Serialization Binary serialization utilities for saving and loading game state. Support primitive types, arrays, Vector2/3/4, and custom types implementing IBinarySerializable. ```csharp using Backdash.Serialization; using Backdash.Serialization.Numerics; using System.Numerics; public class GameState { public Vector2 Position; public Vector3 Velocity; public int Health; public uint RandomSeed; public float Timer; public List<int> Inventory = new(); public bool IsAlive; } // In your INetcodeSessionHandler implementation: public void SaveState(Frame frame, ref readonly BinaryBufferWriter writer) { // Primitives writer.Write(state.Health); // int writer.Write(state.RandomSeed); // uint writer.Write(state.Timer); // float writer.Write(state.IsAlive); // bool // Vectors (use extension methods from Backdash.Serialization.Numerics) writer.Write(state.Position); // Vector2 writer.Write(state.Velocity); // Vector3 // Collections - write length first, then elements writer.Write(state.Inventory.Count); foreach (var item in state.Inventory) writer.Write(item); // Or use built-in list serialization writer.Write(state.Inventory); } public void LoadState(Frame frame, ref readonly BinaryBufferReader reader) { // Read in EXACT same order as SaveState state.Health = reader.ReadInt32(); state.RandomSeed = reader.ReadUInt32(); state.Timer = reader.ReadSingle(); state.IsAlive = reader.ReadBool(); state.Position = reader.ReadVector2(); state.Velocity = reader.ReadVector3(); // Read collection int count = reader.ReadInt32(); state.Inventory.Clear(); for (int i = 0; i < count; i++) state.Inventory.Add(reader.ReadInt32()); // Or use ReadList extension // state.Inventory = reader.ReadList<int>(); } ``` ## Spectator Session - Watch Live Games Spectator mode allows clients to watch an ongoing game session without participating. The spectator receives synchronized inputs from all players and can render the game state. ```csharp using Backdash; using System.Net; // Create spectator session connecting to host player var spectatorSession = RollbackNetcode .WithInputType<GameInput>() .WithPort(9002) .WithPlayerCount(2) // Must match the game session .ForSpectator(hostEndpoint: new IPEndPoint(IPAddress.Parse("192.168.1.100"), 9001)) .WithFileLogWriter("logs/spectator.log") .Build(); // Alternative: Spectator on localhost var localSpectator = RollbackNetcode .WithInputType<GameInput>() .WithPort(9003) .WithPlayerCount(2) .ForSpectator(hostPort: 9001) // Connect to localhost:9001 .Build(); // Set handler and start spectatorSession.SetHandler(new SpectatorHandler(spectatorSession)); spectatorSession.Start(); // Spectator game loop public class SpectatorHandler : INetcodeSessionHandler { private readonly INetcodeSession<GameInput> session; public SpectatorHandler(INetcodeSession<GameInput> session) { this.session = session; } public void OnSessionStart() => Console.WriteLine("Spectating started!"); public void AdvanceFrame() { session.SynchronizeInputs(); var inputs = session.CurrentSynchronizedInputs; // Render game state based on received inputs RenderGame(inputs); session.AdvanceFrame(); } private void RenderGame(ReadOnlySpan<SynchronizedInput<GameInput>> inputs) { // Spectator rendering logic } // ... other callbacks } ``` ## SyncTest Session - Determinism Verification Sync test mode runs multiple copies of the game simulation in parallel to verify determinism. Any state divergence indicates a bug in deterministic logic. ```csharp using Backdash; // Create sync test session var syncTestSession = RollbackNetcode .WithInputType<GameInput>() .WithPlayerCount(2) .ForSyncTest(options => { options.CheckDistance = 1; // Frames between state comparisons }) .WithLogLevel(LogLevel.Debug) .Build(); // Provide random inputs for testing syncTestSession.SetHandler(new SyncTestHandler(syncTestSession)); syncTestSession.Start(); public class SyncTestHandler : INetcodeSessionHandler { private readonly INetcodeSession<GameInput> session; private readonly Random random = new(42); private GameState state = new(); public SyncTestHandler(INetcodeSession<GameInput> session) { this.session = session; } public void OnSessionStart() { // Feed random inputs for sync testing foreach (var player in session.GetPlayers()) { var input = (GameInput)random.Next(0, 127); session.AddLocalInput(player, input); } } // If SaveState/LoadState produce different results, // sync test will detect the desync and report it public void SaveState(Frame frame, ref readonly BinaryBufferWriter writer) { writer.Write(state.FrameCount); writer.Write(state.Checksum); } public void LoadState(Frame frame, ref readonly BinaryBufferReader reader) { state.FrameCount = reader.ReadInt32(); state.Checksum = reader.ReadUInt32(); } // ... other callbacks } ``` ## Replay Session - Record and Playback Record confirmed inputs during gameplay and replay them later for debugging, highlights, or training modes. ```csharp using Backdash; using Backdash.Synchronizing.Input.Confirmed; // During gameplay: Enable input history saving var gameSession = RollbackNetcode .WithInputType<GameInput>() .WithPort(9001) .WithPlayers(player1, player2) .WithConfirmedInputHistory(enabled: true) // Save all confirmed inputs .ForRemote() .Build(); // After game ends: Get confirmed inputs for replay IReadOnlyList<ConfirmedInputs<GameInput>> confirmedInputs = gameSession.GetConfirmedInputs(); byte[] inputBytes = gameSession.GetConfirmedInputsBytes(); // Save to file for later File.WriteAllBytes("replay.dat", inputBytes); // Create replay session var replaySession = RollbackNetcode .WithInputType<GameInput>() .WithPlayerCount(2) .ForReplay(options => { options.Inputs = confirmedInputs; // Or load from bytes: options.LoadFromBytes(inputBytes); }) .Build(); // Control replay playback replaySession.SetHandler(new ReplayHandler(replaySession)); replaySession.Start(); var controller = replaySession.ReplayController; if (controller != null) { controller.Speed = 1.0f; // Normal speed controller.Speed = 0.5f; // Half speed controller.Speed = 2.0f; // Double speed controller.Pause(); // Pause playback controller.Resume(); // Resume playback // controller.SeekTo(frame); // Jump to specific frame } ``` ## INetcodeRandom - Deterministic Random Numbers The session provides a synchronized random number generator that produces identical sequences on all peers when given the same seed and inputs. ```csharp using Backdash; using Backdash.Synchronizing.Random; public class GameLogic { private readonly INetcodeSession<GameInput> session; public void Update(ref GameState state) { // Set seed before synchronizing inputs (must be deterministic!) session.SetRandomSeed(state.RandomSeed); session.SynchronizeInputs(); // Access deterministic random after synchronization INetcodeRandom random = session.Random; // Generate random values (identical on all peers) uint rawRandom = random.Next(); // uint [0, uint.MaxValue] int intRandom = random.NextInt(); // int [0, int.MaxValue) int rangedInt = random.NextInt(1, 100); // int [1, 100) int maxInt = random.NextInt(10); // int [0, 10) float floatRandom = random.NextFloat(); // float [0.0, 1.0) // Use for game logic int damage = random.NextInt(10, 20); bool criticalHit = random.NextFloat() < 0.1f; int spawnPosition = random.NextInt(0, 5); // Store seed for next frame (ensures rollback consistency) state.RandomSeed = random.CurrentSeed; session.AdvanceFrame(); } } ``` ## Custom Input Serializer - Complex Input Types For input types requiring specific serialization (e.g., analog sticks with endianness conversion), implement a custom IBinarySerializer. ```csharp using Backdash; using Backdash.Serialization; using System.Runtime.InteropServices; [Flags] public enum PadButtons : short { None = 0, A = 1 << 0, B = 1 << 1, X = 1 << 2, Y = 1 << 3, Up = 1 << 4, Down = 1 << 5, Left = 1 << 6, Right = 1 << 7, LeftBumper = 1 << 8, RightBumper = 1 << 9, } [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct GamepadInput { public PadButtons Buttons; public byte LeftTrigger; public byte RightTrigger; public sbyte LeftStickX; public sbyte LeftStickY; public sbyte RightStickX; public sbyte RightStickY; } // Custom serializer with endianness support public class GamepadInputSerializer : BinarySerializer<GamepadInput> { protected override void Serialize(in BinarySpanWriter writer, in GamepadInput data) { writer.Write((short)data.Buttons); writer.Write(data.LeftTrigger); writer.Write(data.RightTrigger); writer.Write(data.LeftStickX); writer.Write(data.LeftStickY); writer.Write(data.RightStickX); writer.Write(data.RightStickY); } protected override void Deserialize(in BinaryBufferReader reader, ref GamepadInput result) { result.Buttons = (PadButtons)reader.ReadInt16(); result.LeftTrigger = reader.ReadByte(); result.RightTrigger = reader.ReadByte(); result.LeftStickX = reader.ReadSByte(); result.LeftStickY = reader.ReadSByte(); result.RightStickX = reader.ReadSByte(); result.RightStickY = reader.ReadSByte(); } } // Use custom serializer var session = RollbackNetcode .WithInputType(t => t.Custom(new GamepadInputSerializer())) .WithPort(9001) .Build(); ``` ## Complete Game Example A minimal but complete example showing session setup, game loop, and handler implementation. ```csharp using Backdash; using Backdash.Core; using Backdash.Serialization; using System.Numerics; [Flags] public enum GameInput { None = 0, Up = 1, Down = 2, Left = 4, Right = 8 } public class Program { public static async Task Main(string[] args) { using var cts = new CancellationTokenSource(); Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; // Parse args: --port 9001 --remote 192.168.1.100:9002 int localPort = 9001; var remoteEndpoint = IPEndPoint.Parse("127.0.0.1:9002"); // Build session var session = RollbackNetcode .WithInputType<GameInput>() .WithPort(localPort) .WithPlayers( NetcodePlayer.CreateLocal(), NetcodePlayer.CreateRemote(remoteEndpoint) ) .WithInputDelayFrames(2) .WithInitialRandomSeed(42) .WithLogLevel(LogLevel.Information) .ForRemote() .Build(); // Create and set handler var game = new Game(session, cts); session.SetHandler(game); // Start networking session.Start(cts.Token); // Run game loop at 60 FPS await game.RunLoop(cts.Token); // Cleanup session.Dispose(); await session.WaitUntilFinish(); } } public class Game : INetcodeSessionHandler { private readonly INetcodeSession<GameInput> session; private readonly CancellationTokenSource cts; private Vector2 playerPos = Vector2.Zero; private bool isRunning; public Game(INetcodeSession<GameInput> session, CancellationTokenSource cts) { this.session = session; this.cts = cts; } public async Task RunLoop(CancellationToken ct) { using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(1000.0 / 60)); try { while (await timer.WaitForNextTickAsync(ct)) Update(); } catch (OperationCanceledException) { } } private void Update() { if (!isRunning) return; session.BeginFrame(); // Read and submit local input var input = ReadInput(); if (session.TryGetLocalPlayer(out var local)) { if (session.AddLocalInput(local, input) != ResultCode.Ok) return; } // Sync and apply inputs if (session.SynchronizeInputs() != ResultCode.Ok) return; foreach (var syncInput in session.CurrentSynchronizedInputs) { if (syncInput.IsConnected) ApplyInput(syncInput.Input); } session.AdvanceFrame(); } private GameInput ReadInput() { if (!Console.KeyAvailable) return GameInput.None; return Console.ReadKey(true).Key switch { ConsoleKey.W => GameInput.Up, ConsoleKey.S => GameInput.Down, ConsoleKey.A => GameInput.Left, ConsoleKey.D => GameInput.Right, ConsoleKey.Escape => (cts.Cancel(), GameInput.None).Item2, _ => GameInput.None }; } private void ApplyInput(GameInput input) { if (input.HasFlag(GameInput.Up)) playerPos.Y -= 1; if (input.HasFlag(GameInput.Down)) playerPos.Y += 1; if (input.HasFlag(GameInput.Left)) playerPos.X -= 1; if (input.HasFlag(GameInput.Right)) playerPos.X += 1; } public void OnSessionStart() => isRunning = true; public void OnSessionClose() => isRunning = false; public void SaveState(Frame frame, ref readonly BinaryBufferWriter writer) { writer.Write(playerPos.X); writer.Write(playerPos.Y); } public void LoadState(Frame frame, ref readonly BinaryBufferReader reader) { playerPos.X = reader.ReadSingle(); playerPos.Y = reader.ReadSingle(); } public void AdvanceFrame() { session.SynchronizeInputs(); foreach (var input in session.CurrentSynchronizedInputs) ApplyInput(input.Input); session.AdvanceFrame(); } public void TimeSync(FrameSpan framesAhead) => Thread.Sleep(framesAhead.Duration()); public void OnPeerEvent(NetcodePlayer player, PeerEventInfo evt) => Console.WriteLine($"[{player.Number}] {evt.Type}"); } ``` ## Summary Backdash provides a complete rollback netcode solution for .NET multiplayer games, handling network communication, input prediction, state synchronization, and rollback execution. The library supports multiple session modes including remote multiplayer, local play, spectating, sync testing for determinism verification, and replay functionality. Integration requires implementing the INetcodeSessionHandler interface for state save/load and frame advance callbacks, then using the fluent builder API to configure and create sessions. The architecture is designed for fighting games and other latency-sensitive competitive games where frame-perfect input timing is essential. Developers should use flags enums for compact input encoding, ensure fully deterministic game logic, and implement efficient binary state serialization. The deterministic random number generator, automatic input delay handling, and connection event callbacks provide all the tools needed to build professional-grade online multiplayer experiences in Godot, Monogame, Stride3D, or any other .NET game engine.