Table of Contents

Transport layer

The library never talks to a socket: it hands byte spans to an AbstractNetPeer and expects two delivery modes back. LiteNetLib is the shipped implementation; anything that can offer reliable-ordered and unreliable delivery can replace it.

What a transport must provide

AbstractNetPeer has four members:

Member Contract
SendReliableOrdered(ReadOnlySpan<byte>) Guaranteed delivery, in order. Used for baselines and client requests
SendUnreliable(ReadOnlySpan<byte>) Best effort, no retransmission. Used for state deltas and inputs
GetMaxUnreliablePacketSize() Largest payload that fits one unreliable packet, so state can be split into parts
TriggerSend() Flush now rather than at the transport's next opportunity

The library relies on those semantics being honest. Unreliable must really be allowed to drop packets — layering it on a reliable stream adds head-of-line blocking exactly where the design assumes there is none, and stale states would queue up behind lost ones instead of being superseded.

Using LiteNetLib

Wrap the connection and pass it in — server side per player, client side once:

// server
var player = _entityManager.AddPlayer(new LiteNetLibNetPeer(peer, assignToTag: true));

// client
_entityManager = new ClientEntityManager(typesMap, new LiteNetLibNetPeer(peer, true), headerByte);

assignToTag: true stores the adapter in LiteNetLib's Tag, which is how later callbacks recover it ((LiteNetLibNetPeer)peer.Tag). The extension methods GetLiteNetLibNetPeerFromTag() and GetLiteNetLibNetPeer() do the same lookups in one call.

Sharing a connection

Every packet the library produces starts with the header byte given to the manager, so your own protocol can travel on the same connection. Route by peeking at the first byte:

// server side
if ((PacketType)reader.PeekByte() == PacketType.EntitySystem)
    _entityManager.Deserialize((LiteNetLibNetPeer)peer.Tag, reader.GetRemainingBytesSpan());
else
    _myProtocol.Read(reader, peer);

Deserialize also validates the header itself and returns HeaderCheckFailed for anything that is not a library packet, so a mistake here is diagnosable rather than corrupting.

Writing a custom transport

Implement the four members over your own connection object and use it wherever LiteNetLibNetPeer would appear — the managers accept any AbstractNetPeer:

MyTransportPeer.cs

using System;
using LiteEntitySystem.Transport;

public class MyTransportPeer : AbstractNetPeer
{
    private readonly MyConnection _connection;

    public MyTransportPeer(MyConnection connection) => _connection = connection;

    public override void SendReliableOrdered(ReadOnlySpan<byte> data) =>
        _connection.Send(data, reliable: true);

    public override void SendUnreliable(ReadOnlySpan<byte> data) =>
        _connection.Send(data, reliable: false);

    public override int GetMaxUnreliablePacketSize() => _connection.Mtu - MyConnection.HeaderOverhead;

    public override void TriggerSend() => _connection.Flush();
}

Then feed received bytes to Deserialize — with the sending player on the server, without arguments on the client — and the rest of the library is unchanged. Peer-management APIs still work: AddPlayer(peer) accepts your type, and NetPlayer.Peer hands it back.

Report GetMaxUnreliablePacketSize honestly: too large and packets fragment or drop below the library, too small and state is split into more parts than necessary.

Warning

Common mistakes

  • Implementing "unreliable" as reliable — deltas then queue behind lost packets, adding latency the design specifically avoids.
  • An optimistic GetMaxUnreliablePacketSize — states are sized against it; overshooting means silent drops at the network layer.
  • Skipping TriggerSend — everything still works, but each send waits for the transport's own schedule, adding avoidable latency.
  • Feeding foreign packets into Deserialize — check the header byte first; the return value tells you when you got it wrong.