DotBoxD Unity Integration Guide
A comprehensive guide to integrating DotBoxD into your Unity project for type-safe, high-performance client-server communication.
Table of Contents
- Overview
- Prerequisites
- Project Setup
- Defining Service Contracts
- Server Implementation
- Unity Client Setup
- Connection Management
- Error Handling
- IL2CPP and AOT Considerations
- Best Practices
- Troubleshooting
- Advanced Topics
Overview
DotBoxD is a transport-agnostic RPC framework designed with Unity compatibility as a primary goal. It uses compile-time source generation to create type-safe proxies and dispatchers, avoiding runtime reflection that causes issues with IL2CPP builds.
Why this works on Unity/IL2CPP: the three libraries you drop into Assets/Plugins — Services (RPC), the channel/transport, and the MessagePack codec — all target netstandard2.1 and keep runtime reflection off the hot path. One C# contract compiles to a typed proxy plus dispatcher (no hand-written marshaling), so nothing on the call path needs the dynamic codegen IL2CPP's AOT compiler rejects. Reach for this stack whenever you need type-safe client/server calls that must survive AOT; on Mono it works the same, but the netstandard2.1 / no-reflection design is what makes the IL2CPP target hold.
Key Features for Unity
- IL2CPP Safe: No runtime reflection or dynamic code generation
- Source Generators: Compile-time proxy generation
- Shared Contracts: Same C# interfaces on client and server
- MessagePack: Fast binary serialization with Unity support
- Transport Agnostic: TCP, WebSocket, or custom transports
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Shared Library │
│ [DotBoxDService] IGameService + Models (PlayerId, PlayerState) │
└─────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ Unity Client │ │ .NET Server │
│ ┌───────────────────────┐ │ │ ┌───────────────────────┐ │
│ │ GameServiceProxy (gen)│ │ │ │GameServiceDispatcher │ │
│ └───────────────────────┘ │ │ │ (generated) │ │
│ ┌───────────────────────┐ │ │ └───────────────────────┘ │
│ │ RpcPeer │ │ │ ┌───────────────────────┐ │
│ └───────────────────────┘ │ │ │ RpcHost │ │
│ ┌───────────────────────┐ │ │ └───────────────────────┘ │
│ │ TcpTransport │◄─┼───┼─►│ TcpServerTransport │ │
│ └───────────────────────┘ │ │ └───────────────────────┘ │
└─────────────────────────────┘ └─────────────────────────────┘
Prerequisites
Unity Requirements
- Unity 2021.3 LTS or newer (for .NET Standard 2.1 support)
- Scripting Backend: Mono or IL2CPP
- API Compatibility Level: .NET Standard 2.1
Server Requirements
- .NET 6.0 or newer (recommended: .NET 8.0+)
NuGet Packages
MessagePack(2.5.x or newer)
Project Setup
Step 1: Solution Structure
Create a solution with shared contracts accessible to both Unity and server:
YourGame/
├── src/
│ ├── YourGame.Shared/ # Shared contracts (netstandard2.1)
│ │ ├── IGameService.cs
│ │ └── Models.cs
│ └── YourGame.Server/ # Server (net8.0)
│ ├── GameService.cs
│ └── Program.cs
├── unity/
│ └── YourGameClient/ # Unity project
│ └── Assets/
│ ├── Plugins/
│ │ ├── DotBoxD.Services.dll
│ │ ├── DotBoxD.Transports.Tcp.dll
│ │ ├── DotBoxD.Codecs.MessagePack.dll
│ │ ├── YourGame.Shared.dll
│ │ └── MessagePack.dll
│ └── Scripts/
│ └── Networking/
└── YourGame.sln
Step 2: Shared Library Project
Create YourGame.Shared.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MessagePack" Version="2.5.187" />
</ItemGroup>
<!-- Reference source generator for proxy/dispatcher generation -->
<ItemGroup>
<ProjectReference Include="..\DotBoxD.Services\DotBoxD.Services.csproj" />
<ProjectReference Include="..\DotBoxD.Services.SourceGenerator\DotBoxD.Services.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>
Step 3: Build DLLs for Unity
Create a build script to copy DLLs to Unity:
#!/bin/bash
# build-for-unity.sh
UNITY_PLUGINS="unity/YourGameClient/Assets/Plugins"
dotnet build src/YourGame.Shared/YourGame.Shared.csproj -c Release
# Copy DotBoxD libraries
cp src/DotBoxD.Services/bin/Release/netstandard2.1/DotBoxD.Services.dll "$UNITY_PLUGINS/"
cp src/DotBoxD.Transports.Tcp/bin/Release/netstandard2.1/DotBoxD.Transports.Tcp.dll "$UNITY_PLUGINS/"
cp src/DotBoxD.Codecs.MessagePack/bin/Release/netstandard2.1/DotBoxD.Codecs.MessagePack.dll "$UNITY_PLUGINS/"
# Copy shared library
cp src/YourGame.Shared/bin/Release/netstandard2.1/YourGame.Shared.dll "$UNITY_PLUGINS/"
# Copy MessagePack (get from NuGet cache)
cp ~/.nuget/packages/messagepack/2.5.187/lib/netstandard2.0/MessagePack.dll "$UNITY_PLUGINS/"
echo "DLLs copied to Unity"
Defining Service Contracts
Basic Service Interface
// YourGame.Shared/IGameService.cs
using DotBoxD.Services.Attributes;
namespace YourGame.Shared;
[DotBoxDService]
public interface IGameService
{
// Simple request/response
Task<ServerInfo> GetServerInfoAsync(CancellationToken ct = default);
// With parameters
Task<PlayerState> GetPlayerAsync(string playerId, CancellationToken ct = default);
// With complex request object
Task<JoinResult> JoinGameAsync(JoinRequest request, CancellationToken ct = default);
// Fire-and-forget style (still awaitable)
Task SendInputAsync(PlayerInput input, CancellationToken ct = default);
}
Model Classes
Models must be serializable by MessagePack. Use attributes for best compatibility:
// YourGame.Shared/Models.cs
using MessagePack;
namespace YourGame.Shared;
[MessagePackObject]
public class ServerInfo
{
[Key(0)] public string ServerName { get; set; } = "";
[Key(1)] public int PlayerCount { get; set; }
[Key(2)] public int MaxPlayers { get; set; }
[Key(3)] public string Version { get; set; } = "";
}
[MessagePackObject]
public class PlayerState
{
[Key(0)] public string PlayerId { get; set; } = "";
[Key(1)] public string DisplayName { get; set; } = "";
[Key(2)] public float PositionX { get; set; }
[Key(3)] public float PositionY { get; set; }
[Key(4)] public float PositionZ { get; set; }
[Key(5)] public float RotationY { get; set; }
[Key(6)] public int Health { get; set; }
[Key(7)] public int MaxHealth { get; set; }
}
[MessagePackObject]
public class JoinRequest
{
[Key(0)] public string DisplayName { get; set; } = "";
[Key(1)] public string AuthToken { get; set; } = "";
}
[MessagePackObject]
public class JoinResult
{
[Key(0)] public bool Success { get; set; }
[Key(1)] public string? PlayerId { get; set; }
[Key(2)] public string? ErrorMessage { get; set; }
[Key(3)] public PlayerState? InitialState { get; set; }
}
[MessagePackObject]
public class PlayerInput
{
[Key(0)] public float MoveX { get; set; }
[Key(1)] public float MoveY { get; set; }
[Key(2)] public bool Jump { get; set; }
[Key(3)] public bool Fire { get; set; }
[Key(4)] public float AimYaw { get; set; }
[Key(5)] public float AimPitch { get; set; }
[Key(6)] public long Timestamp { get; set; }
}
Custom Method Names (Optional)
[DotBoxDService(Name = "Game")]
public interface IGameService
{
[DotBoxDMethod(Name = "Info")]
Task<ServerInfo> GetServerInfoAsync(CancellationToken ct = default);
}
Server Implementation
Service Implementation
// YourGame.Server/GameService.cs
using YourGame.Shared;
namespace YourGame.Server;
public class GameService : IGameService
{
private readonly GameState _gameState;
public GameService(GameState gameState)
{
_gameState = gameState;
}
public Task<ServerInfo> GetServerInfoAsync(CancellationToken ct = default)
{
return Task.FromResult(new ServerInfo
{
ServerName = _gameState.ServerName,
PlayerCount = _gameState.Players.Count,
MaxPlayers = _gameState.MaxPlayers,
Version = "1.0.0"
});
}
public Task<PlayerState> GetPlayerAsync(string playerId, CancellationToken ct = default)
{
if (!_gameState.Players.TryGetValue(playerId, out var player))
{
throw new KeyNotFoundException($"Player '{playerId}' not found");
}
return Task.FromResult(player.ToPlayerState());
}
public Task<JoinResult> JoinGameAsync(JoinRequest request, CancellationToken ct = default)
{
// Validate auth token
if (!ValidateToken(request.AuthToken))
{
return Task.FromResult(new JoinResult
{
Success = false,
ErrorMessage = "Invalid authentication"
});
}
// Create player
var player = _gameState.CreatePlayer(request.DisplayName);
return Task.FromResult(new JoinResult
{
Success = true,
PlayerId = player.Id,
InitialState = player.ToPlayerState()
});
}
public Task SendInputAsync(PlayerInput input, CancellationToken ct = default)
{
_gameState.ProcessInput(input);
return Task.CompletedTask;
}
private bool ValidateToken(string token) => !string.IsNullOrEmpty(token);
}
Server Startup
// YourGame.Server/Program.cs
using YourGame.Server;
using YourGame.Shared;
using DotBoxD.Services;
using DotBoxD.Services.Generated;
using DotBoxD.Codecs.MessagePack;
using DotBoxD.Transports.Tcp;
const int Port = 7777;
Console.WriteLine($"Starting game server on port {Port}...");
var gameState = new GameState();
var gameService = new GameService(gameState);
// RpcHost listens and turns every accepted connection into a peer. ForEachPeer
// runs once per connection to provide the services that peer exposes.
await using var host = RpcHost
.Listen(new TcpServerTransport(Port), new MessagePackRpcSerializer())
.ForEachPeer(peer => peer.ProvideGameService(gameService)); // Generated extension method
await host.StartAsync();
Console.WriteLine("Server started. Press Ctrl+C to stop.");
var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
try
{
await Task.Delay(Timeout.Infinite, cts.Token);
}
catch (OperationCanceledException) { }
await host.StopAsync();
Console.WriteLine("Server stopped.");
Unity Client Setup
NetworkManager Component
// Assets/Scripts/Networking/NetworkManager.cs
using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using DotBoxD.Services;
using DotBoxD.Services.Generated;
using DotBoxD.Codecs.MessagePack;
using DotBoxD.Transports.Tcp;
using YourGame.Shared;
public class NetworkManager : MonoBehaviour
{
public static NetworkManager Instance { get; private set; }
[Header("Connection Settings")]
[SerializeField] private string serverHost = "localhost";
[SerializeField] private int serverPort = 7777;
[SerializeField] private float connectionTimeout = 10f;
public IGameService GameService { get; private set; }
public bool IsConnected => _peer?.IsConnected ?? false;
public event Action OnConnected;
public event Action OnDisconnected;
public event Action<string> OnConnectionError;
private RpcPeer _peer;
private TcpTransport _transport;
private CancellationTokenSource _cts;
private void Awake()
{
if (Instance != null)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
private void OnDestroy()
{
Disconnect();
}
public async Task<bool> ConnectAsync(string host = null, int? port = null)
{
if (IsConnected)
{
Debug.LogWarning("Already connected");
return true;
}
host ??= serverHost;
port ??= serverPort;
_cts = new CancellationTokenSource();
try
{
Debug.Log($"Connecting to {host}:{port}...");
_transport = new TcpTransport(host, port.Value);
var serializer = new MessagePackRpcSerializer();
using var connectCts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token);
connectCts.CancelAfter(TimeSpan.FromSeconds(connectionTimeout));
// Establish the underlying connection, then layer a peer over it.
await _transport.ConnectAsync(connectCts.Token);
_peer = RpcPeer
.Over(_transport.Connection!, serializer, new RpcPeerOptions
{
RequestTimeout = TimeSpan.FromSeconds(connectionTimeout),
// This client only calls out; it never serves inbound calls.
RejectInboundCalls = true
})
.Start();
// Create the service proxy
GameService = _peer.GetGameService();
Debug.Log("Connected to server!");
OnConnected?.Invoke();
return true;
}
catch (OperationCanceledException)
{
Debug.LogWarning("Connection cancelled");
return false;
}
catch (Exception ex)
{
Debug.LogError($"Connection failed: {ex.Message}");
OnConnectionError?.Invoke(ex.Message);
return false;
}
}
public void Disconnect()
{
if (_peer == null) return;
_cts?.Cancel();
try
{
// Disposing the peer stops its read loop; dispose the transport to close the socket.
_peer.DisposeAsync().AsTask().Wait(TimeSpan.FromSeconds(2));
_transport?.DisposeAsync().AsTask().Wait(TimeSpan.FromSeconds(2));
}
catch { }
_peer = null;
_transport = null;
GameService = null;
_cts?.Dispose();
_cts = null;
Debug.Log("Disconnected from server");
OnDisconnected?.Invoke();
}
// Helper for fire-and-forget calls with error logging
public async void SendAsync(Func<Task> action)
{
if (!IsConnected)
{
Debug.LogWarning("Not connected");
return;
}
try
{
await action();
}
catch (Exception ex)
{
Debug.LogError($"RPC call failed: {ex.Message}");
}
}
}
Usage in Game Scripts
// Assets/Scripts/Game/GameClient.cs
using System.Threading.Tasks;
using UnityEngine;
using YourGame.Shared;
public class GameClient : MonoBehaviour
{
[SerializeField] private string playerName = "Player";
private string _playerId;
private async void Start()
{
var network = NetworkManager.Instance;
// Connect to server
if (!await network.ConnectAsync())
{
Debug.LogError("Failed to connect!");
return;
}
// Get server info
var serverInfo = await network.GameService.GetServerInfoAsync();
Debug.Log($"Connected to '{serverInfo.ServerName}' ({serverInfo.PlayerCount}/{serverInfo.MaxPlayers})");
// Join the game
var joinResult = await network.GameService.JoinGameAsync(new JoinRequest
{
DisplayName = playerName,
AuthToken = GetAuthToken()
});
if (!joinResult.Success)
{
Debug.LogError($"Failed to join: {joinResult.ErrorMessage}");
return;
}
_playerId = joinResult.PlayerId;
Debug.Log($"Joined as {joinResult.InitialState.DisplayName} (ID: {_playerId})");
// Initialize player position
transform.position = new Vector3(
joinResult.InitialState.PositionX,
joinResult.InitialState.PositionY,
joinResult.InitialState.PositionZ
);
}
private void Update()
{
if (string.IsNullOrEmpty(_playerId)) return;
// Send input to server
var input = new PlayerInput
{
MoveX = Input.GetAxis("Horizontal"),
MoveY = Input.GetAxis("Vertical"),
Jump = Input.GetButtonDown("Jump"),
Fire = Input.GetButton("Fire1"),
AimYaw = transform.eulerAngles.y,
AimPitch = Camera.main.transform.eulerAngles.x,
Timestamp = System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
};
// Fire-and-forget with error handling
NetworkManager.Instance.SendAsync(() =>
NetworkManager.Instance.GameService.SendInputAsync(input));
}
private string GetAuthToken()
{
// Implement your authentication logic
return "demo-token";
}
}
UI Integration
// Assets/Scripts/UI/ConnectionUI.cs
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class ConnectionUI : MonoBehaviour
{
[SerializeField] private TMP_InputField hostInput;
[SerializeField] private TMP_InputField portInput;
[SerializeField] private Button connectButton;
[SerializeField] private Button disconnectButton;
[SerializeField] private TMP_Text statusText;
private void Start()
{
var network = NetworkManager.Instance;
network.OnConnected += () => UpdateUI(true);
network.OnDisconnected += () => UpdateUI(false);
network.OnConnectionError += error => statusText.text = $"Error: {error}";
connectButton.onClick.AddListener(OnConnectClicked);
disconnectButton.onClick.AddListener(OnDisconnectClicked);
UpdateUI(false);
}
private async void OnConnectClicked()
{
connectButton.interactable = false;
statusText.text = "Connecting...";
var host = string.IsNullOrEmpty(hostInput.text) ? "localhost" : hostInput.text;
var port = int.TryParse(portInput.text, out var p) ? p : 7777;
var success = await NetworkManager.Instance.ConnectAsync(host, port);
if (success)
{
statusText.text = "Connected!";
}
else
{
connectButton.interactable = true;
}
}
private void OnDisconnectClicked()
{
NetworkManager.Instance.Disconnect();
}
private void UpdateUI(bool connected)
{
connectButton.gameObject.SetActive(!connected);
disconnectButton.gameObject.SetActive(connected);
hostInput.interactable = !connected;
portInput.interactable = !connected;
if (!connected)
{
statusText.text = "Disconnected";
connectButton.interactable = true;
}
}
}
Connection Management
Automatic Reconnection
// Assets/Scripts/Networking/ReconnectionHandler.cs
using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class ReconnectionHandler : MonoBehaviour
{
[SerializeField] private float reconnectDelay = 2f;
[SerializeField] private float maxReconnectDelay = 30f;
[SerializeField] private int maxAttempts = 10;
private CancellationTokenSource _cts;
private bool _shouldReconnect = true;
private void Start()
{
NetworkManager.Instance.OnDisconnected += OnDisconnected;
}
private void OnDestroy()
{
_cts?.Cancel();
_cts?.Dispose();
}
private async void OnDisconnected()
{
if (!_shouldReconnect) return;
_cts?.Cancel();
_cts = new CancellationTokenSource();
await AttemptReconnection(_cts.Token);
}
private async Task AttemptReconnection(CancellationToken ct)
{
var delay = reconnectDelay;
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
if (ct.IsCancellationRequested) return;
Debug.Log($"Reconnection attempt {attempt}/{maxAttempts}...");
try
{
await Task.Delay(TimeSpan.FromSeconds(delay), ct);
if (await NetworkManager.Instance.ConnectAsync())
{
Debug.Log("Reconnected successfully!");
return;
}
}
catch (OperationCanceledException)
{
return;
}
// Exponential backoff
delay = Math.Min(delay * 1.5f, maxReconnectDelay);
}
Debug.LogError("Failed to reconnect after maximum attempts");
}
public void StopReconnection()
{
_shouldReconnect = false;
_cts?.Cancel();
}
public void EnableReconnection()
{
_shouldReconnect = true;
}
}
Connection Health Monitoring
// Assets/Scripts/Networking/ConnectionHealth.cs
using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class ConnectionHealth : MonoBehaviour
{
[SerializeField] private float pingInterval = 5f;
public float LastPingMs { get; private set; }
public event Action<float> OnPingUpdated;
private CancellationTokenSource _cts;
private void Start()
{
NetworkManager.Instance.OnConnected += StartPinging;
NetworkManager.Instance.OnDisconnected += StopPinging;
}
private void OnDestroy()
{
StopPinging();
}
private async void StartPinging()
{
_cts = new CancellationTokenSource();
while (!_cts.Token.IsCancellationRequested)
{
try
{
await Task.Delay(TimeSpan.FromSeconds(pingInterval), _cts.Token);
var start = DateTime.UtcNow;
await NetworkManager.Instance.GameService.GetServerInfoAsync(_cts.Token);
LastPingMs = (float)(DateTime.UtcNow - start).TotalMilliseconds;
OnPingUpdated?.Invoke(LastPingMs);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
Debug.LogWarning($"Ping failed: {ex.Message}");
}
}
}
private void StopPinging()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
}
}
Error Handling
Exception Types
using DotBoxD.Services.Exceptions;
try
{
var result = await NetworkManager.Instance.GameService.GetPlayerAsync(playerId);
}
catch (ServiceTimeoutException)
{
// Request timed out
Debug.LogWarning("Request timed out - server may be overloaded");
}
catch (RemoteServiceException ex)
{
// Server threw an exception
Debug.LogError($"Server error ({ex.RemoteExceptionType}): {ex.Message}");
}
catch (ServiceConnectionException)
{
// Connection lost
Debug.LogError("Connection lost");
// Trigger reconnection logic
}
catch (ServiceException ex)
{
// Other RPC errors
Debug.LogError($"RPC error: {ex.Message}");
}
Wrapper for Safe Calls
// Assets/Scripts/Networking/RpcHelper.cs
using System;
using System.Threading.Tasks;
using UnityEngine;
using DotBoxD.Services.Exceptions;
public static class RpcHelper
{
public static async Task<T> SafeCallAsync<T>(
Func<Task<T>> call,
T defaultValue = default,
Action<Exception> onError = null)
{
try
{
return await call();
}
catch (ServiceTimeoutException ex)
{
Debug.LogWarning($"RPC timeout: {ex.Message}");
onError?.Invoke(ex);
return defaultValue;
}
catch (RemoteServiceException ex)
{
Debug.LogError($"Server error: {ex.Message}");
onError?.Invoke(ex);
return defaultValue;
}
catch (ServiceConnectionException ex)
{
Debug.LogError($"Connection error: {ex.Message}");
onError?.Invoke(ex);
return defaultValue;
}
catch (Exception ex)
{
Debug.LogError($"Unexpected error: {ex}");
onError?.Invoke(ex);
return defaultValue;
}
}
public static async Task SafeCallAsync(
Func<Task> call,
Action<Exception> onError = null)
{
try
{
await call();
}
catch (Exception ex)
{
Debug.LogError($"RPC error: {ex.Message}");
onError?.Invoke(ex);
}
}
}
// Usage:
var serverInfo = await RpcHelper.SafeCallAsync(
() => NetworkManager.Instance.GameService.GetServerInfoAsync(),
defaultValue: new ServerInfo { ServerName = "Unknown" },
onError: ex => ShowErrorPopup(ex.Message)
);
IL2CPP and AOT Considerations
MessagePack AOT Configuration
For IL2CPP builds, MessagePack needs AOT code generation hints:
// Assets/Scripts/AOT/MessagePackAOTSetup.cs
using MessagePack;
using MessagePack.Resolvers;
using UnityEngine;
public static class MessagePackAOTSetup
{
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Initialize()
{
// Configure MessagePack for AOT
StaticCompositeResolver.Instance.Register(
GeneratedResolver.Instance, // Your generated resolver
StandardResolver.Instance
);
var options = MessagePackSerializerOptions.Standard
.WithResolver(StaticCompositeResolver.Instance)
.WithSecurity(MessagePackSecurity.UntrustedData);
MessagePackSerializer.DefaultOptions = options;
}
}
Generate MessagePack Formatters
Add to your shared project:
// YourGame.Shared/Generated/GeneratedResolver.cs
// This file is auto-generated by MessagePack.Generator
using MessagePack;
using MessagePack.Formatters;
public class GeneratedResolver : IFormatterResolver
{
public static readonly GeneratedResolver Instance = new();
public IMessagePackFormatter<T> GetFormatter<T>()
{
return FormatterCache<T>.Formatter;
}
private static class FormatterCache<T>
{
public static readonly IMessagePackFormatter<T> Formatter;
static FormatterCache()
{
// Formatter lookup logic
}
}
}
Use mpc (MessagePack Compiler) to generate formatters:
dotnet tool install -g MessagePack.Generator
mpc -i src/YourGame.Shared/YourGame.Shared.csproj -o src/YourGame.Shared/Generated
Link.xml for IL2CPP
Create Assets/link.xml to prevent code stripping:
<linker>
<!-- Preserve DotBoxD -->
<assembly fullname="DotBoxD.Services" preserve="all"/>
<assembly fullname="DotBoxD.Transports.Tcp" preserve="all"/>
<assembly fullname="DotBoxD.Codecs.MessagePack" preserve="all"/>
<!-- Preserve your shared library -->
<assembly fullname="YourGame.Shared" preserve="all"/>
<!-- Preserve MessagePack -->
<assembly fullname="MessagePack" preserve="all"/>
<!-- Preserve System.Buffers for ArrayPool -->
<assembly fullname="System.Buffers" preserve="all"/>
</linker>
Best Practices
1. Main Thread Dispatching
RPC callbacks may not be on the main thread. Use a dispatcher:
// Assets/Scripts/Util/MainThreadDispatcher.cs
using System;
using System.Collections.Concurrent;
using UnityEngine;
public class MainThreadDispatcher : MonoBehaviour
{
public static MainThreadDispatcher Instance { get; private set; }
private readonly ConcurrentQueue<Action> _actions = new();
private void Awake()
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
private void Update()
{
while (_actions.TryDequeue(out var action))
{
action?.Invoke();
}
}
public void Enqueue(Action action)
{
_actions.Enqueue(action);
}
public static void RunOnMainThread(Action action)
{
Instance?.Enqueue(action);
}
}
// Usage in async code:
var result = await service.GetPlayerAsync(id);
MainThreadDispatcher.RunOnMainThread(() => {
// Safe to access Unity APIs here
playerObject.transform.position = new Vector3(result.PositionX, ...);
});
2. Request Batching
Batch multiple requests to reduce overhead:
public class RequestBatcher
{
private readonly List<PlayerInput> _pendingInputs = new();
private readonly object _lock = new();
private float _lastSendTime;
private const float SendInterval = 0.05f; // 20 times per second
public void QueueInput(PlayerInput input)
{
lock (_lock)
{
_pendingInputs.Add(input);
}
}
public void Update()
{
if (Time.time - _lastSendTime < SendInterval) return;
PlayerInput[] toSend;
lock (_lock)
{
if (_pendingInputs.Count == 0) return;
toSend = _pendingInputs.ToArray();
_pendingInputs.Clear();
}
// Send batched inputs
NetworkManager.Instance.SendAsync(() =>
NetworkManager.Instance.GameService.SendBatchedInputAsync(toSend));
_lastSendTime = Time.time;
}
}
3. Cancellation Token Usage
Always pass cancellation tokens for proper cleanup:
public class GameClient : MonoBehaviour
{
private CancellationTokenSource _cts;
private void OnEnable()
{
_cts = new CancellationTokenSource();
}
private void OnDisable()
{
_cts?.Cancel();
_cts?.Dispose();
}
private async Task FetchDataAsync()
{
try
{
var data = await NetworkManager.Instance.GameService
.GetPlayerAsync(playerId, _cts.Token);
}
catch (OperationCanceledException)
{
// Component was disabled, ignore
}
}
}
4. Connection State Checks
Always verify connection before making calls:
public async Task<bool> TrySendInput(PlayerInput input)
{
if (!NetworkManager.Instance.IsConnected)
{
Debug.LogWarning("Cannot send input: not connected");
return false;
}
try
{
await NetworkManager.Instance.GameService.SendInputAsync(input);
return true;
}
catch
{
return false;
}
}
Troubleshooting
Common Issues
"TypeLoadException" or "MissingMethodException" in IL2CPP
Cause: Code stripping removed required types.
Solution:
- Add types to
link.xml - Ensure MessagePack AOT generation is set up
- Use
[Preserve]attribute on model classes
"Connection refused" error
Cause: Server not running or wrong port/host.
Solution:
- Verify server is running:
netstat -an | grep <port> - Check firewall settings
- Use correct IP (not
localhostfor device builds)
Timeout on all requests
Cause: Serialization mismatch or protocol error.
Solution:
- Ensure same MessagePack version on client and server
- Rebuild shared library and update Unity DLLs
- Check for model property mismatches
"Operation cancelled" immediately
Cause: CancellationToken cancelled too early.
Solution:
- Don't pass disposed/cancelled tokens
- Check component lifecycle (OnDisable cancelling tokens)
High latency / slow responses
Cause: Network issues or server overload.
Solution:
- Use connection health monitoring
- Implement request timeouts
- Check for blocking operations on server
Debug Logging
Enable detailed logging:
// Add to NetworkManager
#if UNITY_EDITOR || DEVELOPMENT_BUILD
private void LogRpcCall(string method, object request = null)
{
Debug.Log($"[RPC] {method}" + (request != null ? $": {JsonUtility.ToJson(request)}" : ""));
}
#endif
Advanced Topics
Custom Transport
Implement your own transport for platforms like Steam or Epic:
public class SteamTransport : ITransport
{
private CSteamID _serverId;
private SteamConnection _connection;
public IRpcChannel Connection => _connection;
public bool IsConnected => _connection?.IsConnected ?? false;
public async Task ConnectAsync(CancellationToken ct = default)
{
// Implement Steam networking connection
var result = await SteamNetworking.ConnectP2P(_serverId);
_connection = new SteamConnection(result);
}
public ValueTask DisposeAsync()
{
_connection?.Dispose();
return default;
}
}
Multiple Services
Register multiple services on the same server:
// Shared
[DotBoxDService] public interface IGameService { ... }
[DotBoxDService] public interface IChatService { ... }
[DotBoxDService] public interface IInventoryService { ... }
// Server: provide every service on each accepted peer
await using var host = RpcHost
.Listen(serverTransport, serializer)
.ForEachPeer(peer =>
{
peer.ProvideGameService(gameService);
peer.ProvideChatService(chatService);
peer.ProvideInventoryService(inventoryService);
});
// Client: get a proxy per service from the same peer
var game = peer.GetGameService();
var chat = peer.GetChatService();
var inventory = peer.GetInventoryService();
Server-to-Client Notifications (Future)
While DotBoxD currently supports request/response patterns, you can implement push notifications using a polling pattern or extend the protocol:
// Polling approach
public class NotificationPoller : MonoBehaviour
{
private async void Start()
{
while (NetworkManager.Instance.IsConnected)
{
var notifications = await NetworkManager.Instance.GameService
.PollNotificationsAsync(_lastNotificationId);
foreach (var notification in notifications)
{
ProcessNotification(notification);
_lastNotificationId = notification.Id;
}
await Task.Delay(100); // Poll every 100ms
}
}
}
Version Compatibility
| DotBoxD Version | Unity Version | .NET Server | MessagePack |
|---|---|---|---|
| 1.0.x | 2021.3+ | 6.0+ | 2.5.x |