UNET - New Network Technology in Unity 3D

Original author: Erik Juhl
  • Transfer
Some time ago, at the Unite Asia conference, we announced the development of new multiplayer tools, technologies and services for Unity developers. The internal name of this project is UNET, which simply means Unity Networking. But our plans extend far beyond simple networking. As you all know, Unity's main goal is to democratize the game development process. The Unity Networking team wants to democratize the development of multiplayer games. We want all game developers to be able to develop multiplayer games of any type with any number of players. Of course, this is not the easiest task, but we have all solved it in the past and really want to do it again (because it is really cool!). We decided to divide our common goal into several phases, which should be familiar to Unity developers. According to this approach, we will release phase 1, get user feedback, take them into account in our work, to make the next phase even better and repeat this cycle. For UNET, phase 1 will be what we call the Multiplayer Foundation, which we will talk about a little later. Phase 2 will be built on the basis of phase 1 and will provide technology for creating games with authorization on the server, which we call Simulation Server, about it in the following articles. In phase 3, we will add the ability to coordinate multiple Simulation Servers using the Master Simulation Server system. As always, the exact release date is impossible to name, especially given the collection of feedback from our users. But we can say that phase 1 will be part of the 5.x release cycle, and phase 2 is now at the research stage. we will take them into account in our work in order to make the next phase even better and repeat this cycle. For UNET, phase 1 will be what we call the Multiplayer Foundation, which we will talk about a little later. Phase 2 will be built on the basis of phase 1 and will provide technology for creating games with authorization on the server, which we call Simulation Server, about it in the following articles. In phase 3, we will add the ability to coordinate multiple Simulation Servers using the Master Simulation Server system. As always, the exact release date is impossible to name, especially given the collection of feedback from our users. But we can say that phase 1 will be part of the 5.x release cycle, and phase 2 is now at the research stage. we will take them into account in our work in order to make the next phase even better and repeat this cycle. For UNET, phase 1 will be what we call the Multiplayer Foundation, which we will talk about a little later. Phase 2 will be built on the basis of phase 1 and will provide technology for creating games with authorization on the server, which we call Simulation Server, about it in the following articles. In phase 3, we will add the ability to coordinate multiple Simulation Servers using the Master Simulation Server system. As always, the exact release date is impossible to name, especially given the collection of feedback from our users. But we can say that phase 1 will be part of the 5.x release cycle, and phase 2 is now at the research stage. what we call - the Multiplayer Foundation - we will talk about it below. Phase 2 will be built on the basis of phase 1 and will provide technology for creating games with authorization on the server, which we call Simulation Server, about it in the following articles. In phase 3, we will add the ability to coordinate multiple Simulation Servers using the Master Simulation Server system. As always, the exact release date is impossible to name, especially given the collection of feedback from our users. But we can say that phase 1 will be part of the 5.x release cycle, and phase 2 is now at the research stage. what we call - the Multiplayer Foundation - we will talk about it below. Phase 2 will be built on the basis of phase 1 and will provide technology for creating games with authorization on the server, which we call Simulation Server, about it in the following articles. In phase 3, we will add the ability to coordinate multiple Simulation Servers using the Master Simulation Server system. As always, the exact release date is impossible to name, especially given the collection of feedback from our users. But we can say that phase 1 will be part of the 5.x release cycle, and phase 2 is now at the research stage. In phase 3, we will add the ability to coordinate multiple Simulation Servers using the Master Simulation Server system. As always, the exact release date is impossible to name, especially given the collection of feedback from our users. But we can say that phase 1 will be part of the 5.x release cycle, and phase 2 is now at the research stage. In phase 3, we will add the ability to coordinate multiple Simulation Servers using the Master Simulation Server system. As always, the exact release date is impossible to name, especially given the collection of feedback from our users. But we can say that phase 1 will be part of the 5.x release cycle, and phase 2 is now at the research stage.



Before joining Unity, our network team members worked mostly on MMOs like Ultima Online, Lord of the Rings Online, Dungeons and Dragons Online, Marvel Heroes, Need for Speed ​​Online and World of Warcraft. We have a lot of enthusiasm and vast experience in creating multiplayer games, technologies and infrastructure. The mission of Unity was known to all of us and always seemed very attractive. We could not refuse the opportunity to do something truly great, such as the realization of this dream in the field of multiplayer. So we left our previous jobs and joined Unity to make this dream a reality. Now we are working hard on these tools, technologies and services so that anyone can realize their dream of a multiplayer game.

So what do we mean by the Multiplayer Foundation in Phase 1? Here are its main parts:

- a high-performance UDP-based transport protocol that supports all types of games

- a Low Level API (LLAPI) that provides full control through a socket-like interface

- a High Level API (HLAPI) that provides a simple and secure model client / server

- Matchmaker Service, which provides basic functionality for creating rooms and helping players find each other

- Relay Server, which solves communication problems between players trying to connect via firewalls

Given some historically established limitations and a grandiose goal, it became obvious to us that we would have to start from scratch. Since our goal was to support all types of games and any number of connections, we started with a new, high-performance transport layer based on UDP. We are aware that many games have enough TCP, but fast games still need UDP, since TCP delays the last packets if they arrive out of order.

Based on this new transport layer, we built two APIs. The high-level High Level API (HLAPI), provides a simple and secure client-server model. If you are not a network engineer and just want to make a multiplayer game, you will be interested in HLAPI.

We also took into account reviews of the old system: some users wanted to get low-level access for more control. Now it has a low-level Low Level API (LLAPI), which provides a more socket-like interface for the transport layer. If you are a network engineer and want to build your own network model or just fine-tune network performance, then LLAPI will interest you.

Matchmaker's Matching Service is used to set up rooms in your multiplayer games and help players find each other. Relay Server ensures that your players can always connect with each other.

We have seen from our own experience that creating multiplayer games brings a lot of pain. The Multiplayer Foundation is a new set of easy-to-use, professional networking technologies, tools and infrastructure for the painless creation of multiplayer games. It seems to me that it can be said that the creation of a multiplayer game requires a good knowledge of networks and protocols. You either overcome a painfully steep learning curve yourself, or you are looking for a network engineer. After going through this, you have to solve the problem of providing players with the means to search for each other. Having solved this problem, you have to deal with providing players with the ability to connect with each other, which can be very difficult if they are behind firewalls with NAT. To cope with all this, you will have to create a decent-sized infrastructure, which is not particularly nice and has nothing to do with game development. After that, you will have to think about dynamically scaling your infrastructure, the correct implementation of which usually requires some experience.

Phase 1 will save you from all these painful problems. HLAPI will eliminate the need for a deep understanding of network technology. But if you are a network engineer and want to do everything in your own way, then LLAPI will always be available for you. Matchmaker will solve your problems by providing players the opportunity to find each other. Relay Server will solve your problems of providing players with the opportunity to really connect with each other. We will also solve your problems of building the necessary infrastructure and its dynamic scaling. Matchmaker and Relay Server will live in the Unity Multiplayer Cloud. So not only physical servers, but also processes will be scaled depending on demand.

High Level API UNET and SyncVar


Introduction and requirements


Some background information. It is common practice for network games to have a server that owns the objects and clients who need to be informed that the data in these objects has changed. For example, in a combat game, the player’s life should be visible to all players. This requires a member-variable in the script class, which is sent to all clients when it changes on the server. Here is an example of a simple battle class:

class Combat : MonoBehaviour
{
    public int Health;
    public bool Alive;
    public void TakeDamage(int amount)
    {
        if (amount >= Health) {
            Alive = false;
            Health = 0;
        } else {
            Health -= amount;
        }
    }
}


When a player on the server receives damage, all players must be informed of the new life value for that player.

It seems simple, but the difficulty is to make the system invisible to developers writing code, effective in terms of CPU load, memory and bandwidth, and flexible enough to support all types that developers will want to use. So the specific goals for this system will be:

1. Minimize memory usage without storing shadow copies of variables

2. Minimize bandwidth usage by sending only those states that have really changed (incremental updates)

3. Minimize CPU usage, without constantly checking, it has changed whether the condition

4. Minimize protocol and serialization discrepancies without forcing developers to manually write serialization functions

5. Do not require developers to directly mark variables as dirty

6. Work with all Unity-supported programming languages

7. Do not violate the existing development process

8. Do not enter manual steps that developers would have to do to use the system

9. Allow the system to be guided meta data (custom attributes (the attributes custom))

10. Handle and simple and complex type

11. Do not use reflection at runtime.

A very ambitious list of requirements!

Old network system


The existing Unity network system has a “ReliableDeltaCompressed” type of synchronization that performs state synchronization by providing the OnSerializeNetworkView () function. This function is embedded in objects with the NetworkView component and the serialization code written by the developer is written to (or read from) the provided byte stream. The contents of this byte stream are cached by the engine and if the next time the function is called, the result does not match the cached version, the object is considered dirty and its state is sent to clients. Here is an example of a possible serialization function:

void OnSerializeNetworkView (Bitstream stream, NetworkMessageInfo info)
{
    float horizontalInput = 0.0f;
    if (stream.isWriting) {
        // Sending
        horizontalInput = Input.GetAxis ("Horizontal");
       	stream.Serialize (horizontalInput);
    } else {
        // Receiving
        stream.Serialize (horizontalInput);
        // ... do something meaningful with the received variable
    }
}


This approach meets some of the requirements from the list above, but not all. At runtime, it works automatically, since OnSerializeNetworkView () is called by the engine with a frequency of sending data over the network, and the developer does not need to mark the variables as dirty. It does not add any additional steps to the assembly process and does not interrupt the existing development process.

But its performance is not very high - especially when there are a lot of network objects. CPU time is spent on comparisons, and memory is used on cached copies of byte streams. It is also prone to mismatch errors in serialization functions, since they must be updated manually when adding new member variables that need to be synchronized. It is not guided by metadata, so the editor and other tools cannot find out which variables are synchronized.

Code Generation for SyncVars


While working on a new state synchronization system in UNET, our team worked out a solution with code generation based on custom attributes. In user code, it looks like this:

using UnityEngine.UNetwork;
class Combat : UNetBehaviour
{
    [SyncVar]
    public int Health;
    [SyncVar]
    public bool Alive;
}


This new custom attribute tells the system that the Health and Alive instance variables need to be synchronized. Now the developer does not need to write a serialization function, since the code generator has data from custom attributes, based on which it can generate excellent serialization and deserialization functions with the correct order and types. The generated functions will look something like this:

public override void UNetSerializeVars(UWriter writer)
{
    writer.WriteInt(Health);
    writer.WriteBool(Alive);
}


Since this function overrides the virtual function in the UNetBehaviour base class, when serializing a game object, script variables will be serialized automatically. After that, they will be unpacked at the other end using the appropriate deserialization function. Mismatches are not possible, because when a new [SyncVar] variable is added, the code is updated automatically.

This data is now available to the editor, so the inspector window can show more information:



But with this approach, a number of problems still remain. The function always sends the entire state - it is not incremental, so when you change one member of the object, the state of the entire object will be sent. And how do we know when to call the serialization function? It is not very efficient to send a state if nothing has changed.

We overcame this with the help of properties and dirty labels. It seems natural that each [SyncVar] variable can be wrapped in a property that will put dirty labels when it changes. This approach has been partially successful. The presence of a bitmask with dirty marks allowed the code generator to perform incremental updates. The generated code began to look like this:

public override void UNetSerializeVars(UWriter writer)
{
    Writer.Write(m_DirtyFlags)
    if (m_DirtyFlags & 0x01) { writer.WriteInt(Health); }
    if (m_DirtyFlags & 0x02) { writer.WriteBool(Alive); }
    m_DirtyFlags = 0;
}


Now the serialization function can read the mask with dirty labels and serialize only those variables that need to be written to the stream. We get efficient use of bandwidth and the ability to find out if an object is dirty. For the user, this is still fully automatic. But how will these properties work?

Suppose we are trying to wrap [SyncVar] instance variables:

using UnityEngine.UNetwork;
class Combat : UNetBehaviour
{
    [SyncVar]
    public int Health;
    // generated property
    public int HealthSync {
        get { return Health; }
        set { m_dirtyFlags |= 0x01;  Health = value; }
    }
}


Such a property fulfills the task, but has an invalid name. The TakeDamage () function in the above example uses Health, not HealthSync, so it will ignore the property. The user will not be able to directly use the HealthSync property at all, since it does not exist before the code generation is executed. It would be possible to execute it in two steps, when at the first stage an automatic code is generated, and at the second the user updates his code - but this is very fragile. This approach is subject to compilation errors that cannot be fixed without rewriting large pieces of code.

Another option would be for developers to write the above properties for each [SyncVar] variable. This approach adds work to programmers and is potentially error prone. The bit masks in the user and generated code must match exactly, so adding or removing [SyncVar] variables will be an extremely delicate process.

Introducing Mono Cecil


Thus, we need to generate wrapper properties and force the source code to use them even if it does not suspect their existence. Fortunately, there is a tool for Mono called Cecil that does just that. Cecil is able to download Mono assemblies in ECMA CIL format, modify them and write them back.

At this moment, everything becomes a little crazy. The UNET code generator creates wrapper properties, then it finds all the places in the code where the source variables are used, and then replaces the references to these variables with references to the wrapper properties and voila! User code now invokes freshly created properties without requiring any work from the user.

Since Cecil works at the CIL level, there is an additional advantage in the form of support for all languages, since they are all compiled into one format.

The generated CIL for the final serialization, which is inserted into the assembly with the script, now looks like this:

IL_0000: ldarg.2
IL_0001: brfalse IL_000d
IL_0006: ldarg.0
IL_0007: ldc.i4.m1
IL_0008: stfld uint32 [UnityEngine]UnityEngine.UNetBehaviour::m_DirtyBits
IL_000d: nop
IL_000e: ldarg.1
IL_000f: ldarg.0
IL_0010: ldfld uint32 [UnityEngine]UnityEngine.UNetBehaviour::m_DirtyBits
IL_0015: callvirt instance void [UnityEngine]UnityEngine.UNetwork.UWriter::UWriteUInt32(uint32)
IL_001a: ldarg.0
IL_001b: ldfld uint32 [UnityEngine]UnityEngine.UNetBehaviour::m_DirtyBits
IL_0020: ldc.i4 1
IL_0025: and
IL_0026: brfalse IL_0037
IL_002b: ldarg.1
IL_002c: ldarg.0
IL_002d: ldfld valuetype Buf/BufType Powerup::mbuf
IL_0032: callvirt instance void [mscorlib]System.IO.BinaryWriter::Write(int32)
IL_0037: nop
IL_0038: ldarg.0
IL_0039: ldc.i4.0
IL_003a: stfld uint32 [UnityEngine]UnityEngine.UNetBehaviour::m_DirtyBits
IL_003f: ret


Fortunately, ILSpy can convert CIL to C # and vice versa, so that we can see the generated CIL as C #. ILSpy is a great Mono / .Net build tool. C # looks like this:

public override void UNetSerializeVars(UWriter writer, bool forceAll)
{
    if (forceAll)
    {
        this.m_DirtyBits = 4294967295u;
    }
    writer.UWriteUInt32(this.m_DirtyBits);
    if ((this.m_DirtyBits & 1u) != 0u)
    {
        writer.Write((int)this.mbuf);
    }
    this.m_DirtyBits = 0u;
}


Let's see how this meets our requirements:

1. There are no shadow copies of variables

2. Incremental updates

3. There are no checks for changing the state

4. There are no hand-written serialization functions

5. There are no direct checks for dirt 6. No explicit dirty calls

6. Works with all programming languages ​​supported by Unity

7. Does not affect the usual development process

8. Does not require manual work from the developer

9. Based on metadata

10. Works with all types (with new UWriter / UReader serializers)

11. Does not use reflection at run time niya

It seems like they are all done. The system will be efficient and developer friendly. I want to believe that it will simplify the development of multiplayer games on Unity for everyone.

We also use Cecil to implement RPC calls to avoid searching for functions by name using reflection. We will discuss this in future articles.

Low-Level LLAPI and UNET Transport Layer


Starting to design a new network library for Unity, we wanted to understand what an ideal library should look like. We found that we (roughly speaking) have two types of users:

1. Users who want network tools to give them a ready-to-use result with minimal effort (ideally without any effort).

2. Users who develop network-oriented games and want very flexible and powerful tools.

Based on these two types, we divided our network library into two different parts: high-level HLAPI (high-level API) and low-level LLAPI (low-level API).

In this part, we will talk about the low-level API and the structure of the library, which are based on the following principles:

Performance, performance, performance


LLAPI is a thin layer on top of UDP sockets, most of the work is done in a separate stream (therefore, LLAPI can be configured to use only the main stream). There is no dynamic memory allocation and no heavy synchronization (most of the library uses synchronization based on memory barrier synchronization with a small number of atomic increment / decrement operation).

If something can be done in C #, then you need to do it


We decided to give access only to what our users really need. Like BSD sockets, LLAPI supports only one abstraction - raw binary messaging. It has no tcp-like streams, serializers or RPC calls, only low-level messages.

Flexibility and customizability? Yes please...


If you look at the socket implementation in TCP, you will see a lot of parameters (timeouts, buffer length, and so on) that you can change. We chose a similar approach and allowed users to change almost all the parameters of our library so that they could adjust them to their needs. Faced with the choice between simple and flexible, we chose flexibility.

Simplicity and pleasantness


We tried to design LLAPI as similar as possible to the BSD socket API.

Network and transport layers


Logically, the low-level UNET library is a set of network protocols built on top of UDP that includes a “network” layer and a “transport” layer. The network layer is used for connecting between nodes, delivering packets and controlling possible flow and congestion. The transport layer works with “messages” belonging to various communication channels.



Channels have two purposes: they can separate messages logically and provide different delivery guarantees and quality of service (delivery grants or quality of service).

Channel tuning is part of the tuning procedure, which we will cover in future articles. For now, let's just assume that the setup looks like “My system will contain up to 10 connections, each connection will have 5 channels, channel 0 will have this type, channel 1 will have a different type, and so on.” The last part of the proposal is defined as: The



second parameter is the channel number, the last is the type of channel or the quality of service of the channel (delivery grant).

UNET (at the moment) supports the following QOS:

- Unreliable : an unreliable message that may be lost due to network problems or internal buffer overflows, similar to a UDP packet. Example: short log entries.

- UnreliableFragmented:The maximum packet length is unchanged, but at times you will most likely want to send “large” messages. This type of channel before sending will parse your messages into fragments and collect them back before receiving. Since such a quality service is unreliable, delivery is not guaranteed. Example: a long magazine.

- UnreliableSequenced: The channel provides the delivery order, but since this quality of service is unreliable, the message may be lost. Example: voice, video.

- Reliable: The channel guarantees delivery (or disconnection), but does not guarantee order. Example: transferring damage.

- ReliableFragmented: the same as UnreliableFragmented, but in addition to it guarantees delivery. Example: group damage.

- ReliableSequenced:same as UnreliableSequenced, but additionally guarantees delivery. This QOS is similar to a TCP stream. Example: transferring files and patches.

- StateUpdate: unreliable channel type, forcibly dropping packets older than received / sent. If the buffer contains more than one message during transmission, only the latest will be sent. If the receiver’s buffer contains more than one message while reading, only the most recent will be delivered. Example: location transfer.

- AllCostDelivery:very similar to Reliable, but there are differences. A reliable channel will re-send messages based on the round trip time value (RTT), which is determined dynamically, while AllCostDelivery will automatically send messages after a certain period of time (specified in the settings). This can be useful for small but important messages: "I hit the head of player A" or "The mini-game begins." Example: game events like bullets flying out.

Let's look at a typical LLAPI function call:

1. Initializing the library



2. Network setup: topology, number of channels, their types, various timeouts and buffer sizes (this will be discussed in other articles).

3. Creating a socket



This function will open a socket on port 5000 on all network interfaces and return an integer value as a description of socket

4. Connect to



another host This function will send a connection request to another host at 127.0.0.1/6000. It will return an integer value as a description of the connection to this node. You will receive a connection event when the connection is established or a disconnection event if the connection cannot be established.

5.



Send the message. The last function will send the binary data contained in the buffer through the socket described in hostId for the connectionId describing the node using channel 1 (in our case it is a “reliable channel”, so message delivery will be guaranteed)

6. Receive network events

To receive network events, we select a survey model. The user must poll the UTransport.Receive () function in order to receive notifications of network events. Note that this model is very similar to a regular select () call with a zero timeout. This function receives 4 different events

UNETEventType.kConnectEvent - someone connects to you or a connection is successfully established via UTransport.Connect ()

UNETEventType.kDisconnectEvent - someone disconnects from you or the connection requested using UTransport.Connect (), cannot be installed for some reason that the error code will report.

UNETEventType.kDatatEvent - new data

received UNETEventType.kNothing - nothing interesting happened



7. Send a request to disconnect.

This function call will send a request to disconnect from connectionId to a host with hostId. The connection will be immediately closed and may be reused in the future.



Notes


1. The article is composed of three entries on the Unity English blog:

Announcing UNET - New Unity Multiplayer Technology
UNET SyncVar
All about the Unity networking transport layer

2. Examples of the source code in the form of pictures were in the original article in English.

Also popular now: