Getting Started with LiteNetLib
Tags: csharp, tutorial
Published: 2020-08-26 21:16:01

Seen as there are not many tutorials or documentation for the library, I've decided to write a guide that you can follow to get started with setting up [LiteNetLib](https://github.com/RevenantX/LiteNetLib/) for a basic multiplayer game.

If you're not familiar with it, LiteNetLib is a lightweight C# networking library for game development that implements a reliable UDP protocol. It has been successfully used in commercial games such as [7 Days to Die](https://store.steampowered.com/app/251570/7_Days_to_Die/).

The code snippets in this article will be using Godot, but you can easily adapt the code for Unity or whatever other platform you're using.

# Client and Server

Let's start with basic classes for the client and the server. If you're using Godot, you might want to add the client to AutoLoad, to keep it loaded between scenes.

You'll also want to create a separate scene for the server class. CLI Godot can be used for running two instances of your game at the same time. `cd` into your project's directory and run `godot-mono scenes/Server.tscn`.

```cs
using Godot;
using System;
using LiteNetLib;
using LiteNetLib.Utils;

public class Client : Node, INetEventListener {
    private NetManager client;

    public void Connect() {
        client = new NetManager(this) {
            AutoRecycle = true,
        };
    }

    public override void _Process(float delta) {
        if (client != null) {
            client.PollEvents();
        }
    }

    // ... INetEventListener methods omitted
}
```

```cs
using Godot;
using System;
using LiteNetLib;
using LiteNetLib.Utils;

public class Server : Node, INetEventListener {
    private NetManager server;

    public override void _Ready() {
        server = new NetManager(this) {
            AutoRecycle = true,
        };
    }

    public override void _Process(float delta) {
        server.PollEvents();
    }

    // ... INetEventListener methods omitted
}
```

Things to note:

- I am implementing `INetEventListener` on the classes themselves. Hopefully your code editor can automatically generate the interface implementation, but if not, you can create a `EventBasedNetListener` and pass it to `NetManager` instead, as per the [README example](https://github.com/RevenantX/LiteNetLib/blob/64d7fe36ec29d54c04cc129fb4313646f7358ca9/README.md#client).
- You can keep the methods mostly empty for now, as they're all callbacks.
- We check if `client != null` since it usually won't be started immediately, unlike the server.

The next step is connecting the client to the server:

```cs
    private NetPeer server;

    public void Connect() {
        // ...
        client.Start();
        GD.Print("Connecting to server");
        client.Connect("localhost", 12345, "");
    }

    public void OnPeerConnected(NetPeer peer) {
        GD.Print("Connected to server");
        server = peer;
    }
```

```cs
    public override void _Ready() {
        // ...
        GD.Print("Starting server");
        server.Start(12345);
    }

    public void OnConnectionRequest(ConnectionRequest request) {
        GD.Print($"Incoming connection from {request.RemoteEndPoint.ToString()}");
        request.Accept();
    }
```

Things to note:

- You'll probably want to limit the number of peers that can connect to the server eventually. This can be done by checking `server.ConnectedPeerCounts` and rejecting requests when necessary.
- You can add a "password" (connection key) to the server, which is useful for things like blocking players with outdated clients from joining the server. Take a look at the parameters of `client.Connect` and `connection.AcceptIfKey`.

# Communicating with Packets

To communicate between client and server, we must first define some packets:

```cs
public class JoinPacket {
    public string username { get; set; }
}

public class JoinAcceptPacket {
    public PlayerState state { get; set; }
}

public struct PlayerState : INetSerializable {
    public uint pid;
    public Vector2 position;

    public void Serialize(NetDataWriter writer) {
        writer.Put(pid);
        writer.Put(position);
    }

    public void Deserialize(NetDataReader reader) {
        pid = reader.GetUInt();
        position = reader.GetVector2();
    }
}

public class ClientPlayer {
    public PlayerState state;
    public string username;
}

public class ServerPlayer {
    public NetPeer peer;
    public PlayerState state;
    public string username;
}
```

The packet classes are automatically serialized by LiteNetLib. Only properties are serialized though, not fields, so the `{ get; set; }` is necessary. You can also define a struct instead and implement `INetSerializable` manually, as it was done for `PlayerState`. This is useful since you also get the copy semantics of a struct.

We are still missing something, however. You might have noticed the `reader.GetVector2` and `writer.Put(Vector2)` functions don't actually exist. LiteNetLib can serialize most basic data types by default, but not Godot's `Vector2`, since it's a struct. Let's implement extension methods for `NetDataWriter` and `NetDataReader`:

```cs
public static class SerializingExtensions {
    public static void Put(this NetDataWriter writer, Vector2 vector) {
        writer.Put(vector.x);
        writer.Put(vector.y);
    }

    public static Vector2 GetVector2(this NetDataReader reader) {
        return new Vector2(reader.GetFloat(), reader.GetFloat());
    }
}
```

Now let's start sending packets:

```cs
    private NetDataWriter writer;
    private NetPacketProcessor packetProcessor;
    private ClientPlayer player = new ClientPlayer();

    public void Connect(string username) {
        player.username = username;
        writer = new NetDataWriter();
        packetProcessor = new NetPacketProcessor();
        packetProcessor.RegisterNestedType((w, v) => w.Put(v), reader => reader.GetVector2());
        packetProcessor.RegisterNestedType<PlayerState>();
        packetProcessor.SubscribeReusable<JoinAcceptPacket>(OnJoinAccept);
        // ...
    }

    public void SendPacket<T>(T packet, DeliveryMethod deliveryMethod) where T : class, new() {
        if (server != null) {
            writer.Reset();
            packetProcessor.Write(writer, packet);
            server.Send(writer, deliveryMethod);
        }
    }

    public void OnJoinAccept(JoinAcceptPacket packet) {
        GD.Print($"Join accepted by server (pid: {packet.state.pid})");
        player.state = packet.state;
    }

    public void OnPeerConnected(NetPeer peer) {
        // ...
        SendPacket(new JoinPacket { username = player.username }, DeliveryMethod.ReliableOrdered);
    }

    public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod) {
        packetProcessor.ReadAllPackets(reader);
    }
```

```cs
    [Export] public Vector2 initialPosition = new Vector2();
    private NetDataWriter writer;
    private NetPacketProcessor packetProcessor;
    private Dictionary<uint, ServerPlayer> players = new Dictionary<uint, ServerPlayer>();

    public override void _Ready() {
        writer = new NetDataWriter();
        packetProcessor = new NetPacketProcessor();
        packetProcessor.RegisterNestedType((w, v) => w.Put(v), reader => reader.GetVector2());
        packetProcessor.RegisterNestedType<PlayerState>();
        packetProcessor.SubscribeReusable<JoinPacket, NetPeer>(OnJoinReceived);
        // ...
    }

    public void SendPacket<T>(T packet, NetPeer peer, DeliveryMethod deliveryMethod) where T : class, new() {
        if (peer != null) {
            writer.Reset();
            packetProcessor.Write(writer, packet);
            peer.Send(writer, deliveryMethod);
        }
    }

    public void OnJoinReceived(JoinPacket packet, NetPeer peer) {
        GD.Print($"Received join from {packet.username} (pid: {(uint)peer.Id})");

        ServerPlayer newPlayer = (players[(uint)peer.Id] = new ServerPlayer {
            peer = peer,
            state = new PlayerState {
                pid = (uint)peer.Id,
                position = initialPosition,
            },
            username = packet.username,
        });

        SendPacket(new JoinAcceptPacket { state = newPlayer.state }, peer, DeliveryMethod.ReliableOrdered);
    }

    public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod) {
        packetProcessor.ReadAllPackets(reader, peer);
    }

    public void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) {
        if (peer.Tag != null) {
            players.Remove((uint)peer.Id);
        }
    }
```

Things to note:

- **`packetProcessor.SubscribeReusable` will reuse the same packet class instance instead of creating new ones, so make sure to not store references to it or its contents!** This is another advantage of making `PlayerState` a struct, so that we can easily copy it out.
- If you had previously defined a struct with `INetSerializable`, you must register it using `packetProcessor.RegisterNestedType<YourType>`.
- LiteNetLib has a couple different packet delivery methods. You'll want to use `DeliveryMethod.ReliableOrdered` (the safest one) for most things, and `DeliveryMethod.Unreliable` for fast updates, where a dropped packet or two wouldn't matter too much.
- You can pass anything you want as the second argument to `packetProcessor.ReadAllPackets` to be used by the packet callbacks. In this case we only need the peer.

If you test the game now, everything should work fine. The client will send a `JoinPacket` and the server will respond with an appropriate `JoinAcceptPacket`.

# Updating Players

In order to send player info over the network, we first need a player class. You should already have one in your game.

```cs
public class Player : Node2D {
    [Export] public float moveSpeed = 200;
    public static Player instance;

    public override void _Ready() {
        instance = this;
    }

    public override void _Process(float delta) {
        Vector2 velocity = new Vector2();

        if (Input.IsActionPressed("ui_left")) velocity.x -= 1;
        if (Input.IsActionPressed("ui_right")) velocity.x += 1;
        if (Input.IsActionPressed("ui_up")) velocity.y -= 1;
        if (Input.IsActionPressed("ui_down")) velocity.y += 1;

        Position += velocity * moveSpeed * delta;
    }
}
```

For any nontrivial game, you'll most likely also need a separate class to represent other players, since they don't need all the processing logic of the client player:

```cs
public class RemotePlayer : Node2D {}
```

Now let's define some more packets:

```cs
public class PlayerSendUpdatePacket {
    public Vector2 position { get; set; }
}

public class PlayerReceiveUpdatePacket {
    public PlayerState[] states { get; set; }
}

public class PlayerJoinedGamePacket {
    public ClientPlayer player { get; set; }
}

public class PlayerLeftGamePacket {
    public uint pid { get; set; }
}

// ...

public struct ClientPlayer : INetSerializable {
    public PlayerState state;
    public string username;

    public void Serialize(NetDataWriter writer) {
        state.Serialize(writer);
        writer.Put(username);
    }

    public void Deserialize(NetDataReader reader) {
        state.Deserialize(reader);
        username = reader.GetString();
    }
}
```

Since we will be sending `ClientPlayer` over the network, we turn it into a struct and implement `INetSerializable`.

Now, to send those packets:

```cs
    public override void Connect() {
        // ...
        packetProcessor.RegisterNestedType<ClientPlayer>();
        packetProcessor.SubscribeReusable<PlayerReceiveUpdatePacket>(OnReceiveUpdate);
        packetProcessor.SubscribeReusable<PlayerJoinedGamePacket>(OnPlayerJoin);
        packetProcessor.SubscribeReusable<PlayerLeftGamePacket>(OnPlayerLeave);
        // ...
    }

    public override void _Process(float delta) {
        if (client != null) {
            client.PollEvents();
            if (Player.instance != null) {
                SendPacket(new PlayerSendUpdatePacket { position = Player.instance.Position }, DeliveryMethod.Unreliable);
            }
        }
    }

    public void OnJoinAccept(JoinAcceptPacket packet) {
        // ...
        Player.instance.Position = player.state.position;
    }

    public void OnReceiveUpdate(PlayerReceiveUpdatePacket packet) {
        foreach (PlayerState state in packet.states) {
            if (state.pid == player.state.pid) {
                continue;
            }

            ((RemotePlayer)Player.instance.GetParent().GetNode(state.pid.ToString())).Position = state.position;
        }
    }

    public void OnPlayerJoin(PlayerJoinedGamePacket packet) {
        GD.Print($"Player '{packet.player.username}' (pid: {packet.player.state.pid}) joined the game");
        RemotePlayer remote = (RemotePlayer)((PackedScene)GD.Load("res://scenes/RemotePlayer.tscn")).Instance();
        remote.Name = packet.player.state.pid.ToString();
        remote.Position = packet.player.state.position;
        Player.instance.GetParent().AddChild(remote);
    }

    public void OnPlayerLeave(PlayerLeftGamePacket packet) {
        GD.Print($"Player (pid: {packet.pid}) left the game");
        ((RemotePlayer)Player.instance.GetParent().GetNode(packet.pid.ToString())).QueueFree();
    }
```

```cs
    public override void _Ready() {
        // ...
        packetProcessor.RegisterNestedType<ClientPlayer>();
        packetProcessor.SubscribeReusable<PlayerSendUpdatePacket, NetPeer>(OnPlayerUpdate);
        // ...
    }

    public override void _Process(float delta) {
        // ...
        PlayerState[] states = players.Values.Select(p => p.state).ToArray();
        foreach (ServerPlayer player in players.Values) {
            SendPacket(new PlayerReceiveUpdatePacket { states = states }, player.peer, DeliveryMethod.Unreliable);
        }
    }

    public void OnJoinReceived(JoinPacket packet, NetPeer peer) {
        // ...
        foreach (ServerPlayer player in players.Values) {
            if (player.state.pid != newPlayer.state.pid) {
                SendPacket(new PlayerJoinedGamePacket {
                    player = new ClientPlayer {
                        username = newPlayer.username,
                        state = newPlayer.state,
                    },
                }, player.peer, DeliveryMethod.ReliableOrdered);

                SendPacket(new PlayerJoinedGamePacket {
                    player = new ClientPlayer {
                        username = player.username,
                        state = player.state,
                    },
                }, newPlayer.peer, DeliveryMethod.ReliableOrdered);
            }
        }
    }

    public void OnPlayerUpdate(PlayerSendUpdatePacket packet, NetPeer peer) {
        players[(uint)peer.Id].state.position = packet.position;
    }

    public void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) {
        GD.Print($"Player (pid: {(uint)peer.Id}) left the game");
        if (peer.Tag != null) {
            ServerPlayer playerLeft;
            if (players.TryGetValue(((uint)peer.Id), out playerLeft)) {
                foreach (ServerPlayer player in players.Values) {
                    if (player.state.pid != playerLeft.state.pid) {
                        SendPacket(new PlayerLeftGamePacket { pid = playerLeft.state.pid }, player.peer, DeliveryMethod.ReliableOrdered);
                    }
                }
                players.Remove((uint)peer.Id);
            }
        }
    }
```

Things to note:

- You should probably put the code for sending updates from server on a timer rather than doing it every frame. 20 to 30 updates per second is good enough for most cases. This doesn't matter as much in the client, as it can generally send updates faster than the server.
- It might be useful to verify some of the data received from the server. For example, if it gives the client an invalid pid, Godot will try to access a node that doesn't exist, crashing the game. Those checks have been excluded from the example code for simplicity.

If you run the game now, you should be able to see other connected players moving with you.

![The result](https://dev-to-uploads.s3.amazonaws.com/i/oplbtc4jo1b9efbe0f99.png)

# Moving Forward

That's about everything you need to start developing a multiplayer game with LiteNetLib. This guide doesn't cover more advanced topics such as creating an authoritative server to prevent cheating or client-side prediction, but those can be implemented on top of the base provided by the code here.

An example implementation of more complex features can be found in [NetGameExample](https://github.com/RevenantX/NetGameExample/), which I would also encourage you to read through, as it was one of the resources I used to learn LiteNetLib and write all of this.

The source code for LiteNetLib has doc comments for most functions, so it could be a good idea to take a look at least. There are many functions and features of the library which weren't covered here.

Hopefully this article has been useful to you. Thanks for reading!