A lightweight, transport-agnostic networking library for C# games with support for:
- LAN/Local connections via LiteNetLib
- Steam P2P via Facepunch.Steamworks
- Room/Lobby abstraction for multiplayer game sessions
- Fluent builder API for easy setup
- Logging integration (Microsoft.Extensions.Logging)
- Connection statistics (latency, bytes sent/received)
- .NET 8.0+ (supports net8.0, net9.0, net10.0)
- For Steam support: Steam client must be running
- For Godot: Godot 4.3+ with .NET support
NuGet:
# Core library
dotnet add package PeersAck
# For Godot 4 integration
dotnet add package PeersAck.GodotFrom source:
dotnet restore
dotnet buildPeersAck provides first-class Godot 4 support with Node-based wrappers and signals.
using Godot;
using PeersAck.Godot;
public partial class Game : Node
{
[Export] public NetworkManagerNode Network { get; set; }
public override void _Ready()
{
Network.PeerConnected += OnPeerConnected;
Network.DataReceived += OnDataReceived;
Network.StartServer(7777);
}
private void OnPeerConnected(long connectionId)
{
GD.Print($"Peer connected: {connectionId}");
}
private void OnDataReceived(long connectionId, byte[] data)
{
GD.Print($"Received {data.Length} bytes");
}
}extends Node
@onready var network = $NetworkManagerNode
func _ready():
network.peer_connected.connect(_on_peer_connected)
network.data_received.connect(_on_data_received)
network.start_server(7777)
func _on_peer_connected(connection_id: int):
print("Peer connected: ", connection_id)
func _on_data_received(connection_id: int, data: PackedByteArray):
print("Received ", data.size(), " bytes")NetworkManagerNode- Low-level transport wrapper with auto-pollingRoomManagerNode- High-level room/lobby system wrapper
See PeersAck.Godot/README.md for full documentation.
using PeersAck;
using PeersAck.Rooms;
using Microsoft.Extensions.Logging;
// Server with fluent configuration
var (transport, roomServer) = new PeersAckBuilder()
.UseLanTransport()
.WithLogging(loggerFactory) // Optional: add logging
.WithConnectionKey("MyGame_v1")
.WithMaxConnections(16)
.WithMaxPlayersPerRoom(4)
.BuildServer();
await transport.StartServerAsync(7777);
// Run game loop
await transport.RunLoopAsync(tickRate: 60, onTick: () =>
{
// Your game logic here
});// Client with fluent configuration
var (transport, roomClient) = new PeersAckBuilder()
.UseLanTransport()
.WithConnectionKey("MyGame_v1")
.WithOperationTimeout(TimeSpan.FromSeconds(10))
.BuildClient();
await roomClient.ConnectAsync("127.0.0.1", 7777);
await roomClient.JoinRoomAsync("game-room");using PeersAck.Transport;
using PeersAck.Rooms;
await using var transport = new LanTransport();
using var roomServer = new RoomServer(transport);
roomServer.OnPlayerJoinedRoom += (roomId, playerId) =>
Console.WriteLine($"Player {playerId} joined {roomId}");
await transport.StartServerAsync(7777);
while (true)
{
transport.Poll();
await Task.Delay(16);
}await using var transport = new LanTransport();
using var roomClient = new RoomClient(transport);
await roomClient.ConnectAsync("127.0.0.1", 7777);
await roomClient.JoinRoomAsync("game-room");
roomClient.SendToRoom("game-room", myDataBytes, DeliveryMode.Reliable);
while (roomClient.IsConnected)
{
transport.Poll();
await Task.Delay(16);
}using PeersAck.Steam;
using Steamworks;
SteamClient.Init(480); // Your App ID
// Create lobby for matchmaking
using var lobbyManager = new SteamLobbyManager();
var lobby = await lobbyManager.CreateLobbyAsync(maxPlayers: 4);
lobbyManager.SetLobbyData("game", "MyGame");
// Create P2P server
var (transport, roomServer) = new PeersAckBuilder()
.UseSteamTransport()
.BuildServer();
await transport.StartServerAsync();
while (true)
{
transport.Poll();
await Task.Delay(16);
}
SteamClient.Shutdown();SteamClient.Init(480);
using var lobbyManager = new SteamLobbyManager();
var lobbies = await lobbyManager.FindLobbiesAsync(
new Dictionary<string, string> { ["game"] = "MyGame" });
await lobbyManager.JoinLobbyAsync(lobbies[0]);
var hostId = lobbyManager.GetHostSteamId()!.Value;
var (transport, roomClient) = new PeersAckBuilder()
.UseSteamTransport()
.BuildClient();
await ((SteamTransport)transport).ConnectAsync(hostId);
await roomClient.JoinRoomAsync("main");
SteamClient.Shutdown();PeersAck integrates with Microsoft.Extensions.Logging:
using Microsoft.Extensions.Logging;
// Create a logger factory (e.g., from DI container or manually)
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Debug);
});
// Pass to builder
var (transport, server) = new PeersAckBuilder()
.UseLanTransport()
.WithLogging(loggerFactory)
.BuildServer();Or pass a logger directly:
var logger = loggerFactory.CreateLogger<LanTransport>();
var transport = new LanTransport(logger);Monitor connection health and bandwidth:
var transport = new LanTransport();
// Get stats for a specific connection
var stats = transport.GetConnectionStats(connectionId);
if (stats != null)
{
Console.WriteLine($"Latency: {stats.Latency}ms");
Console.WriteLine($"Bytes sent: {stats.BytesSent}");
Console.WriteLine($"Bytes received: {stats.BytesReceived}");
Console.WriteLine($"Packets sent: {stats.PacketsSent}");
Console.WriteLine($"Connected for: {stats.ConnectionDuration}");
}
// Get aggregate transport stats
var transportStats = transport.Stats;
Console.WriteLine($"Active connections: {transportStats.ActiveConnections}");
Console.WriteLine($"Total bytes sent: {transportStats.TotalBytesSent}");┌─────────────────────────────────────────────────────┐
│ Your Game │
├─────────────────────────────────────────────────────┤
│ PeersAckBuilder │
│ (Fluent configuration) │
├─────────────────────────────────────────────────────┤
│ RoomServer / RoomClient │
│ (Room management, player tracking) │
├─────────────────────────────────────────────────────┤
│ INetworkTransport │
├──────────────────────┬──────────────────────────────┤
│ LanTransport │ SteamTransport │
│ (LiteNetLib) │ (Facepunch.Steamworks) │
└──────────────────────┴──────────────────────────────┘
| Type | Description |
|---|---|
INetworkTransport |
Core transport interface |
LanTransport |
LAN/local network transport |
SteamTransport |
Steam P2P transport |
ConnectionId |
Unique connection identifier |
DeliveryMode |
Reliable or Unreliable |
IConnectionStats |
Connection statistics interface |
TransportStats |
Aggregate transport statistics |
| Type | Description |
|---|---|
RoomServer |
Server-side room management |
RoomClient |
Client-side room handling |
RoomId |
Room identifier (string-based) |
PlayerId |
Player identifier |
| Type | Description |
|---|---|
PeersAckBuilder |
Fluent builder for configuration |
TransportType |
Enum: Lan or Steam |
| Type | Description |
|---|---|
SteamLobbyManager |
Steam lobby create/join/search |
new PeersAckBuilder()
// Transport selection
.UseLanTransport() // Use LiteNetLib (default)
.UseSteamTransport() // Use Steam P2P
// Logging
.WithLogging(loggerFactory) // Add logging support
// Connection settings
.WithConnectionKey("key") // LAN connection key
.WithMaxConnections(32) // Max server connections
.WithDisconnectTimeout(5000) // Timeout in ms
.WithDisconnectTimeout(TimeSpan.FromSeconds(5))
// Room settings
.WithAutoCreateRooms(true) // Auto-create rooms on join
.WithMaxPlayersPerRoom(8) // Max players per room
// Client settings
.WithOperationTimeout(10000) // Async operation timeout
// Build
.BuildTransport() // Just the transport
.BuildServer() // (transport, roomServer)
.BuildClient() // (transport, roomClient)- Reliable: Guaranteed delivery, ordered. Use for important events (chat, state changes).
- Unreliable: Fast, no guarantees. Use for frequently-updated data (positions, inputs).
- Implement
INetworkTransport - Map your transport's connection IDs to
ConnectionId - Raise events:
OnPeerConnected,OnPeerDisconnected,OnDataReceived
public class MyTransport : INetworkTransport
{
// Implement all interface members...
}Extend RoomServer/RoomClient or create wrapper classes:
public class GameRoomServer : RoomServer
{
public Dictionary<RoomId, GameState> RoomStates { get; } = new();
// Add game-specific room logic...
}Extend RoomProtocol with new message types:
public static class GameProtocol
{
public const byte SpawnPlayer = 0x20;
public const byte UpdatePosition = 0x21;
public static byte[] CreateSpawnPlayer(PlayerId id, Vector3 position) { ... }
}The library is designed to be called from a single thread (your game loop). Call Poll() regularly to process network events.
dotnet test PeersAck.Tests/PeersAck.Tests.csprojMIT