Networking
Multiplayer & Networking
Adding multiplayer to a MonoGame project ranges from simple split-screen to full client-server networking. This guide covers the concepts, patterns, and code to get started at each level.
Download the runnable sample project
Use the verified ZIP bundle for this article to review the extracted example files, run the sample host, and repackage the sources without bin or obj folders.
Local multiplayer
The simplest multiplayer approach uses multiple gamepads or keyboard zones on a single machine. MonoGame supports up to four gamepads via PlayerIndex.
private Vector2[] _playerPositions = new Vector2[4];
private float _speed = 200f;
protected override void Update(GameTime gameTime)
{
float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;
for (int i = 0; i < 4; i++)
{
GamePadState pad = GamePad.GetState((PlayerIndex)i);
if (!pad.IsConnected) continue;
_playerPositions[i].X += pad.ThumbSticks.Left.X * _speed * dt;
_playerPositions[i].Y -= pad.ThumbSticks.Left.Y * _speed * dt;
}
// Player 1 can also use keyboard
var kb = Keyboard.GetState();
if (kb.IsKeyDown(Keys.W)) _playerPositions[0].Y -= _speed * dt;
if (kb.IsKeyDown(Keys.S)) _playerPositions[0].Y += _speed * dt;
if (kb.IsKeyDown(Keys.A)) _playerPositions[0].X -= _speed * dt;
if (kb.IsKeyDown(Keys.D)) _playerPositions[0].X += _speed * dt;
base.Update(gameTime);
}
Split-screen rendering
Use GraphicsDevice.Viewport to render each player’s view in a separate region of the screen. Set the viewport before each player’s draw pass.
private Viewport _fullViewport;
private Viewport[] _splitViewports;
protected override void Initialize()
{
_fullViewport = GraphicsDevice.Viewport;
int halfWidth = _fullViewport.Width / 2;
_splitViewports = new Viewport[]
{
new Viewport(0, 0, halfWidth, _fullViewport.Height),
new Viewport(halfWidth, 0, halfWidth, _fullViewport.Height)
};
base.Initialize();
}
protected override void Draw(GameTime gameTime)
{
for (int i = 0; i < 2; i++)
{
GraphicsDevice.Viewport = _splitViewports[i];
GraphicsDevice.Clear(Color.CornflowerBlue);
_cameras[i].Follow(_playerPositions[i]);
_spriteBatch.Begin(transformMatrix: _cameras[i].GetTransform());
DrawWorld();
_spriteBatch.Draw(_playerTextures[i], _playerPositions[i], Color.White);
_spriteBatch.End();
}
// Reset viewport for HUD
GraphicsDevice.Viewport = _fullViewport;
DrawHUD();
base.Draw(gameTime);
}
Networking fundamentals
Online multiplayer sends data between machines over a network. Key concepts:
- TCP — Reliable, ordered delivery. Good for chat, lobby, and turn-based games.
- UDP — Fast, no guaranteed delivery. Good for real-time games (action, racing, FPS).
- Client-server — One authoritative server; clients send input and receive state.
- Peer-to-peer — Each player communicates directly. Simpler but harder to secure.
- Tick rate — How often the server updates and broadcasts state (e.g., 20–60 times per second).
TCP socket communication
Use System.Net.Sockets.TcpClient and TcpListener for reliable message passing. This is suitable for lobbies, chat, turn-based logic, and initial handshakes.
public class GameServer
{
private TcpListener _listener;
private List<TcpClient> _clients = new();
public void Start(int port)
{
_listener = new TcpListener(IPAddress.Any, port);
_listener.Start();
_ = AcceptClientsAsync();
}
private async Task AcceptClientsAsync()
{
while (true)
{
var client = await _listener.AcceptTcpClientAsync();
_clients.Add(client);
_ = HandleClientAsync(client);
}
}
private async Task HandleClientAsync(TcpClient client)
{
var stream = client.GetStream();
var buffer = new byte[1024];
while (client.Connected)
{
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0) break;
// Process message and broadcast to other clients
byte[] data = buffer[..bytesRead];
foreach (var other in _clients)
{
if (other == client) continue;
await other.GetStream().WriteAsync(data);
}
}
_clients.Remove(client);
}
}
public class GameClient
{
private TcpClient _tcp;
private NetworkStream _stream;
public Queue<byte[]> IncomingMessages { get; } = new();
public async Task ConnectAsync(string host, int port)
{
_tcp = new TcpClient();
await _tcp.ConnectAsync(host, port);
_stream = _tcp.GetStream();
_ = ReceiveLoopAsync();
}
private async Task ReceiveLoopAsync()
{
var buffer = new byte[1024];
while (_tcp.Connected)
{
int bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0) break;
IncomingMessages.Enqueue(buffer[..bytesRead]);
}
}
public async Task SendAsync(byte[] data)
{
await _stream.WriteAsync(data);
}
}
UDP for real-time games
UDP sacrifices reliability for speed. Send position/state snapshots frequently and tolerate occasional packet loss. Use sequence numbers to detect and discard out-of-order packets.
public class UdpNetwork
{
private UdpClient _udp;
private IPEndPoint _remoteEndPoint;
public void Start(int localPort, string remoteHost, int remotePort)
{
_udp = new UdpClient(localPort);
_remoteEndPoint = new IPEndPoint(
IPAddress.Parse(remoteHost), remotePort);
}
public void Send(byte[] data)
{
_udp.Send(data, data.Length, _remoteEndPoint);
}
public byte[] Receive()
{
if (_udp.Available > 0)
{
IPEndPoint sender = null;
return _udp.Receive(ref sender);
}
return null;
}
}
Client-server architecture
In the authoritative server model, the server runs the game simulation. Clients send their input; the server validates it, advances the game state, and broadcasts the result.
// Server runs a fixed-tick loop independent of rendering
private const float TickRate = 1f / 30f; // 30 ticks per second
private float _tickAccumulator;
public void ServerUpdate(float deltaTime)
{
_tickAccumulator += deltaTime;
while (_tickAccumulator >= TickRate)
{
// Process all queued client inputs
foreach (var client in _clients)
{
while (client.InputQueue.TryDequeue(out var input))
ApplyInput(client.PlayerId, input);
}
// Advance game simulation
_gameWorld.Simulate(TickRate);
// Broadcast state snapshot to all clients
byte[] snapshot = SerializeWorldState(_gameWorld);
BroadcastToAll(snapshot);
_tickAccumulator -= TickRate;
}
}
Message serialisation
Convert game data to bytes for transmission. Use BinaryWriter/BinaryReader for compact formats, or System.Text.Json for readability during development.
public enum MessageType : byte { Input = 1, State = 2, Chat = 3 }
public static byte[] SerializeInput(int playerId, Vector2 direction)
{
using var ms = new MemoryStream();
using var bw = new BinaryWriter(ms);
bw.Write((byte)MessageType.Input);
bw.Write(playerId);
bw.Write(direction.X);
bw.Write(direction.Y);
return ms.ToArray();
}
public static (int playerId, Vector2 direction) DeserializeInput(byte[] data)
{
using var ms = new MemoryStream(data);
using var br = new BinaryReader(ms);
br.ReadByte(); // skip message type
int id = br.ReadInt32();
float x = br.ReadSingle();
float y = br.ReadSingle();
return (id, new Vector2(x, y));
}
Lag compensation and prediction
Network latency causes visible delays. Compensate with these techniques:
- Client-side prediction — The client applies its own input immediately and corrects when the server confirms.
- Server reconciliation — When the server state arrives, the client rewinds and replays unconfirmed inputs.
- Entity interpolation — Smooth remote player positions between received snapshots instead of snapping.
public class NetworkEntity
{
private Vector2 _previousPosition;
private Vector2 _targetPosition;
private float _lerpTimer;
private const float LerpDuration = 0.1f; // 100ms between snapshots
public Vector2 InterpolatedPosition { get; private set; }
public void ReceiveSnapshot(Vector2 newPosition)
{
_previousPosition = InterpolatedPosition;
_targetPosition = newPosition;
_lerpTimer = 0f;
}
public void Update(float dt)
{
_lerpTimer += dt;
float t = MathHelper.Clamp(_lerpTimer / LerpDuration, 0f, 1f);
InterpolatedPosition = Vector2.Lerp(
_previousPosition, _targetPosition, t);
}
}
Networking libraries
Rather than building everything from scratch, consider these .NET networking libraries that work well with MonoGame:
| Library | Protocol | Best for |
|---|---|---|
| LiteNetLib | UDP with reliability layer | Real-time action games |
| Lidgren.Network | UDP with reliability | Established library, wide community use |
| System.Net.Sockets | TCP / UDP raw | Full control, no dependencies |
| SignalR | WebSocket / long-polling | Turn-based or lobby systems |
| Mirror / Netcode | Various | Unity-focused but concepts transfer |
Start with local multiplayer to prove your gameplay works, then layer networking on top. Networking adds significant complexity — get the single-player version right first.