Multiplayer Framework

This project was aiming to provide a low-level multiplayer API that can be used in Unity. It was developed in a small group of me and 3 others while being in University. Since the original repository is a privat one which I can't change the linked github repo is a mirror. The difference from common multiplayer frameworks is that it supports (and was designed) to allow multiplayer for AR/VR applications. This was a major learning step for me since this multiplayer framework allows sending packets using UDP or TCP. It also allows for file sending and message sending over the network. A lobby can be created in which people can join.

One of the core components are network shared variables defined as:

using NetLib.Serialization;

namespace NetLib.NetworkVar
{
    /// <summary>
    /// Generic container for networked variables inside of a NetworkBehaviour.
    /// </summary>
    /// <typeparam name="T">A serializable type for the internal value</typeparam>
    public class NetworkVar<T> : INetworkVar
    {
        private T internalValue;

        /// <summary>
        /// Used to get and set the value of this <see cref="NetworkVar{T}"/>.
        /// Use this instead of reassigning the NetworkVar object.
        /// </summary>
        public T Value
        {
            get => internalValue;
            set
            {
                internalValue = value;
                IsDirty = true;
            }
        }

        /// <inheritdoc/>
        public bool IsDirty { get; private set; }

        /// <summary>
        /// Initializes the internal value to its type's default value.
        /// </summary>
        public NetworkVar()
        {
            internalValue = default;
        }

        /// <summary>
        /// Initializes the internal value to the given value.
        /// </summary>
        /// <param name="value">the initial value</param>
        public NetworkVar(T value)
        {
            internalValue = value;
        }

        public static implicit operator T(NetworkVar<T> networkVar) => networkVar.internalValue;
        public static explicit operator NetworkVar<T>(T t) => new NetworkVar<T>(t);

        /// <inheritdoc/>
        public void ResetDirty()
        {
            IsDirty = false;
        }

        /// <inheritdoc/>
        public void WriteValue(byte[] data)
        {
            Serializer.Deserialize(ref internalValue, data);
        }

        /// <inheritdoc/>
        public void ReadValue(out byte[] data)
        {
            data = Serializer.Serialize(internalValue);
        }
    }
}

The Client is defined as an Interface that also has to implement the Transport Interface.

namespace NetLib.Transport
{
    /// <summary>
    /// Represents a transport layer client.
    /// </summary>
    /// <remarks>
    /// For more information see <see cref="ITransport"/>.
    /// </remarks>
    public interface IClient : ITransport
    {
        /// <summary>
        /// Connects this client to a server.
        /// </summary>
        /// <param name="ip">The ip address of the server to which to connect.</param>
        /// <param name="port">The network port of the server on which to connect to.</param>
        /// <exception cref="FailedToStartClientException">The client could not be initialized.</exception>
        void Connect(string ip, ushort port);

        /// <summary>
        /// Disconnects this client from the connected server.
        /// </summary>
        void Disconnect();
    }
}
namespace NetLib.Transport
{
    /// <summary>
    /// Delegate for handling connect messages.
    /// </summary>
    /// <remarks>
    /// For a server receiving this message the <c>id</c> will be a new connection id for the client which just
    /// connected. For a client receiving this message the <c>id</c> will be 0, denoting the connection id of the
    /// server.
    /// </remarks>
    /// <param name="id">Connection ID of the participant which just connected.</param>
    public delegate void OnConnect(ulong id);

    /// <summary>
    /// Delegate for handling disconnect messages.
    /// </summary>
    /// <remarks>
    /// For a server receiving this message the <c>id</c> will be a new connection id for the client which just
    /// disconnected. For a client receiving this message the <c>id</c> will be 0, denoting the connection id of the
    /// server.
    /// </remarks>
    /// <param name="id">Connection ID of the client which just disconnected.</param>
    public delegate void OnDisconnect(ulong id);

    /// <summary>
    /// Delegate for handling data messages.
    /// </summary>
    /// <param name="id">Connection ID of the participant who sent the message.</param>
    /// <param name="data">The data sent by the participant.</param>
    public delegate void OnData(ulong id, byte[] data);

    /// <summary>
    /// Represents a transport layer peer for sending and receiving messages across the network.
    /// </summary>
    /// <remarks>
    /// The transport layer peer is a participant in a client/server architecture. There can be many clients but only
    /// one server in a given connection. A connection is bound to a network port. Therefore multiple client/server
    /// connections can be run in parallel on separate ports.
    /// <para>
    /// Each participant gets assigned a connection id, which is used to identify the receiver when sending messages.
    /// The server will always have the ID 0.
    /// </para>
    /// <para>
    /// The transport object can be either a client or a server. A server can send and receive messages to and from
    /// all connected clients. A client can only send and receive messages to and from the server it is connected to.
    /// </para>
    /// <para>
    /// In order to react to received messages the <see cref="OnConnect"/>, <see cref="OnData"/> and
    /// <see cref="OnDisconnect"/> delegate members can be subscribed to. Incoming messages are internally queued and
    /// the delegates are only invoked when <see cref="Poll"/> is called.
    /// </para>
    /// Base interface for <see cref="IServer"/> and <see cref="IClient"/>.
    /// </remarks>
    public interface ITransport
	{
        /// <summary>
        /// Contains functions called if a connection is established.
        /// <remarks>
        /// See OnConnect delegate type for more information. 
        /// </remarks>
        /// </summary>
        OnConnect OnConnect { get; set; }

        /// <summary>
        /// Contains functions called if data is received.
        /// <remarks>
        /// See OnData delegate type for more information.
        /// </remarks>
        /// </summary>
        OnData OnData { get; set; }

        /// <summary>
        /// Contains functions called if peer disconnects.
        /// <remarks>
        /// See OnDisconnect delegate type for more information.
        /// </remarks>
        /// </summary>
        OnDisconnect OnDisconnect { get; set; }

        /// <summary>
        /// Whether this transport is connected and running.
        /// </summary>
        bool IsActive { get; }

        /// <summary>
        /// Polls all received messages since the last call to <see cref="Poll"/> and invokes the corresponding
        /// delegates.
        /// </summary>
        void Poll();

        /// <summary>
        /// Sends a data message to a connected peer.
        /// </summary>
        /// <param name="message">The data to be sent.</param>
        /// <param name="id">The connection id of the peer to send the data to.</param>
        /// <exception cref="System.ArgumentNullException">The <c>message</c> is null.</exception>
        /// <exception cref="InvalidConnectionIdException">The <c>id</c> is not a valid connected peer.</exception>
        /// <exception cref="MessageNotSentException">The message could not be sent.</exception>
        void Send(byte[] message, ulong id = 0);
    }
}

The actual implementation of for example the UDP client can be seen here:

using NetLib.Serialization;

namespace NetLib.NetworkVar
{
    /// <summary>
    /// Generic container for networked variables inside of a NetworkBehaviour.
    /// </summary>
    /// <typeparam name="T">A serializable type for the internal value</typeparam>
    public class NetworkVar<T> : INetworkVar
    {
        private T internalValue;

        /// <summary>
        /// Used to get and set the value of this <see cref="NetworkVar{T}"/>.
        /// Use this instead of reassigning the NetworkVar object.
        /// </summary>
        public T Value
        {
            get => internalValue;
            set
            {
                internalValue = value;
                IsDirty = true;
            }
        }

        /// <inheritdoc/>
        public bool IsDirty { get; private set; }

        /// <summary>
        /// Initializes the internal value to its type's default value.
        /// </summary>
        public NetworkVar()
        {
            internalValue = default;
        }

        /// <summary>
        /// Initializes the internal value to the given value.
        /// </summary>
        /// <param name="value">the initial value</param>
        public NetworkVar(T value)
        {
            internalValue = value;
        }

        public static implicit operator T(NetworkVar<T> networkVar) => networkVar.internalValue;
        public static explicit operator NetworkVar<T>(T t) => new NetworkVar<T>(t);

        /// <inheritdoc/>
        public void ResetDirty()
        {
            IsDirty = false;
        }

        /// <inheritdoc/>
        public void WriteValue(byte[] data)
        {
            Serializer.Deserialize(ref internalValue, data);
        }

        /// <inheritdoc/>
        public void ReadValue(out byte[] data)
        {
            data = Serializer.Serialize(internalValue);
        }
    }
}

More functionality

Even file sending is all implemented by scratch without any high level functionality:

using System.Collections.Generic;
using NetLib.Serialization;

namespace NetLib.Utils
{
    /// <summary>
    /// Responsible for reconstructing file messages that are split into multiple parts.
    /// </summary>
    public class FileHandler
    {
        // Caches the parts of the messages for later reconstruction
        private readonly Dictionary<string, List<byte>> fileArrays = new Dictionary<string, List<byte>>();

        /// <summary>
        /// Adds a message part to the message cache and writes the files, if all parts of the message are received.
        /// </summary>
        /// <param name="totalMessageParts">Total number of parts the message is split into.</param>
        /// <param name="currentMessagePart">The current message part which will get added.</param>
        /// <param name="fileHash">The hash value by which to identify the file to which the message belongs to.</param>
        /// <param name="fileBytes">The file bytes of the current message part will will get added.</param>
        /// <param name="destinationPath">Path to where to save the completed file on the target system.</param>
        public void AddMessage(int totalMessageParts, int currentMessagePart, string fileHash, byte[] fileBytes, string destinationPath)
        {
            // add new part
            if (currentMessagePart < totalMessageParts)
            {
                if (!fileArrays.ContainsKey(fileHash))
                    fileArrays.Add(fileHash, new List<byte>(fileBytes));
                else
                    fileArrays[fileHash].AddRange(fileBytes);
            }

            // save file if parts are complete
            if (currentMessagePart == totalMessageParts - 1)
            {
                ReconstructFullMessage(fileArrays[fileHash], destinationPath);
                fileArrays.Remove(fileHash);
            }
        }

        private void ReconstructFullMessage(List<byte> msg, string path)
        {
            FileSerializer.Deserialize(msg.ToArray(), path);
        }
    }
}