DoubleDice relay game SDK for GameService - client-authoritative multiplayer game
$ dotnet add package GameService.Sdk.LudoDoublesThe DoubleDice SDK is a relay client for client-authoritative games. Game logic runs entirely on the Unity client — the server simply broadcasts data between all players in a room.
GameService.Sdk.CoreGameService.Sdk.DoubleDiceprotobuf-netAssets/Scripts/Sdk/using GameService.Sdk.Core;
using GameService.Sdk.LudoDoubles;
public class DoubleDiceManager : MonoBehaviour
{
private GameClient gameClient;
private LudoClient ludoClient;
private async void Start()
{
// Connect to GameService
gameClient = await GameClient.Create("https://your-server.com")
.WithAccessToken(accessToken)
.WithSynchronizationContext(true) // Ensures callbacks run on Unity main thread
.ConnectAsync();
// Create the DoubleDice client wrapper
ludoClient = new LudoClient(gameClient);
// Listen for data from other players
ludoClient.OnDataReceived += OnDataReceived;
}
private void OnDataReceived(string actionName, byte[] data)
{
// Handle incoming game data from other players
Debug.Log($"Received '{actionName}' ({data.Length} bytes)");
}
}
// Create a room (uses a server-side template name)
var createResult = await ludoClient.CreateRoomAsync("DoubleDice");
if (createResult.Success)
Debug.Log($"Room: {createResult.RoomId}, Code: {createResult.ShortCode}");
// Join an existing room by short code or room ID
var joinResult = await ludoClient.JoinRoomAsync("12345");
if (joinResult.Success)
Debug.Log($"Joined at seat {joinResult.SeatIndex}");
// Matchmaking: auto-join an available room or create a new one
var matchResult = await ludoClient.CreateOrJoinRoomAsync("DoubleDice");
// Leave the room
await ludoClient.LeaveRoomAsync();
Use SendData to broadcast any data to all players in the room.
// Send raw byte array
byte[] myGameState = SerializeMyState();
var result = await ludoClient.SendData("Sync", myGameState);
if (result.Success)
Debug.Log("Data sent to all players!");
Define your game messages with protobuf-net:
using ProtoBuf;
[ProtoContract]
public class DiceRollMessage
{
[ProtoMember(1)] public int Dice1 { get; set; }
[ProtoMember(2)] public int Dice2 { get; set; }
[ProtoMember(3)] public string PlayerId { get; set; }
}
// Send typed data (auto-serialized via protobuf)
var roll = new DiceRollMessage { Dice1 = 4, Dice2 = 6, PlayerId = "player1" };
await ludoClient.SendData("Roll", roll);
All incoming data arrives via the OnDataReceived event:
ludoClient.OnDataReceived += (actionName, data) =>
{
switch (actionName)
{
case "Roll":
var roll = Deserialize<DiceRollMessage>(data);
Debug.Log($"Player {roll.PlayerId} rolled {roll.Dice1} + {roll.Dice2}");
break;
case "Sync":
ApplyGameState(data);
break;
case "Move":
var move = Deserialize<MoveMessage>(data);
AnimateToken(move);
break;
}
};
// Helper to deserialize protobuf messages
T Deserialize<T>(byte[] data)
{
using var ms = new MemoryStream(data);
return ProtoBuf.Serializer.Deserialize<T>(ms);
}
Player join/leave events come from the underlying GameClient:
gameClient.OnPlayerJoined += async (player) =>
{
Debug.Log($"{player.UserName} joined at seat {player.SeatIndex}");
};
gameClient.OnPlayerLeft += async (player) =>
{
Debug.Log($"{player.UserName} left the game");
};
gameClient.OnPlayerDisconnected += async (info) =>
{
Debug.Log($"{info.UserName} disconnected (grace: {info.GracePeriodSeconds}s)");
};
gameClient.OnPlayerReconnected += async (info) =>
{
Debug.Log($"{info.UserName} reconnected!");
};
| Method | Description |
|---|---|
SendData(string action, byte[] data) | Broadcast raw bytes to all room players |
SendData<T>(string action, T payload) | Broadcast a Protobuf object to all room players |
CreateRoomAsync(string template) | Create a new room |
JoinRoomAsync(string roomIdOrCode) | Join by room ID or 5-digit short code |
CreateOrJoinRoomAsync(string template) | Matchmaking: join available or create new |
LeaveRoomAsync() | Leave the current room |
| Event | Signature | Description |
|---|---|---|
OnDataReceived | Action<string, byte[]> | Fired when data is received. Args: (actionName, data) |
Unity Client A Server Unity Client B
│ │ │
│ SendData("Roll", bytes) ──► │ │
│ │ ── GameEvent("Roll") ──────► │
│ │ (broadcast to room) │
│ ◄── OnDataReceived ──────── │ │
.WithSynchronizationContext(true) to receive callbacks on Unity's main threadactionName string is arbitrary — define whatever actions your game needs ("Roll", "Move", "Sync", "Chat", etc.)OnDataReceived callbackLeaveRoomAsync() or gameClient.DisposeAsync() when done