paint-brush
Unity Realtime Multiplayer, Part 3: Reliable UDP Protocolby@dmitrii
2,237 reads
2,237 reads

Unity Realtime Multiplayer, Part 3: Reliable UDP Protocol

by Dmitrii IvashchenkoAugust 15th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Reliable UDP (RUDP) combines TCP's reliability with UDP's efficiency. RUDP overcomes UDP's drawbacks like data loss and order changes by methods like error checking, retransmission, and sequence management. It's faster than TCP, ensuring reliable packet delivery while preserving order. Not a standard protocol, RUDP requires implementation according to its specs or using libraries like LiteNetLib, Ruffles, Hazel Networking, ENet-CSharp, kcp2k, or Unity Transport Package. The article provides RUDP client and server code examples using C# in Unity. The next article will delve into challenges in connecting devices globally despite available protocols and libraries.
featured image - Unity Realtime Multiplayer, Part 3: Reliable UDP Protocol
Dmitrii Ivashchenko HackerNoon profile picture


In the previous parts of this series, we've talked about TCP and UDP protocols, which each have their own strengths and weaknesses. But, what if we could combine the reliability of TCP with the efficiency of UDP? That's where Reliable UDP, or RUDP, comes in!


Hello again! My name is Dmitrii Ivashchenko, Lead Software Engineer at MY.GAMES. As part of our ongoing series titled "Unity Networking Landscape 2023", we'll explore some methods to ensure dependable data transmission utilizing the (not so steady) UDP protocol.


Content Overview

  • Reliable UDP
  • Basic RUDP Client
  • Basic RUDP Server
  • Implementations
  • Wrapping Up


Reliable UDP

The architecture of RUDP (Reliable UDP) provides a solution that allows us to overcome the disadvantages related to the unreliability of the UDP protocol. The main disadvantages of UDP that need to be overcome are data loss, changes in their delivery order, and the possibility of errors. The goal of RUDP is to create reliable connections similar to the TCP protocol.


The following methods can be used to implement RUDP: to prevent data loss, an error code can be inserted into the transmitted packet, and errors can be checked. When an error is detected, the packet can either be ignored or retransmission requested. The insertion of error correction codes can also be used, but it can lead to excessive bandwidth consumption, as the probability of error at the UDP level is usually very low.


To solve the problem of data loss, we can continually retransmit undelivered packets until confirmation is received from the receiver. This method is more efficient in terms of processing complexity and resource usage than waiting for a signal of packet loss from the receiver.


To ensure the sequence of data processing, the order of packet arrival can be checked, and data waiting for the undelivered packet can be temporarily saved. Then, after any lost packets have arrived, all the data is transmitted into the game. Thus, RUDP effectively controls lag.

Comparison with TCP and UDP

RUDP has the ability to retransmit lost packets and receive acknowledgments from the recipient. It's faster than TCP and takes advantage of UDP's guaranteed packet delivery and convenient retransmission structure. However, RUDP is not a standard protocol like UDP or TCP. It must be implemented according to its specification or use ready-made implementations (such as in the form of a software library).


RUDP can be a good alternative to UDP when faster response times and high reliability are required in a game, as it guarantees the retransmission of lost packets and preserves the order of their delivery. RUDP is specifically designed to provide a normal flow of data and can be used in any type of game. It provides reliable communication by confirming the successful delivery of transmitted packets and by use of the timeout function.


RUDP provides a reliable communication channel between the sender and receiver to ensure successful packet delivery. The sender and a receiver maintain a predetermined window size to avoid errors when packets are lost. When sending data over an unreliable channel using RUDP, one can expect a higher success rate in delivery. During the sending and receiving process, the "base" and "next" variables track the window size. The process is repeated continuously to check the number of packets in the buffer, and the timeout is reset after a packet is transmitted.


To test the performance of RUDP, simulations of packet loss and network delay are conducted. Packets are correlated with the current system time and network delay before transmission.

Basic RUDP Client

This is a basic example of how you can implement RUDP in Unity using C#.


The RUDPClient class demonstrates reliable packet transmission by attaching sequence numbers to the data packets and tracking the sequence numbers for in-order delivery. Out-of-order packets are stored and processed when the missing packets arrive.


The SendData method attaches the sequence number to the data and sends the packet to the server using UDP (implementation code not included).


The ReceiveData method processes the received packets, checks for in-order delivery, and handles out-of-order packets.


using System;
using System.Collections.Generic;
using UnityEngine;

public class RUDPClient : MonoBehaviour
{
    // Variables for reliable packet transmission
    private int nextSequenceNumber = 0;
    private int expectedSequenceNumber = 0;
    private Dictionary<int, byte[]> sentPackets = new Dictionary<int, byte[]>();

    // Send data over the reliable UDP connection
    private void SendData(byte[] data)
    {
        // Attach the sequence number to the data
        byte[] packet = AttachSequenceNumber(data);

        // Send the packet to the server
        SendPacket(packet);
    }

		// Process the received data
    private void ProcessData(byte[] data)
    {
        // Process the received data here
        Debug.Log("Received data: " + System.Text.Encoding.UTF8.GetString(data));
    }

    // Send the packet to the server
    private void SendPacket(byte[] packet)
    {
        // Send the packet to the server using UDP
        // UDP implementation code goes here
    }
}


Now we'll need the AttachSequenceNumber method. This code adds a sequential number to the transmitted data. It creates a new byte array that includes the sequential number and the transmitted information.


Then, the packet is saved in the sentPackets dictionary for tracking. Every time this method is called, the sequential number is incremented for the next packet.


// Attach the sequence number to the data
private byte[] AttachSequenceNumber(byte[] data)
{
    // Create a new byte array to hold the packet
    byte[] packet = new byte[data.Length + sizeof(int)];

    // Copy the sequence number to the packet
    BitConverter.GetBytes(nextSequenceNumber).CopyTo(packet, 0);

    // Copy the data to the packet
    data.CopyTo(packet, sizeof(int));

    // Store the packet in the sentPackets dictionary for tracking
    sentPackets[nextSequenceNumber] = packet;

    // Increment the sequence number for the next packet
    nextSequenceNumber++;

    return packet;
}


To receive data from the server, we will need the ReceiveData method. It extracts the sequence number from the packet and checks if the packet has arrived in the correct order. If the packet arrives in the expected order, the data is extracted from the packet and processed. Then, the expected sequence number is incremented for the next packet. If a packet with a number different from the expected one arrives, it is saved for subsequent processing.


Next, we check for the presence of packets with sequential numbers that arrived out of order. Such packets are also processed and removed from the sentPackets dictionary.


// Receive data from the server
private void ReceiveData(byte[] packet)
{
    // Extract the sequence number from the packet
    int sequenceNumber = BitConverter.ToInt32(packet, 0);

    // Check if the packet is in order
    if (sequenceNumber == expectedSequenceNumber)
    {
        // Process the data from the packet
        byte[] data = new byte[packet.Length - sizeof(int)];
        Array.Copy(packet, sizeof(int), data, 0, data.Length);

        // Process the received data
        ProcessData(data);

        // Increment the expected sequence number for the next packet
        expectedSequenceNumber++;

        // Check if there are any out-of-order packets waiting
        while (sentPackets.ContainsKey(expectedSequenceNumber))
        {
            // Process the data from the out-of-order packet
            byte[] outOfOrderData = new byte[sentPackets[expectedSequenceNumber].Length - sizeof(int)];
            Array.Copy(sentPackets[expectedSequenceNumber], sizeof(int), outOfOrderData, 0, outOfOrderData.Length);

            // Process the received out-of-order data
            ProcessData(outOfOrderData);

            // Remove the processed out-of-order packet from the dictionary
            sentPackets.Remove(expectedSequenceNumber);

            // Increment the expected sequence number for the next packet
            expectedSequenceNumber++;
        }
    }
    else
    {
        // Packet is out of order, store it for later processing
        sentPackets[sequenceNumber] = packet;
    }
}


Basic RUDP Server

This example demonstrates the server-side implementation of RUDP in Unity using C#. The RUDPServer class receives data packets from the client and checks for duplicate packets. If a packet is not a duplicate, it stores it for processing and checks if there are any out-of-order packets waiting to be processed.


using System.Collections.Generic;
using UnityEngine;

public class RUDPServer : MonoBehaviour
{
    private Dictionary<int, byte[]> receivedPackets = new Dictionary<int, byte[]>();
    private int nextSequenceNumber = 0;

    // Receive data from the client
    private void ReceiveData(byte[] packet)
    {
        // Extract the sequence number from the packet
        int sequenceNumber = BitConverter.ToInt32(packet, 0);

        // Check if the packet has already been received
        if (!receivedPackets.ContainsKey(sequenceNumber))
        {
            // Store the packet for processing
            receivedPackets[sequenceNumber] = packet;

            // Check if there are any out-of-order packets waiting
            while (receivedPackets.ContainsKey(nextSequenceNumber))
            {
                // Process the data from the out-of-order packet
                byte[] data = receivedPackets[nextSequenceNumber];

                // Process the received data
                ProcessData(data);

                // Remove the processed out-of-order packet from the dictionary
                receivedPackets.Remove(nextSequenceNumber);

                // Increment the next expected sequence number
                nextSequenceNumber++;
            }
        }

        // Send an acknowledgement packet to the client
        SendAcknowledgement(sequenceNumber);
    }

    // Process the received data
    private void ProcessData(byte[] data)
    {
        // Process the received data here
        Debug.Log("Received data: " + System.Text.Encoding.UTF8.GetString(data));
    }

    // Send an acknowledgement packet to the client
    private void SendAcknowledgement(int sequenceNumber)
    {
        // Create the acknowledgement packet
        byte[] acknowledgement = BitConverter.GetBytes(sequenceNumber);

        // Send the acknowledgement packet to the client using UDP
        // UDP implementation code goes here
    }
}


The ProcessData method handles the received data. The server also sends an acknowledgment packet to the client to confirm the successful receipt of a packet.


Implementations

When it comes to developing your own version of RUDP, there are a lot of complexities to consider, so it's easier to use a proven third-party solution. There are several RUDP implementations available for Unity. Let's take a look.

LiteNetLib

LiteNetLib is a reliable lightweight library for .NET Standard 2.0 that can be used in Mono, .NET Core, and .NET Framework.


It offers a range of features without overloading the processor or using a lot of memory. Data packets have a small size (only 1 byte for unreliable and 4 bytes for reliable packets). The library provides simple mechanisms for handling connections and supports peer-to-peer connections.


LiteNetLib includes helper classes for sending and reading messages, supports multiple data transmission channels, and various sending mechanisms. Data transmission can be reliable considering order, reliable without considering order, reliable considering sequence (only the last packet), ordered but unreliable with duplicate prevention, and simple sending of UDP packets without considering order and reliability.


Additional features include a fast packet serializer, automatic merging of small packets, automatic fragmentation of reliable packets, automatic determination of MTU, optional CRC32C checksums, NAT punching for UDP, NTP time requests, IPv6 support, packet loss, and delay simulation, and connection statistics collection.


LiteNetLib supports numerous platforms, including Windows, Mac, Linux (.NET Framework, Mono, .NET Core, .NET Standard), Lumin OS (Magic Leap), Monogame, Godot, and Unity 2018.3 (Desktop platforms, Android, iOS, Switch). The library also offers support for multicast, which is useful for finding hosts in a local network.


Ruffles

Ruffles is a fully managed UDP library designed for high performance and low latency.


One of the distinctive features of Ruffles is its connection calling system, which prevents DOS attacks and slot filling. The library manages connections, ensuring high performance without creating garbage, making it an excellent choice for performance-intensive applications.


Ruffles also supports working with channels and uses multithreading by default. The library is thread-safe and has no external dependencies, making it easy to integrate into various projects.


It supports both IPv4 and IPv6 in dual mode, allowing both technologies to be used together. Ruffles also has a feature for merging small packets and fragmentation, as well as merging acknowledgments, which improves data transmission efficiency.


With Ruffles, you can collect connection statistics, detect MTU along the route, and track network bandwidth.

Hazel Networking

Hazel Networking is a low-level networking library for C# that provides connection-oriented, message-based communication over UDP and RUDP. The goal of this fork is to create a simple interface for ultra-fast UDP-based communication for games.


It supports UDP and reliable RUDP, as well as packet encryption using DTLS for increased security. Hazel Networking also offers the ability to broadcast UDP for local multiplayer.


The library is fully thread-safe, and all of its protocols are connection-oriented, like TCP, and message-based, like UDP. It supports both IPv4 and IPv6.


In addition, Hazel Networking automatically collects statistics on data flowing in and out of connections, and is designed to be as fast and lightweight as possible.


ENet-CSharp

ENet-CSharp is an independent implementation of ENet with a modified protocol for C# and other languages. This implementation is characterized by its simplicity and efficiency, while consuming few resources. It supports dual a IPv4/IPv6 stack, making it versatile for use in various network conditions.


Connection management is also implemented, which ensures reliability and orderliness of data transmission. Data can be transmitted through different channels, which increases communication efficiency.


The benefit of this ENet implementation is that it provides data exchange reliability by implementing packet fragmentation and reassembly. This allows for efficient operation even when transmitting large amounts of data.


Through aggregation, the system can combine multiple data packets into one, which increases transmission efficiency. The adaptability and portability of the implementation make it easy to integrate and use in various systems and applications.


kcp2k

Kcp2k is a C# implementation of KCP based on the original version of KCP in C. It is compatible with netcore and Unity, and is designed to work with Mirror Networking.


KCP is a protocol that is both fast and reliable: it can reduce average delay by 30%-40%, and reduce maximum delay by three times. However, KCP requires 10%-20% more bandwidth than TCP.

KCP is implemented using a pure algorithm, and it does not handle sending and receiving the underlying protocol (e.g. UDP). Therefore, users must define their own method of transmission for the base data packet and provide it to KCP as a callback. Additionally, external transmission of clocks is necessary as there are no internal system calls for this purpose.


In addition to the main features, kcp2k offers additional high-level C# code for managing client and server connections, as well as an additional Unreliable channel.


Unity Transport Package

Unity Transport Package (UTP) is a networking library specifically created for developing multiplayer games. It functions as the core protocol for both Netcode for GameObjects and Netcode for Entities, but it can also be utilized with a custom solution.

Thanks to its connection-based abstraction layer provided through UDP and WebSockets, Unity Transport supports all platforms compatible with the Unity engine. You can configure both UDP and WebSockets with or without encryption.


Wrapping up

In this part of the series, we took a look at ensuring dependable data transmission while using the untrustworthy UDP protocol. We now know about the most prevalent implementations and their functionalities. We discussed libraries that offer useful utilities for utilizing RUDP and incorporating it into multiplayer games.


In the next article of this series, we'll talk about why connecting multiple devices located at different parts of the world is not a straightforward process, despite having a complete range of libraries and protocols available to us.