Skip to content

Commit

Permalink
patch: input pop-count based random seed (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasteles authored May 27, 2024
1 parent 7e137a6 commit a75794d
Show file tree
Hide file tree
Showing 31 changed files with 397 additions and 69 deletions.
24 changes: 21 additions & 3 deletions samples/ConsoleGame/Program.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
// ReSharper disable AccessToDisposedClosure, UnusedVariable

using System.Diagnostics.CodeAnalysis;
using System.Net;
using Backdash;
using Backdash.Core;
using Backdash.Data;
using ConsoleGame;

var frameDuration = FrameSpan.GetDuration(1);
using CancellationTokenSource cts = new();

// stops the game with ctr+c
Console.CancelKeyPress += (_, eventArgs) =>
{
Expand All @@ -21,13 +24,14 @@
|| !int.TryParse(playerCountArg, out var playerCount)
)
throw new InvalidOperationException("Invalid port argument");

// netcode configurations
RollbackOptions options = new()
{
FrameDelay = 2,
Log = new()
{
EnabledLevel = LogLevel.Debug,
EnabledLevel = LogLevel.Information,
},
Protocol = new()
{
Expand All @@ -37,8 +41,10 @@
// DelayStrategy = Backdash.Network.DelayStrategy.Constant,
},
};
// setup the rollback network session

// Set up the rollback network session
IRollbackSession<GameInput, GameState> session;

// parse console arguments checking if it is a spectator
if (endpoints is ["spectate", { } hostArg] && IPEndPoint.TryParse(hostArg, out var host))
session = RollbackNetcode.CreateSpectatorSession<GameInput, GameState>(
Expand All @@ -50,12 +56,16 @@
// not a spectator, creating a peer 2 peer game session
else
session = CreatePlayerSession(port, options, ParsePlayers(playerCount, endpoints));

// create the actual game
Game game = new(session, cts);

// set the session callbacks (like save state, load state, network events, etc..)
session.SetHandler(game);

// start background worker, like network IO, async messaging
session.Start(cts.Token);

try
{
// kinda run a game-loop using a timer
Expand All @@ -67,10 +77,12 @@
{
// skip
}

// finishing the session
session.Dispose();
await session.WaitToStop();
Console.Clear();

// -------------------------------------------------------------- //
// Create and configure a game session //
// -------------------------------------------------------------- //
Expand All @@ -91,23 +103,27 @@ Player[] players
new()
{
LogWriter = fileLogWriter,
// StateSerializer = new MyStateSerializer(),
// StateSerializer = new GameStateSerializer(),
}
);
session.AddPlayers(players);
return session;
}

static Player[] ParsePlayers(int totalNumberOfPlayers, IEnumerable<string> endpoints)
{
var players = endpoints
.Select((x, i) => TryParsePlayer(totalNumberOfPlayers, i + 1, x, out var player)
? player
: throw new InvalidOperationException("Invalid endpoint address"))
.ToArray();

if (players.All(x => !x.IsLocal()))
throw new InvalidOperationException("No defined local player");

return players;
}

static bool TryParsePlayer(
int totalNumber,
int number, string address,
Expand All @@ -118,6 +134,7 @@ static bool TryParsePlayer(
player = new LocalPlayer(number);
return true;
}

if (IPEndPoint.TryParse(address, out var endPoint))
{
if (number <= totalNumber)
Expand All @@ -126,6 +143,7 @@ static bool TryParsePlayer(
player = new Spectator(endPoint);
return true;
}

player = null;
return false;
}
3 changes: 1 addition & 2 deletions samples/ConsoleGame/Util.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@

namespace ConsoleGame;

[StateSerializer<GameState>]
[BinarySerializer<GameState>]
public partial class GameStateSerializer;

1 change: 0 additions & 1 deletion samples/SpaceWar.Lobby/Scenes/ChooseModeScene.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using SpaceWar.Models;
using SpaceWar.Services;

namespace SpaceWar.Scenes;

Expand Down
1 change: 0 additions & 1 deletion samples/SpaceWar.Lobby/Scenes/ChooseNameScene.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Text;
using System.Text.RegularExpressions;
using SpaceWar.Services;

namespace SpaceWar.Scenes;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace SpaceWar.Services;
namespace SpaceWar;

public class KeyboardController
{
Expand Down
28 changes: 25 additions & 3 deletions samples/SpaceWar/Game1.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#nullable disable
using Backdash;
using Backdash.Core;
using Backdash.Synchronizing;
using SpaceWar.Logic;

namespace SpaceWar;
Expand All @@ -9,6 +10,8 @@ public class Game1 : Game
{
readonly GraphicsDeviceManager graphics;
readonly IRollbackSession<PlayerInputs, GameState> rollbackSession;
readonly SessionReplayControl replayControls = new();
readonly KeyboardController keyboard = new();

readonly RollbackOptions options = new()
{
Expand Down Expand Up @@ -36,7 +39,7 @@ public Game1(string[] args)
graphics = new(this);
Content.RootDirectory = "Content";
IsMouseVisible = true;
rollbackSession = GameSessionFactory.ParseArgs(args, options);
rollbackSession = GameSessionFactory.ParseArgs(args, options, replayControls);
}

protected override void Initialize()
Expand Down Expand Up @@ -102,7 +105,7 @@ protected override void LoadContent()
ngs.StatusText.Append("Connecting to peers ...");
}

if (rollbackSession.IsSpectating)
if (rollbackSession.Mode is SessionMode.Spectating)
{
Window.Title = "SpaceWar - Spectator";
Window.Position = Window.Position with
Expand Down Expand Up @@ -141,12 +144,31 @@ void ConfigurePlayerWindow(PlayerHandle player)

protected override void Update(GameTime gameTime)
{
if (Keyboard.GetState().IsKeyDown(Keys.Escape))
keyboard.Update();
if (keyboard.IsKeyPressed(Keys.Escape))
Exit();

HandleReplayKeys();

gameSession.Update(gameTime);
base.Update(gameTime);
}

void HandleReplayKeys()
{
if (rollbackSession.Mode is not SessionMode.Replaying)
return;

if (keyboard.IsKeyPressed(Keys.Space))
replayControls.TogglePause();

if (keyboard.IsKeyPressed(Keys.Right))
replayControls.Play();

if (keyboard.IsKeyPressed(Keys.Left))
replayControls.Play(backwards: true);
}

protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
Expand Down
7 changes: 5 additions & 2 deletions samples/SpaceWar/GameSessionFactory.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Net;
using Backdash;
using Backdash.Synchronizing;
using Backdash.Synchronizing.Input;
using Backdash.Synchronizing.Input.Confirmed;
using SpaceWar.Logic;
Expand All @@ -9,7 +10,9 @@ namespace SpaceWar;
public static class GameSessionFactory
{
public static IRollbackSession<PlayerInputs, GameState> ParseArgs(
string[] args, RollbackOptions options
string[] args,
RollbackOptions options,
SessionReplayControl replayControls
)
{
if (args is not [{ } portArg, { } playerCountArg, .. { } lastArgs]
Expand Down Expand Up @@ -42,7 +45,7 @@ public static IRollbackSession<PlayerInputs, GameState> ParseArgs(
var inputs = SaveInputsToFileListener.GetInputs(playerCount, replayFile).ToArray();

return RollbackNetcode.CreateReplaySession<PlayerInputs, GameState>(
playerCount, inputs
playerCount, inputs, controls: replayControls
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/Backdash.Analyzers/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Backdash.Analyzers;

static class Parser
{
const string BackdashSerializerAttribute = "Backdash.Serialization.StateSerializer";
const string BackdashSerializerAttribute = "Backdash.Serialization.BinarySerializerAttribute";

public static bool IsTypeTargetForGeneration(SyntaxNode node)
=> node is TypeDeclarationSyntax { AttributeLists.Count: > 0 } t
Expand Down
29 changes: 20 additions & 9 deletions src/Backdash/Backends/Peer2PeerBackend.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ sealed class Peer2PeerBackend<TInput, TGameState> : IRollbackSession<TInput, TGa
Frame nextListenerFrame = Frame.Zero;
IRollbackHandler<TGameState> callbacks;
SynchronizedInput<TInput>[] syncInputBuffer = [];
TInput[] inputBuffer = [];
Task backgroundJobTask = Task.CompletedTask;
readonly ushort magicNumber;
readonly ushort syncNumber;
bool disposed;
bool closed;

Expand All @@ -69,7 +70,7 @@ BackendServices<TInput, TGameState> services
logger = services.Logger;
inputListener = services.InputListener;
deterministicRandom = services.DeterministicRandom;
magicNumber = services.Random.MagicNumber();
syncNumber = services.Random.MagicNumber();

peerInputEventQueue = new();
peerCombinedInputsEventPublisher = new ProtocolCombinedInputsEventPublisher<TInput>(peerInputEventQueue);
Expand Down Expand Up @@ -140,9 +141,11 @@ public void Close()
public FrameSpan RollbackFrames => synchronizer.RollbackFrames;
public FrameSpan FramesBehind => synchronizer.FramesBehind;
public int NumberOfPlayers => addedPlayers.Count;

public int NumberOfSpectators => addedSpectators.Count;
public IDeterministicRandom Random => deterministicRandom;
public bool IsSpectating => false;
public SessionMode Mode => SessionMode.Rollback;

public IReadOnlyCollection<PlayerHandle> GetPlayers() => addedPlayers;
public IReadOnlyCollection<PlayerHandle> GetSpectators() => addedSpectators;

Expand Down Expand Up @@ -263,12 +266,18 @@ ResultCode AddLocalPlayer(in LocalPlayer player)
player.Handle = handle;
endpoints.Add(null);

Array.Resize(ref syncInputBuffer, syncInputBuffer.Length + 1);
IncrementInputBufferSize();
synchronizer.AddQueue(player.Handle);

return ResultCode.Ok;
}

void IncrementInputBufferSize()
{
Array.Resize(ref syncInputBuffer, syncInputBuffer.Length + 1);
Array.Resize(ref inputBuffer, syncInputBuffer.Length);
}

ResultCode AddRemotePlayer(RemotePlayer player)
{
if (addedPlayers.Count >= Max.NumberOfPlayers)
Expand All @@ -280,13 +289,13 @@ ResultCode AddRemotePlayer(RemotePlayer player)

var endpoint = player.EndPoint;
var protocol = peerConnectionFactory.Create(
new(player.Handle, endpoint, localConnections, magicNumber),
new(player.Handle, endpoint, localConnections, syncNumber),
inputSerializer, peerInputEventQueue
);

peerObservers.Add(protocol.GetUdpObserver());
endpoints.Add(protocol);
Array.Resize(ref syncInputBuffer, syncInputBuffer.Length + 1);
IncrementInputBufferSize();
synchronizer.AddQueue(player.Handle);
logger.Write(LogLevel.Information, $"Adding {player.Handle} at {endpoint}");
protocol.Synchronize();
Expand Down Expand Up @@ -318,7 +327,7 @@ ResultCode AddSpectator(Spectator spectator)

spectator.Handle = spectatorHandle;
var protocol = peerConnectionFactory.Create(
new(spectatorHandle, spectator.EndPoint, localConnections, magicNumber),
new(spectatorHandle, spectator.EndPoint, localConnections, syncNumber),
inputGroupSerializer, peerCombinedInputsEventPublisher
);
peerObservers.Add(protocol.GetUdpObserver());
Expand Down Expand Up @@ -360,8 +369,10 @@ public ResultCode SynchronizeInputs()
{
if (isSynchronizing)
return ResultCode.NotSynchronized;
synchronizer.SynchronizeInputs(syncInputBuffer);
deterministicRandom.UpdateSeed(CurrentFrame.Number);
synchronizer.SynchronizeInputs(syncInputBuffer, inputBuffer);

var inputPopCount = options.UseInputSeedForRandom ? Mem.PopCount<TInput>(inputBuffer.AsSpan()) : 0;
deterministicRandom.UpdateSeed(CurrentFrame.Number, inputPopCount);

return ResultCode.Ok;
}
Expand Down
Loading

0 comments on commit a75794d

Please sign in to comment.