Skip to content

Commit b2617ac

Browse files
committed
add desync handler sample and docs
1 parent 21ad459 commit b2617ac

File tree

11 files changed

+208
-91
lines changed

11 files changed

+208
-91
lines changed

docfx/docs/developer_guide.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ state as well.
3434
Each player in a [Backdash](https://github.com/lucasteles/Backdash) networked game has a complete copy of your game
3535
running. [Backdash](https://github.com/lucasteles/Backdash) needs to keep both copies of the
3636
game state in sync to ensure that both players are experiencing the same game. It would be much too expensive to send an
37-
entire copy of the game state between players every frame. Instead [Backdash](https://github.com/lucasteles/Backdash)
37+
entire copy of the game state between players every frame. Instead, [Backdash](https://github.com/lucasteles/Backdash)
3838
sends the players' inputs to each other and has
3939
each player step the game forward. In order for this to work, your game engine must meet three criteria:
4040

@@ -98,14 +98,14 @@ var session = RollbackNetcode
9898
```
9999

100100
> [!TIP]
101-
> If you want to use a integer type as your input type:
101+
> If you want to use an integer type as your input type:
102102
>
103103
> `RollbackNetcode.WithInputType(t => t.Integer<uint>())`
104104
105105
The session builder can be used to configure the session by setting [`NetcodeOptions`](https://lucasteles.github.io/Backdash/api/Backdash.NetcodeOptions.html):
106106
- passing an instance to `.WithOptions(..)`
107-
- using the a delegate function on `.Configure(options => {})`
108-
- using the a fluent api
107+
- using a delegate function on `.Configure(options => {})`
108+
- using the fluent api
109109

110110

111111
```csharp

docfx/docs/troubleshooting.md

Lines changed: 95 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ not starting a game from scratch. Most applications will already conform to most
77
## Isolate **Game State** from **Non-Game State**
88

99
[Backdash](https://github.com/lucasteles/Backdash) will periodically request that you save and load the entire state of
10-
your game. For most games, the state that needs to be saved is a tiny fraction of the entire game. Usually, the video and
10+
your game. For most games, the state that needs to be saved is a tiny fraction of the entire game. Usually, the video
11+
and
1112
audio renderers, look-up tables, textures, sound data, and your code segments are either constant from frame to frame or
1213
not involved in the calculation of the game state. These do not need to be saved or restored.
1314

@@ -63,7 +64,8 @@ times [Backdash](https://github.com/lucasteles/Backdash) needs to rollback to th
6364
### Beware of External Time Sources (eg. random, clock time)
6465

6566
Be careful if you use the current time of day in your game state calculation. This may be used for an effect on the game
66-
or to derive another game state (e.g. using the timer as a seed to the random number generator). The time on two computers
67+
or to derive another game state (e.g. using the timer as a seed to the random number generator). The time on two
68+
computers
6769
or game consoles is almost never in sync and using time in your game state calculations can lead to synchronization
6870
issues. You should either eliminate the use of time in your game state or include the current time for one of the
6971
players as part of the input to a frame and always use that time in your calculations.
@@ -72,8 +74,11 @@ The use of external time sources in **non-game state** calculations is fine (_e.
7274
screen, or the attenuation of audio samples_).
7375

7476
> [!INFORMATION]
75-
> We provide an implementation of a _[Deterministic Random](https://lucasteles.github.io/Backdash/api/Backdash.Synchronizing.Random.IDeterministicRandom.html)_ out of the box
76-
> which can be accessed directly from the _[Rollback Session](https://lucasteles.github.io/Backdash/api/Backdash.INetcodeSession-1.html#Backdash_INetcodeSession_1_Random)_
77+
> We provide an implementation of a
78+
_[Deterministic Random](https://lucasteles.github.io/Backdash/api/Backdash.Synchronizing.Random.IDeterministicRandom.html)_
79+
> out of the box
80+
> which can be accessed directly from the
81+
_[Rollback Session](https://lucasteles.github.io/Backdash/api/Backdash.INetcodeSession-1.html#Backdash_INetcodeSession_1_Random)_
7782
7883
## Beware of Dangling References
7984

@@ -85,7 +90,8 @@ reference instead of the values.
8590

8691
The language your game is written in may have features that make it difficult to track down all your state. [Static
8792
automatic variables in `C`](https://www.javatpoint.com/auto-and-static-variable-in-c)
88-
or [static members in `C#`](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/static-classes-and-static-class-members)
93+
or [static members in
94+
`C#`](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/static-classes-and-static-class-members)
8995
are examples of this behavior. You need to track down all these locations and convert them to
9096
a form that can be saved. For example, compare:
9197

@@ -129,14 +135,34 @@ public class MySessionHandler : INetcodeSessionHandler
129135
}
130136
```
131137

132-
## Use the [Backdash](https://github.com/lucasteles/Backdash) Sync Test Feature. A Lot.
138+
## Use the [Backdash](https://github.com/lucasteles/Backdash) SyncTest Feature. A Lot.
133139

134-
Once you've ported your application to [Backdash](https://github.com/lucasteles/Backdash), you can use
135-
the [`CreateSyncTestSession`](https://lucasteles.github.io/Backdash/api/Backdash.RollbackNetcode.html#Backdash_RollbackNetcode_CreateSyncTestSession__2_System_Nullable_Backdash_Data_FrameSpan__Backdash_NetcodeOptions_Backdash_SessionServices___0___1__System_Boolean_)
136-
function to help track down synchronization issues which may be the result of a leaky game state.
140+
Once you've ported your application to [Backdash](https://github.com/lucasteles/Backdash),
141+
you can use tool called `SyncTest` to help track down synchronization issues which may be the result of a leaky game
142+
state.
137143

138-
The sync test session is a special, single player session which is designed to find errors in your simulation's
139-
determinism. When running in a **sync-test session**, [Backdash](https://github.com/lucasteles/Backdash) by default will
144+
To create a sync test session use:
145+
146+
```csharp
147+
using Backdash;
148+
149+
var session = RollbackNetcode
150+
.WithInputType<MyGameInput>()
151+
.Configure(options =>
152+
{
153+
// ...
154+
})
155+
.ForSyncTest(options => options
156+
{
157+
// ...
158+
})
159+
.Build();
160+
```
161+
162+
The sync test session is a special session which is designed to find errors in your simulation's
163+
determinism.
164+
165+
When running in a **sync-test session**, [Backdash](https://github.com/lucasteles/Backdash) by default will
140166
execute a 1 frame rollback for every frame of your game. It compares the state of the frame when it was executed the
141167
first time to the state executed during the rollback, and raises an error if they differ during your game's execution.
142168
If you set the [`LogLevel`](https://lucasteles.github.io/Backdash/api/Backdash.Core.LogLevel.html) to at
@@ -146,56 +172,76 @@ the rollback frame to track down errors.
146172
By running **sync-test** on developer systems continuously when writing game code, you can identify **de-sync** causing
147173
bugs immediately after they're introduced.
148174

149-
You can also set the **sync-test session** to auto-generate random inputs to help find de-syncs:
175+
### Configuring
150176

151-
```csharp
152-
using Backdash;
153-
using Backdash.Sync.Input;
177+
You can configure a wide range of options to help debug you state, like:
154178

155-
var session = RollbackNetcode.CreateSyncTestSession<MyGameInput>(
156-
options: new()
157-
{
158-
Log = new()
159-
{
160-
EnabledLevel = LogLevel.Debug,
161-
},
162-
},
163-
services: new()
164-
{
165-
InputGenerator = new RandomInputGenerator<MyGameInput>(),
166-
}
167-
);
179+
```csharp
180+
.ForSyncTest(options => options
181+
.UseJsonStateParser() // tries to display you state as json on desync
182+
.UseDesyncHandler<YourDesyncHandler>() // custom handler to deal wih a desync
183+
.UseRandomInputProvider() // generate random inputs
184+
.CheckDistance(4) // the forced rollback check distance in frames
185+
)
168186
```
169187

170-
If you want a meaningful string representation of your state in the sync-test, you need to implement
171-
the method `GetStateString` in your handler.
188+
If you need better meaningful string representation of your state in the sync-test, it is recommended to implement
189+
the optional method `INetcodeSessionHandler.CreateState`. This will be used to materialize previous saved states
190+
before passing them to the `DesyncHandler`.
172191

173-
```csharp
174-
// print the state as json
175-
public string GetStateString(in Frame frame, ref readonly BinaryBufferReader reader)
176-
{
177-
GameState state = new();
192+
> [!NOTE]
193+
> The `.UseJsonStateParser()` will only work if `INetcodeSessionHandler.CreateState` returns a JSON serializable object.
178194

179-
state.LoadState(in reader); // read and fill the state properties
195+
#### Implement a DesyncHandler
180196

181-
return JsonSerializer.Serialize(state, jsonOptions);
182-
}
197+
a `DesyncHandler` will help you to handle whenever a desync happens in a `SyncTest` session.
183198

184-
static readonly JsonSerializerOptions jsonOptions = new()
185-
{
186-
WriteIndented = true,
187-
IncludeFields = true,
188-
};
199+
As example, a `DesyncHandler` that uses [DiffPlex](https://github.com/mmanela/diffplex) to print on the console
200+
the state diff when a desync occurs:
189201

190-
```
202+
```csharp
203+
using Backdash;
204+
using Backdash.Synchronizing.State;
205+
using DiffPlex.DiffBuilder;
206+
using DiffPlex.DiffBuilder.Model;
191207

192-
> [!NOTE]
193-
> For better debugging the `RollbackNetcode.WithInputType<>().ForSyncTest(...)` accepts an implementation of the `IStateDesyncHandler`
194-
> which is called whenever a state desync happens in the test.
195-
>
196-
> You can use it for enhanced state logging or showing semantic diffs.
208+
sealed class DiffPlexDesyncHandler : IStateDesyncHandler
209+
{
210+
public void Handle(INetcodeSession session, in StateSnapshot previous, in StateSnapshot current)
211+
{
212+
var diff = InlineDiffBuilder.Diff(previous.Value, current.Value);
213+
214+
var savedColor = Console.ForegroundColor;
197215

216+
foreach (var line in diff.Lines)
217+
{
218+
switch (line.Type)
219+
{
220+
case ChangeType.Inserted:
221+
Console.ForegroundColor = ConsoleColor.Green;
222+
Console.Write("+ ");
223+
break;
224+
case ChangeType.Deleted:
225+
Console.ForegroundColor = ConsoleColor.Red;
226+
Console.Write("- ");
227+
break;
228+
default:
229+
Console.ForegroundColor = ConsoleColor.Gray;
230+
Console.Write(" ");
231+
break;
232+
}
233+
234+
Console.WriteLine(line.Text);
235+
}
236+
237+
Console.ForegroundColor = savedColor;
238+
}
239+
}
240+
```
198241

242+
> [!TIP]
243+
> You can see this implementation working with JSON in
244+
> the [SpaceWar](https://github.com/lucasteles/Backdash/tree/master/samples/SpaceWar) sample.
199245
200246
## Where to Go from Here
201247

samples/SpaceWar.Shared/GameSession.cs

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,30 @@ void Log(string message)
7171
Console.WriteLine(message);
7272
}
7373

74+
public void SaveState(in Frame frame, ref readonly BinaryBufferWriter writer)
75+
{
76+
// UNCOMMENT TO FORCE DESYNC ON FRAME 200
77+
#pragma warning disable S125
78+
// if (frame.Number is 200)
79+
// gameState.FrameNumber = Random.Shared.Next(1000, 2000);
80+
#pragma warning restore S125
81+
82+
gameState.SaveState(in writer);
83+
}
84+
85+
public void LoadState(in Frame frame, ref readonly BinaryBufferReader reader)
86+
{
87+
Log($"=> LOADING STATE {frame}...");
88+
gameState.LoadState(in reader);
89+
}
90+
91+
public void AdvanceFrame()
92+
{
93+
session.SynchronizeInputs();
94+
gameState.Update(session.CurrentSynchronizedInputs);
95+
session.AdvanceFrame();
96+
}
97+
7498
public void OnSessionStart()
7599
{
76100
Log("=> GAME STARTED");
@@ -93,18 +117,9 @@ public void TimeSync(FrameSpan framesAhead)
93117
void UpdateStats()
94118
{
95119
nonGameState.RollbackFrames = session.RollbackFrames;
96-
97120
var saved = session.GetCurrentSavedFrame();
98121
nonGameState.StateChecksum = saved.Checksum;
99122
nonGameState.StateSize = saved.Size;
100-
101-
for (var i = 0; i < nonGameState.Players.Length; i++)
102-
{
103-
ref var info = ref nonGameState.Players[i];
104-
if (!info.PlayerHandle.IsRemote())
105-
continue;
106-
session.UpdateNetworkStats(info.PlayerHandle);
107-
}
108123
}
109124

110125
public void OnPeerEvent(NetcodePlayer player, PeerEventInfo evt)
@@ -143,23 +158,8 @@ public void OnPeerEvent(NetcodePlayer player, PeerEventInfo evt)
143158
}
144159
}
145160

146-
public void SaveState(in Frame frame, ref readonly BinaryBufferWriter writer) =>
147-
gameState.SaveState(in writer);
148-
149-
public void LoadState(in Frame frame, ref readonly BinaryBufferReader reader)
150-
{
151-
Log($"=> LOADING STATE {frame}...");
152-
gameState.LoadState(in reader);
153-
}
154-
155-
public void AdvanceFrame()
156-
{
157-
session.SynchronizeInputs();
158-
gameState.Update(session.CurrentSynchronizedInputs);
159-
session.AdvanceFrame();
160-
}
161-
162-
object INetcodeSessionHandler.ParseState(in Frame frame, ref readonly BinaryBufferReader reader)
161+
// used by SyncTest, the return value is used on the state desync handler call
162+
object INetcodeSessionHandler.CreateState(in Frame frame, ref readonly BinaryBufferReader reader)
163163
{
164164
GameState state = new();
165165
state.LoadState(in reader);

samples/SpaceWar/Program.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,18 @@ static INetcodeSession<PlayerInputs> ParseSessionArgs(string[] args)
6161
case ["sync-test-auto", ..]:
6262
return builder
6363
.ForSyncTest(options => options
64-
.UseJsonStateViewer()
64+
.UseJsonStateParser()
65+
.UseDesyncHandler<DiffPlexDesyncHandler>()
6566
.UseRandomInputProvider()
6667
)
6768
.Build();
6869

6970
case ["sync-test", ..]:
7071
return builder
71-
.ForSyncTest(options => options.UseJsonStateViewer())
72+
.ForSyncTest(options => options
73+
.UseJsonStateParser()
74+
.UseDesyncHandler<DiffPlexDesyncHandler>()
75+
)
7276
.Build();
7377

7478
default:

samples/SpaceWar/SpaceWar.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,7 @@
2424
<ItemGroup>
2525
<ProjectReference Include="..\SpaceWar.Shared\SpaceWar.Shared.csproj"/>
2626
</ItemGroup>
27+
<ItemGroup>
28+
<PackageReference Include="DiffPlex" Version="1.7.2" />
29+
</ItemGroup>
2730
</Project>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using Backdash;
2+
using Backdash.Synchronizing.State;
3+
using DiffPlex.DiffBuilder;
4+
using DiffPlex.DiffBuilder.Model;
5+
6+
namespace SpaceWar;
7+
8+
/// <summary>
9+
/// This prints the text diff of a state when a desync happens over a SyncTest session.
10+
/// </summary>
11+
sealed class DiffPlexDesyncHandler : IStateDesyncHandler
12+
{
13+
public void Handle(INetcodeSession session, in StateSnapshot previous, in StateSnapshot current)
14+
{
15+
var diff = InlineDiffBuilder.Diff(previous.Value, current.Value);
16+
17+
var savedColor = Console.ForegroundColor;
18+
19+
foreach (var line in diff.Lines)
20+
{
21+
switch (line.Type)
22+
{
23+
case ChangeType.Inserted:
24+
Console.ForegroundColor = ConsoleColor.Green;
25+
Console.Write("+ ");
26+
break;
27+
case ChangeType.Deleted:
28+
Console.ForegroundColor = ConsoleColor.Red;
29+
Console.Write("- ");
30+
break;
31+
default:
32+
Console.ForegroundColor = ConsoleColor.Gray;
33+
Console.Write(" ");
34+
break;
35+
}
36+
37+
Console.WriteLine(line.Text);
38+
}
39+
40+
Console.ForegroundColor = savedColor;
41+
}
42+
}
File renamed without changes.

0 commit comments

Comments
 (0)