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 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:

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.