Unity Networking
In this article I will introduce the networking functionality included in Unity. I will build a simple authorative server solution, introduce the NetworkView component, and show you how RPCs can be used. The screenshots are from Unity 3.5, but the solution works for Unity 4.0 as well.
Table of Contents
Introduction
A brief history of networking
Peer to Peer Lockstep
Authorative Server/Dumb client
Client Side Prediction
Lag Compensation
Non authorative server
A Unity server
Client and server projects
Project wizard
Run in background
A Unity client
Your First Networking Solution
Network View
Observed
State Synchronization
Network Instantiate
Authorative Server
Server Project
Shared Unity package
Spawn players
Client Project
Second Networking Solution
Spawning madness
RPCs
Buffered RPCs
Restrictions on RPCs
Improved Spawning
Clean up client side
Clean up server side
Run the solution
Movement
Server project
Client project
Final run
Download
Introduction
If you are planning to make a multiplayer game in Unity, be it two player turn based or massive multiplayer online, you will have to implement some sort of networking solution capable of ensuring that all players share the same consistent world view. This will require the handling of problems such as:
players playing on differing hardware platforms (cpu/memory), such as pcs and mobiles
players with differing connection speeds
differing network latency between players (round trip time), and differences in the variation thereof (jitter)
lossy connections and the handling of lost data
For real time games the above problems are especially relevant and if not handled correctly will result in players experiencing “lag”.
A brief history of networking
Before diving into the networking solution that Unity offers, lets briefly look at the history of multiplayer games and their networking solutions.
Peer to Peer Lockstep
Fully connected star topology
Initially, each computer exchanged game information with each of the other computers in a star topology. The game consisted of “turns” and in each turn a computer could choose from a limited number of “moves”. Once all the computers had communicated their next move to all the other computers, each computer carried out the moves after which it was time for the next turn. Assuming that all computers started from the same initial game state, this would ensure that all games played out identically across computers. The disadvantage of this networking solution is that each computer has to wait until it has received the move from the last computer, the slowest computer thus determing the pace of the turns (and thus lag). Real Time Strategy games typically use variants of peer to peer lockstep, albeit with better handling of the lag issues.
Authorative Server/Dumb client
Star topology for client-server
To avoid having to wait on the slowest computer, each computer now communicates with a single server. The server has the game “world” and the clients just receive a view on this world. The clients have (almost) no code, they are just dumb terminals sending input (key presses) to the server. The server then carries out the input and sends back a “picture” of the resulting world. There is no longer a problem of keeping the game world consistent across computers as there is only one world (on the server). The solution works fine for computers with the same low network latency but begins to fall apart when players have higher, differing latency. For players with a high latency, the game quickly become unplayable, the world moving in jarring jumps. Doom worked like this and if you had a high latency then most chances were you were dead before you had a chance to see what hit you.
Client Side Prediction
To smoothen the game experience, clients no longer wait until they receive a view back from the server. They take the players input and predict what will happen, immediately updating the world view accordingly. This requires the client to know about the game mechanics and game objects, thus the client is no longer dumb. The resulting game play is much more responsive. Of course, a problem occurs when the server world view comes back and differs from what the client predicted. The world view is then yanked back in line with the server view, often causing a noticeable glitch. This solution was first introduced in Quakeworld.
Lag Compensation
Client side prediction and lag compensation
Reverting to the server state when a client prediction is off, actually means that the world view of the client jumps back in time. To understand this, say that it took 200 ms for the client input to reach the server, and another 200 ms for the resulting world view to come back from the server. The client has in the mean time moved forward 400 ms in time using it’s own predictions. If the client has to revert to the server view resulting from it’s input 400ms ago, it is thus jumping back in time to the time of that input. To avoid this, the client would have to first rewind time and then repredict forward in time to reach the current time again. If the game play mechanics are reasonably deterministic then corrections will occur rarely as server and client are calculating the same world view. Input from other players is basically the only cause for correction, such as the firing of a rocket point blank at the back of the head. This solution results in smooth, responsive gameplay and was first introduced in CounterStrike.
Non authorative server
As a side note, clients can be authorative in that they are allowed to process the players inputs and update the world themselves. In this case, the server does not own the world, it just serves to update the world according to views from it’s clients and distribute these views to the other players. While this ensures that game play is responsive, it can lead to clients having different conflicting world states requiring solutions to resolve these conflicts. It also introduces the risk of cheating clients, one player claiming to have shot another when he didn’t even have a gun, for example. This networking solutions is seldom (if ever) used for games. Sometimes a peer to peer solution, with one of the clients being both server and client, is called a non authorative server, but typically the client-server is authorative.
Reference: What every programmer needs to know about game networking
A Unity server
To understand the networking solution that comes with Unity, let’s start by building a server. Create a new, empty Unity project and add a script called ServerMain.js with the following code:
The Network class is the main networking class in Unity. The function InitializeServer() starts up the server, taking the maximum number of clients allowed:
as well as the port to listen to for incoming clients:
The last parameter to the Network.InitializeServer() function (false in the code above) indicates whether or not the server should use NAT punchthrough to enable clients to connect with it. See the Network Overview for more details. For now, leave this at false.
The function Network.Disconnect() stops the server.
To complete the server side script, add the following UI code to the same script file:
This code snippet uses the startServer() and stopServer() functions we defined above and adds some buttons to start/stop the server. It uses the Network.peerType property to determine the status of the connection and shows this on the screen. The value of the peerType can be:
Disconnected : No client connection running. Server not initialized
Server : Running as server
Client : Running as client
Connecting : Attempting to connect to a server
The code also uses
A NetworkPlayer is a datastructure containing information about a client (such as the ip address and port). Network.connections returns a list of all connected NetworkPlayers.
To finish the server side, create an empty GameObject and add the ServerMain.js script to it. You should now be able to start and stop the server.
Client and server projects
Before we build the client side code, we first need to configure Unity to allow both the client and the server projects to run on the same pc. This is easy for development and debugging purposes.
Project wizard
Unity does not allow the same project to be opened more than once. It also automatically reopens the last project you edited. This combination means that if you have any project open in Unity, clicking on Unity again will result in an error message. This can be avoided by selecting Edit > Preferences from the main menu in the Unity editor and then check “Always Show Project Wizard“.
Unity – Preferences
Unity will now no longer automatically reopen the last project, but allow you to select the project yourself on start up. You should now be able to open both a server project and a client project.
Run in background
By default, Unity will only run the project when the game view is active (the mouse is in the view for example). If not active, the code is paused. This is a nuisance if you have a server and a client project open at the same time and are switching between them for development/debugging purposes. To get around this, select Edit > Project Settings > Player from the main menu in the Unity editor, and select Run In Background in the PlayerSettings inspector.
Unity – Player Settings
If you now run the server project, it will not stop running when the mouse leaves the game view screen. Remember to do this for both the client and the server projects.
Note that as an alternative, bulding the server (.exe) and the client (.exe) projects and running the resulting executable also allows you to run both client and server at the same time on the same pc.
A Unity client
Now that we have a server, let’s build a client to connect to it. Create a new Unity project and add a script called ClientMain.js with following code:
Again we use the Network class. The function Connect() tries to establish a connection to the server. The first parameter is the (remote) IP address of the server, which in the code above is set to:
This is the default ip for localhost (the pc you are running on). This will work fine if the server is also running the same pc, but otherwise this should be set to the actual global ip address of the server.
The second parameter is the (remote) port of the server and should be identical to the listenPort in the ServerMain.js script.
The function Disconnect() is used to disconnect from the server.
To complete the client side script, add the following UI code to the same script file:
This code snippet uses the connectToServer() and disconnectFromServer() functions defined above. In addition, it adds some buttons to connect/disconnect to the server, and shows some status information using the Network.peerType. To complete the client, create a new empty GameObject and add the ClientMain.js script to it.
Your First Networking Solution
Run the Server project and press the “Start Server” button. Now run the Client project and press the “Connect to Server” button.
Screenshot of client connected to server
If you are seeing something comparable to the above screenshot then congratulations, you have successfully built your first networking solution!
Network View
The client server example above allows a connection to be established between server and client, but it doesn’t yet do anything useful. To improve, let’s first discuss the Unity Network View component.
Unity Network View
The Network View component can be added to any GameObject. It allows state information about the GameObject to be communicated across the network. It has the following key properties:
Observed
This property determines the Component of the GameObject for which information will be sent over the network. This is a drop down allowing either a Transform, an Animation, a RigidBody, or a script to be selected. For the selected Component, all the state information required to “recreate” the state of the Component on a different client is sent across the network automatically (by default information is sent 15 times per second). Unity handles all of this information sending and recreating for you, thereby ensuring that all clients see the same state.
State Synchronization
This property determines how information about the GameObject will be sent over the network. One option is Unreliable which means that all the state information about the Observed component will be sent every time (15 times per second). It is called unreliable because if the information is lost somewhere in the network (due to packets being dropped by a router for example) then it is not resent. For a real time racing game, such loss is quite acceptable as any resent information will be too late anyway. As long as the next packets contain the complete state again, the game can recover.
Another option is Reliable Delta Compressed. In this case, not the entire state information is sent every time but only the information that has changed since the last send (hence “delta”). It is called reliable because any information lost in the network is resent and is guaranteed to arrive at all clients. This is necessary because if any change packets are lost then the state on the remote client goes out of sync and any subsequent change packets are unusable. Sending only changes instead of the total state saves on bandwidth but the resending introduces delay, making it less suitable for real time games.
Network Instantiate
Before showing how the Network View works in some sample code, there is another important function in the Network class to discuss. The Network.Instantiate() function allows a prefab instance to be created on all connected clients (note the prefab must have a Network View). The syntax is:
Besides the prefab, the function also requires the position and rotation of the prefab.
The last parameter denotes the Communication Group. This allows clients to be split into different groups and the prefab would only be instantiated on clients in that group. This aovids having clients that are nowhere near each other in the game exchanging useless information.
Instantiate() can be called by any client and/or the server, but whoever makes the call “owns” the instance. This means that if Instantiate() is called on a client, this client is authorative for that instance and can change the position and rotation etc. Other clients can cheat by modifying the position of the instance, but these chances are only local to that client and are not communicated across the network (and are overwritten by changes made by the owner).
We will be building an authorative server solution, so all calls to Instantiate need to be made on the server.
Authorative Server
To demonstrate the Network View and Network.Instantiate(), let’s extend our client-server solution so that a player gameobject is spawned every time a new client connects to the server.
Server Project
In the server project, create a folder called “Server” in the Project View and drag the script ServerMain.js into this folder. This folder will contain everything that is needed only on the server side. Also, delete the GameObject from the scene.
Shared Unity package
Create a second folder called “Game“. This folder will contain everything shared with the client project. Save the scene and call it “Game“. Drag the “Game.unity” scene into the “Game” folder.
Add a Plane GameObject to the scene with Position (0,0,0), Rotation (0,0,0), and Scale (1,1,1). Make sure the Main Camera shows the plane, for example Position (0,1,-10) and Rotation (0,0,0). Add a Directional light to the camera and set the Directional light to Position (0,0,0) and Rotation (0,0,0).
Add an empty GameObject called “GameController” to the scene. Select the GameController and add a NetworkView component by selecting Component > Miscellaneous > Network View from the main menu. In the Inspector, set the State Synchronization property to “Off“, and the Observed property to “None“. This GameObject will serve as a global placeholder used to attach scripts. Why we need a NetworkView here that is doing “nothing” will be explained later on.
None/Off NetworkView
Your scene hierarchy should now look like:
Scene hierarchy
Now we will create a simple cube prefab which will serve as our “player” object. To do this, add a Cube GameObject to the scene. Rename the cube to “Player“. Add a Rigidbody component and also a NetworkView component. In the Inspector, drag the Rigidbody component onto the “Observed” property of the NetworkView component.
Networkview observe Rigidbody
Also in the Inspector, tag the object as “Player“.
Tag as Player
Finally, drag the cube from the Hierarchy view into the Project view. This will create a prefab called “Player” in the project’s Assets folder. Move this prefab into the Game folder and delete the original Player GameObject from the scene.
In the “Game” folder add a JavaScript asset called “PlayerInfo.js” with the following code:
Drag this script from the Project view onto the GameController object in the Hierarchy view. Select the GameController to view the Player Info (Script) in the Inspector. Now drag the “Player” prefab from the Project view onto the “Player Prefab” property in the Inspector. This script serves as a “global” placeholder for the player prefab that the clients will instantiate when they connect.
Player prefab property in Inspector
We have now finished the shared “Game” folder. All that is left to do is to export the folder and (later) import it into the client project. To do this, select and right-click the “Game” folder in the Project view and select “Export Package…” from the pop-up menu that appears.
The Exporting Package dialog will appear.
Export package
Click the “Export…” button and save the Unity package as “NetworkGame“.
We will continue with the client project later on, but first we will complete the server side.
Spawn players
In the folder “Server“, add a second JavaScript called ServerPlayerManager.js. The Project view should now look like the screenshot below.
Project view
Add the following code to the ServerPlayerManager.js script:
This code finds the PlayerInfo script (global placeholder) we created above and uses it to Network instantiate the Player prefab (the cube), positioning it 3 units above the plane (Vector3.up*3) so that it will initially drop down to the plane (it’s a rigid body, so it will be affected by physics). The cube is not rotated (Quaternion.identity). The group parameter is set to 0 which means send to all clients (we are not using groups).
To make sure the spawnPlayer() function is called, we add the following code to ServerMain.js:
This code first ensures that the ServerMain.js script is attached to a GameObject that also has the ServerPlayerManager.js script attached to it. This is done by:
When the script is created, Unity calls the Awake() function and a reference to the ServerPlayerManager component is stored in a private variable:
This private variable is used to call the spawnPlayer() function when a new client connects (Unity will invoke the OnPlayerConnected() function when a client connects to the server):
To finish the server side, drag both the ServerMain.js and ServerPlayerManager.js scripts onto the GameController object.
Client Project
Switch to the client project and create a folder called “Client” in the Project view. This folder will contain everything that is needed only on the client side. Drag the ClientMain.js script into this folder. Also, delete the GameObject in the scene.
Now import the NetworkGame.unitypackage you exported from the server project. To do this right click in the Project view and select Import Package > Custom Package… from the pop-up menu, then browse to the location you exported the package to.
Import custom package
Open up the Game scene, and drag the ClientMain.js on to the GameController object in the Hierarchy view.
Second Networking Solution
Press run in the server project and press the “Start Server” button. Now press run in the client project and press on the “Connect to Server” button. You should see a cube drop down onto the plane in both the server and client game views.
Client side game view instantiated player
Select the Player(clone) in server scene hierarchy and use the inspector to modify the position (change the x to 3 for example). You should see the cube change position on both the server and the client.
Select the Player(clone) in the client scene hierarchy and change it’s position likewise. The cube should now only be modified on the client side, and not the server side. In fact, if you now change the server side position again, the client side will jump to the correct (server) position again.
This clearly demonstrates that the server is authorative.
Spawning madness
In the client game view, press the “Disconnect” button. You will notice that the cube on the server and the client side remain where they are. Press the “Connect to Server” button again. You should see one additional cube spawn on the server side and two additional cubes on the client side, making the total number of cubes on the client side three.
If you repeat this a couple of times, you will soon have cubes exploding all over the place!
To understand why this is happening, we first need to discuss Remote Procedure Calls (RPC) and how they are implemented in Unity.
RPCs
Remote Procedure Calls, or RPCs for short, are function calls made over the Network. This allows a client to call a function on a remote server, or vice versa, for example.
The following code snippet demonstrates an RPC function:
Notice the @RPC attribute placed before the function declaration. This tells Unity that this function can be called remotely.
To call this function, the server or any client can use:
The first parameter of the RPC() call is simply the name of the function to call. The third parameter is the parameter for the function call, i.e. the above call is equivalent to locally calling PrintText(“Hello world”).
The second parameter is the RPCMode, which can be used to indicate to whom the RPC call should be made:
Server: Sends to the server only.
Others : Sends to everyone except the sender
OthersBuffered : Sends to everyone except the sender and adds to the buffer
All : Sends to everyone
AllBuffered : Sends to everyone and adds to the buffer
Buffered RPCs
The RPCMode allows an RPC call to be either unbuffered (Server, Others, All) or buffered (OthersBuffered, AllBuffered). Non buffered calls are carried out and then forgotten. Buffered calls, on the other hand, are never lost but are stored by the server in an ordered stack. When a new client connects, all the buffered calls are carried out on the new client (in order). This allows the game state to recreated on a client who comes into a game that has already started.
Whenever we make a call to Network.Instantiate(), Unity makes use of buffered RPCs underwater. This ensures that when new clients connect, all the instantiated game objects (player instances etc.) will also be created on the new client.
Restrictions on RPCs
You can easily build your own RPCs, but there are some restrictions:
Firstly, the network communication required for RPCs is handled by a NetworkView. This means that the script containing RPC functions must be attached to a GameObject that also has a NetworkView component. This is why we added a NetworkView to the GameController object in the projects above. The GameController is a dummy placeholder and so we don’t need to synchronise the state (position, rotation etc) of the object. However, the script calling Network.Instantiate() (and thus making RPC calls underwater) is attached to the GameController. For this reason, it requires a NetworkView, but the State Synchronisation property can be set to “off” and the Observed property to “None”.
Secondly, an RPC function can have as many parameters as you want, but each parameter must be one of the following types: float, string, NetworkPlayer, NetworkViewID, Vector3, Quaternion. To call an RPC with more than one parameter, use the following syntax:
Improved Spawning
Returning to our client and server projects, we can easily understand the spawning madness we get when the client disconnects and reconnects. When the client connects for the first time, its Intantiate() call is buffered. When the client disconnects and connects again, it triggers a new Instantiate() and it also gets the buffered Instantiate(), thus leading to two cubes. A third connect, would lead to three cubes, and so on.
To avoid this problem, the server needs to clean up the buffered RPC calls for clients that have disconnected.
However, that is not all. On the client side, the client has to clean up its old cubes before connecting again. The sever cannot do it for the client because it is no longer connected.
Clean up client side
To improve the spawning process add the following code to the client side:
The function OnDisconnectedFromServer() will be called by Unity when the client is disconnected from the server. Because we tagged the Player prefab with the “Player” tag, we can get any cube instances using:
We can then use the Destroy() function to delete these old player cubes.
Clean up server side
To improve the server side spawning and cleaning up when a client disconnects is a little more complicated. The reason for this is that the server needs to keep track of which cube instance belongs to which client. This allows the server to delete the right cube when a client disconnects.
Replace the existing code in ServerPlayerManager.js with:
This instantiates a new cube when a client connects, and places the cube instance in a hashtable, using the NetworkPlayer as the key.
To clean up when a client disconnects add the following code:
This function first finds the cube instance for this player in the hastable (using the NetworkPlayer as key). It then calls
which clears away any buffered RPCs belonging to the cube’s NetworkView. This will ensure that new clients connecting will not instantiate a cube for the disconnected player.
Next, the cube is deleted from all clients (including the server) using
This is basically the network version of the local Destroy() function.
Finally, the server removes the player and cube instance from the hashtable:
To make sure this function is called when a client disconnects, as the following code the ServerMain.js
This uses the private variable instance of the ServerPlayerManager.js script initialised earlier.
Run the solution
Start up the Server and the Client projects, and try connecting and reconnecting the client. Spawning and cleaning up of the cubes should now work as expected. Try building the client to create an executable (.exe) and also running this a number of times so that you have more than one client connecting to the server at the same time.
Movement
To complete this introduction tutorial, lets improve our networking solution to allow the clients to move their cubes about. To do this in an authorative server solution requires the clients to send their input (key presses etc) to the server. The server will then update the world (ie the position of the clients cube) and send the result to all connected clients.
Server project
Add the following code to ServerPlayerManager.js:
This is an RPC function which can be called by the clients remotely. It basically finds the cube belonging to the player and then updates the position depending on the value of the “vertical” and “horizontal” parameters the client sends.
Client project
Add the following code to ClientMain.js:
Unity Input is used to check if any key is pressed
and if so the function sendInputToServer() is called. This function uses Input.GetAxis() to check for input:
On the “Horizontal” axis, -1 is left and 1 is right. On the “Vertical” axis, 1 is up and -1 is down. If theri is any input (!=0) then the code calls the RPC “handlePlayerInput” on the server:
passing along as first parameter the client NetworkPlayer
followed by the values of vertical and horizontal.
Note that RPCMode.Server is used, meaning that the RPC call should only be sent to the server for execution.
Also add the following code:
The reason that this dummy (empty) function is needed is that Unity requires the client and the server to both have the RPC function, even if it is only going to be called on the server. This is a typical for Unity networking in that Unity seems to prefer the client and server code to be combined into one and the same project. This would favour the peer to peer networking model, where a client can host a game and be the server as well. In this tutorial, we have split the client and sever code into two projects. This serves the purpose of clarifying what the server does and what the client does.
Final run
Run the server and connect with one or more clients. Each client should now be able to move it’s own cube (but nobody else’s). The server should not be able to move anything. Try bumping one cube into another and see that the server side physics (the cubes are rigid bodies) handles all the collisions.
Congratulations! You have now completed this introduction to Unity networking.
Download
You can download the two projects directly here.