Unity Networking Tutorial

Introduction

Starting a network game can be an involved process. In this tutorial, we’ll start simple and work our way up to the foundation of a multiplayer FPS game. I will assume you are familiar with basic concepts and classes of Unity and C#. This uses the older M2H Networking Tutorial concepts and follows along. You can view it here. (I haven’t read his paid version)

Part 1

Starting the Server

To start, the first thing we will create is a menu to control the code to either create or connect to a server, so we’ll be working in the OnGUI() function.
Setup the scene

  • In a scene, create a new empty game object.
  • Create a new C# script.
  • Attach the script to the empty game object, then open up the script.

We’ll start with two variable declerations, a string and an int to store the IP address and the port.

public string connectionIP = "127.0.0.1";<br />public int connectionPort = 25001;
  • 127.0.0.1 is a special address that refers to “this local computer”. Right now, we’re just creating the server for local connections only.
  • The 25001 is the port that Unity uses. Ports are used to communicate different standards and protocols across the internet. There are a number of standard ports that are ‘open’ for using things like http, ftp, etc. Other, nonstandard, ports are usually automatically set to ‘closed’. There’s nothing inherently special about 25001, it’s just the number Unity decided on.

Now we’ll create the menu which changes depending on the connection status you initialize from within it.

void OnGUI()<br />{<br />    if (Network.peerType == NetworkPeerType.Disconnected)<br />    {<br />        GUI.Label(new Rect(10, 10, 200, 20), "Status: Disconnected");
  • NetworkPeerType is an enumeration with the following values: Disconnected, Connecting, Client, Server. This first statement checks our Network class variable peerType (which is of type NetworkPeerType) to see if we have the Disconnected value. Since we haven’t initialized or connected to any server, Unity automatically assigned peerType to Disconnected. Then we just have a label that states we are disconnected.
if (GUI.Button(new Rect(10, 30, 120, 20), "Client Connect"))<br />{<br />       Network.Connect(connectionIP, connectionPort);<br />}
  • This creates the button to connect to a server as a client. There are several overloaded options for the Connect function, but we are just sending it an IP address in the form of a string and the port number as an int. You can also use a domain name in place of the IP address. We haven’t started a server yet, so if we clicked the button now, it would try to connect at the IP address (which would just be whatever computer you are on) at port 25001. Nothing is active yet so it wouldn’t connect to anything so nothing would happen.
if (GUI.Button(new Rect(10, 50, 120, 20), "Initialize Server"))<br />{<br />    Network.InitializeServer(32, connectionPort, false);<br />}
  • The InitializeServer function in the Network class starts a server on your computer using the connectionPort as the second parameter. The first parameter is the maximum number of connections, or players, you allow to your server. The final parameter is for NAT (Network Address Translation). We’ll ignore it for now, just set it to false.
else if (Network.peerType == NetworkPeerType.Client)<br />    {<br />        GUI.Label(new Rect(10, 10, 300, 20), "Status: Connected as Client");<br />        if (GUI.Button(new Rect(10, 30, 120, 20), "Disconnect"))<br />        {<br />            Network.Disconnect(200);<br />        }<br />    }
  • This statement is triggered we are not disconnected and we are identified as a client (we connected to a server). It just sets the label to show we are a client. It also creates a button that calls the disconnect function which shuts down the network interface. The number sent as a parameter is just the Timeout number, which is how many milliseconds it has to signal that we are disconnecting.

Here’s the completed code.

Starting and Connecting

In the Edit menu, go to Player under Project Settings and make sure there’s a check in ‘Run In Background’. Build a web player version, so with both Unity editor and the Web version running, we can start the server on one, and connect as a client on the other. Since we have added any functionality it doesn’t actually do anything. But we are connected. Now we just have to program the interactions between the server and client (the hard part).

Setting up the Scene

We are going to create a scene where if you start the server you can move a block around. If you are the client, you can’t move it, but you can see the server move it.

  • Create a cube and scale it out so it can act as a floor. Create a Camera looking at the floor (make sure there aren’t any other cameras). Create another cube and attach a NetworkView. This component is what is required for objects to send and receive network data. Create another script and attached it to this cube, and attach a Rigidbody. Also add a light so you can see what’s going on.

Moving an Object on the Server

In the script attached to the cube add the following code in the Update function.

if (Network.isServer)<br />{<br />    Vector3 moveDir = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));<br />    float speed = 5;<br />    transform.Translate(speed * moveDir * Time.deltaTime);<br />}
  • This code gets the values from the two axis, which are already setup in Unity as w,a,s,d. Then is performs transform.Translate which takes the transform of the cube and modifies it with the new vector we create with the input from the keyboard, and the speed determines how big the values are. Obviously the bigger the value the more it will move. So all we are modifying is the transform of the cube.
  • Build and run it as a webplayer. Back in Unity hit the play button and initialize server; the box should move around on your screen. In the web player, if you connect as a client you should see the box move around as you move it on the server.

The NetworkView

Notice the NetworkView attached to the cube. Under the Observed field, it should say Cube (Transform), so it’s watching what happens to the transform (which we are changing in the previous section). The State Synchronization will say “Reliable Delta Compressed”. This will watch the Observed value for any changes. Once it detects the change it will transmit that information across the network.

So the code is setup to modify the transform of the cube, which is being observed by the network view so that whenever a change is detected it’s sent across the network.

NetworkView can observe several different types of things. Instead of the transform, copy the script used to move the cube into the observed field. When a script is being observed by the NetworkView it works with a function called OnSerializeNetworkView.

  • Add the following to the script, after the Update() function.
void OnSerializeNetworkView(BitStream stream, NetworkMessageInfo info)<br />{<br />    if (stream.isWriting)<br />{<br />        Vector3 pos = transform.position;<br />        stream.Serialize(ref pos);<br />}
  • stream.isWriting is only set to true for the owner of the object that the script is attached to. In this case, the server is the owner because the server is the one who actually created the cube. So what this code does is check for the owner. If you are the owner then store your transform position as the variable pos, and then send that information to the Serialize function which sends it out on the network.
else<br />{<br />        Vector3 receivedPosition = Vector3.zero;<br />        stream.Serialize(ref receivedPosition);<br />        transform.position = receivedPosition;<br />}
  • This says if you are not the owner (so everyone besides the server), create a variable that stores a position (initially set to 0). The stream.Serialize on this side of the else does the opposite for clients. Instead of sending the data out like it does for the owner (server), it receives data from the network. It sets the position of the cube equal to the data that it receives.

Summary

This code sets up the cube in the following way: the server is the owner and sends the transform.position of the cube to the network. Any clients that connect are sent the position of the cube. The position is determined by the horizontal and vertical inputs, and only the server is allowed to modify that position given those inputs.

Remote Procedure Call (RPC) Introduction

There’s one more method of communication for multiplayer games in Unity, and that’s using RPCs. Click on the cube that moves, and on the Networkview turn State Synchronization off and change the Observed none. To use RPCs, you just need the object to have the NetworkView attached; it doesn’t need anything else on the component. Open up the script that moves the cube, and comment or delete out the OnSerializeNetworkView function.

  •     At the top of the script, right above the void Update() add:
Vector3 lastPosition;<br />float minimumMovement = .05f;
  •     In that script, go up to the Update() function, and add the following lines after the “transform.Translate…” line, but still within the if statement.
if (Vector3.Distance(transform.position, lastPosition) &gt; minimumMovement)<br />{<br />    lastPosition = transform.position;<br />    networkView.RPC("SetPosition", RPCMode.Others, transform.position);<br />}

Like the M2H tutorial, this if statement checks to see if the distance between our current position after moving is greater than whatever we decide the minimum movement space (.05 meters in this case). If it is, update the lastPosition variable to the current position. Then call the RPC function of the networkView class. If you’ve done any work with SendMessageUpwards, it’s similar syntax. The way it’s setup is that the first parameter is a string of the name of the RPC function (we haven’t defined it yet), the second parameter is who it’s sent to (Server, everyone but yourself, and everyone including yourself), and the last one is the information we are sending to the function.

[RPC]<br />void SetPosition(Vector3 newPosition)<br />{<br />    transform.position = newPosition;<br />}

It’s just like a normal function but has [RPC] before it.

Summary

No actual changes to the end result, just the way the communication is handled. The server is still the only one who can move the cube, and then it calls the RPC SetPosition on all other network instances. So the client cube just sits there waiting for an RPC call, and then sets its position to whatever values are sent.

(This corresponds with Tutorial 2B in the M2H networking tutorial.)

Now we are going to spawn the cube whenever someone creates or joins a server. The creator of the cube is the owner and the owner will be able to move the cube around, sending updates to the other cubes of it’s position.

  • Create a prefab in the project panel, and drag the movable cube on the prefab. Delete it from the scene.
  • We’re going to use the previous method of network communication with the OnSerializeNetworkView, so in the prefab inspector settings change the NetworkView’s State Synchronization to Reliable Delta Compression and the Observed to the script that moves the cube. (You need to drag not the script variable itself but the script component, meaning the bolded heading/title part, and move that to the Observed field.)
  • Create an empty game object, a new c# script, and attach that script to the game object. Add the following code (go ahead and delete the start and update functions).
public Transform cubePrefab;<br /><br />void OnServerInitialized()<br />{<br />    SpawnPlayer();<br />}<br />void OnConnectedToServer()<br />{<br />    SpawnPlayer();<br />}

Here we are creating a variable to hold the cube prefab we just created. Save it, and drag the prefab you created to the cubePrefab variable in the inspector. The two functions both occur when the server is created by the server player, and whenever any client players join the server. They both call the following function:.

void SpawnPlayer()<br />{<br />    Transform myTransform = (Transform)Network.Instantiate(cubePrefab, transform.position, transform.rotation, 0);<br />}

So this calls the Network.Instantiate function which creates the cube we stored as the prefab. Since clients call the OnConnectedToServer when they connect, they are the ones calling the instantiate function so they are the owners if the cube, so when we add code to the OnSerializeNetworkView, the client will be the owner/the one who is streaming the data out, of that particular cube. All other cubes in the scene are in the ‘read-only’ mode just watching to receive updates from other clients or the server.

Now we need to change the script attached to the cube. Instead of just checking to see if it’s the server, we’ll check to see if we are the owner.

  • Add the awake function to the script attached to the cube prefab.
void Awake()<br />{<br />    if (!networkView.isMine)<br />        enabled = false;<br />}

This just turns off the script if we are not the owners of the cube. If we connect as a client, we own the 1 cube that is instantiated when we connect; for all other cubes we are not the owner, so the script on those cubes is turned off. One thing to note; even though the script is turned off, it will still respond to RPCs and OnSerializeNetworkView data.

  • Make the following change in Update(). Change the line “if (Network.isServer)” to:
if (networkView.isMine)

Technically it shouldn’t be necessary since the script should be disabled if we are not the owner, it’s just an extra safe guard to verify that we are infact the owners.

  • Also, remove the if statement that calls the networkView.RPC, and comment out the RPC function.
  • Add the following outside of the Update function:
  • (*Quick note: add the following below the using commands at the very top:)
void OnSerializeNetworkView(BitStream stream, NetworkMessageInfo info)<br />{<br />    if (stream.isWriting)<br />    {<br />    Vector3 myPosition = transform.position;<br />    stream.Serialize(ref myPosition);<br />    }<br />    else<br />    {<br />        Vector3 receivedPosition = Vector3.zero;<br />        stream.Serialize(ref receivedPosition); //"Decode" it and receive it<br />        transform.position = receivedPosition;<br />    }<br />}

This is the same as it was previously. If we are the owner, send out our position, if we are not the owner do nothing but receive the position and make our position equal to the received data.

Non-Authoratitive/Authoratitive Server

Nothing has fundamentally changed in regards to the end result until now. We finally have 2 different cubes moving by two different players. However, this setup is called non-authoritative. This is because the client is sending out his own information to the other clients. It means that the other clients are trusting this information. A client could, in theory, change the data on the way out which could ruin the game for others when they receive the incorrect information. An authoratitive server is one that prevents this from happening. This is different in that the client will send a request (to move its position, for example), and the server checks the request, performs the request for the client, then sends the information once everything checks out.

  • Create a new script and attach it to the game object that contains the previous spawning script. Remove the previous one.
  • Add the following to the new script:
public Transform playerPrefab;<br />public ArrayList playerScripts = new ArrayList();<br /><br />void OnServerInitialized()<br />{<br />    SpawnPlayer(Network.player);<br />}

I’ll explain the first two lines later. When we start the server as the server player, OnServerInitialized is called, and we send Network.player to a function (we are about to declare). Network.player is just the local instance of a NetworkPlayer class. So we are sending ourselves to the SpawnPlayer function.

void OnPlayerConnected(NetworkPlayer player)<br />{<br />    SpawnPlayer(player);<br />}

A critical difference here is this function. It’s not the previous OnConnectedToServer(). This function is called on the server whenever a client connects, and the player variable stores the information about the client that just connected. So when a client connects, the server player calls SpawnPlayer and sends it the information about the client that just connected.

void SpawnPlayer(NetworkPlayer player)<br />{<br />    string tempPlayerString = player.ToString();<br />    int playerNumber = Convert.ToInt32(tempPlayerString);

This just gets the index number (but it gets it as a string) of the player with the ToString() function, then we just convert the string to an int. So the player’s index number is stored as playerNumber. Continuing on:

Transform newPlayerTransform = (Transform)Network.Instantiate(playerPrefab, transform.position, transform.rotation, playerNumber);<br />playerScripts.Add(newPlayerTransform.GetComponent("cubeMoveAuthoritative"));

This creates the network instantiation of the player who just connected. The last parameter we send the index of the player. This is the group that the player is in (we previously just set it to 0, which is fine too). Since the server is the one doing the instantiate function then the server is the owner of this new cube, even though it was created when the client connected. Then we add the script that will be used to control the cube, in my case I named the script cubeMoveAuthoritative. The script is added to the list we declared at the top. We’ll use it later.

  • Now add:
NetworkView theNetworkView = newPlayerTransform.networkView;<br />theNetworkView.RPC("SetPlayer", RPCMode.AllBuffered, player);

This stores a reference to the networkView attached to the instantiated cube, and then calls the RPC SetPlayer from that cube. It RPCMode is set to AllBuffered, so everyone gets this RPC. And since it’s buffered, that means that anyone joining at a *later* time will also get this RPC called as soon as they join the server. And the function is sent the information about the client that just connected in the form of the player variable.

  • Create a new C# script and name it the name you choose in the playerScripts.Add line in the previous script. Remove the previous script from the cube, and attach the new one.
  • Move the script into the Observed variable of the Network View.
  • Select the GameObject that contains the script we’ve been working on, and drag the player cube into the player prefab field.
  • Add the following code to the script that will move the cube, in my case the cubeMoveAuthoritative script. (Go ahead and remove the functions in there already). Before any functions, add:
public NetworkPlayer theOwner;<br />float lastClientHInput = 0f;<br />float lastClientVInput = 0f;<br />float serverCurrentHInput = 0f;<br />float serverCurrentVInput = 0f;
  • Now add:
	void Awake()<br />	{<br />		if (Network.isClient)<br />		   enabled = false;<br />	}

As soon as this cube is instantiated, we check to see which network player we are. If we are not the server, then immediately disable this script.

	[RPC]<br />	void SetPlayer(NetworkPlayer player)<br />	{<br />		theOwner = player;<br />		if (player == Network.player)<br />			enabled = true;<br />	}

This is the RPC that is called right after the cube has been instantiated. The server calls this RPC to everyone and sends it the information about the client that just connected. When it’s called, it sets theOwner equal to the player that just connected. Then it checks if this player who just connected is the same as the local instance of the player. In other words, if I’m a client that just connected to the server, SetPlayer is sent my information. Then the script checks to see if I am the same player whose information was just sent to this function. Since I am the client, and I did just connect, and it did just sent my information, then it evaluates to true, so this script is enabled for me. From another view, if I’m a client and someone else connects, the SetPlayer function is called, and it sent information about the client that just connected. Then it checks to see if the information it was sent is the same as me.

	void Update()<br />		{<br />			if (theOwner != null &amp;&amp; Network.player == theOwner)<br />			{<br />				float HInput = Input.GetAxis("Horizontal");<br />				float VInput = Input.GetAxis("Vertical");

This checks theOwner, which is set to whatever player joined which caused the cube to be instantiated. So if we are not the client that caused the instantiate then our Network.player will not equal theOwner, so we will not run any of the code; only the player that instantiated it will run it. The code stores the input from the axises.

	if(lastClientHInput!=HInput || lastClientVInput!=VInput )<br />	{<br />		lastClientHInput = HInput;<br />		lastClientVInput = VInput; 

If one of the two variables have changed, in other words if any movement has occured, the variables are set to the input movement.

		if (Network.isServer)<br />		{<br />			SendMovementInput(HInput, VInput);<br />		}<br />		else if (Network.isClient)<br />		{<br />			networkView.RPC("SendMovementInput", RPCMode.Server, HInput, VInput);<br />		}<br />	}

We are still in the if statement with this code, so it’s only executed if we are the owner of the cube. It checks to see if we are the server or a client owner of the cube. If we are the server, send our input values to the function SendMovementInput. If we are a client owner of the cube, then call same function as an RPC and send it only to the server, and send it the same variables. This section and the next 2 are all very closely related, so I’ll summerize how they all work together at the end.

	if(Network.isServer)<br />	{<br />			Vector3 moveDirection = new Vector3(serverCurrentHInput, 0, serverCurrentVInput);<br />		float speed = 5;<br />		transform.Translate(speed * moveDirection * Time.deltaTime);<br />	}<br />    }  

This is outside the if statement. This checks to see if we are the server. If we are, create a new Vector3 using the saved input values that are changed whenever movement occurs, and then apply the input to the transform of the cube to move it.

	[RPC]<br />	void SendMovementInput(float HInput, float VInput)<br />	{<br />		serverCurrentHInput = HInput;<br />		serverCurrentVInput = VInput;<br />	}

Outside the Update function. This is the 3rd major piece of the script. It sets the serverCurrent variables to the Input variables, which are set when movement occurs. So essentially what happens up to this point is the following: (in these examples, I use the word owner in the sense that the Client owner is the client that joins that causes the instantiation and has ‘theOwner’ variable set to the client Network.player. Technically the server is the real owner of all the cubes. ‘theOwner’ variable is what we use to find out who caused the instantiation which determines which client will actually be “in control” of the cube, meaning its the one the sends the movement updates).

Client owner perspective: Network.player will equal theOwner, so record our input variables in HInput and VInput, then set those equal to lastClient input. Since Network.isClient is true, call the RPC SendMovementInput and send it the input variables we recorded. This RPC is called on the server. The server takes the input, stores it in the serverCurrent variables. Then in the Update function, since Network.isServer now evaulates to true, run the code the moves the cube based on the received input from the client. So the server actually moves the cube. The client receives the updated transform via the OnSerializeNetworkView, which should be added at the end of the script:

	void OnSerializeNetworkView(BitStream stream, NetworkMessageInfo info)<br />	{<br />		if (stream.isWriting)<br />		{<br />			Vector3 pos = transform.position;<br />			stream.Serialize(ref pos);<br />		}<br />		else<br />		{<br />			Vector3 posReceive = Vector3.zero;<br />			stream.Serialize(ref posReceive);<br />		transform.position = posReceive;<br />		}<br />	}

Client non-owner perspective: another client joins the server, the server instantiates a cube and assigns this cube to the most recent client connected. From this perspective the cube’s script is disabled, but will still receive network updates. The only updates it receives are on OnSerializeNetworkView which change its transform in order to move it.

Server non-owner perspective: for each cube, on each Update is sets the transform of the cube to the values of the serverCurrent variables for that particular cube. It receives updates to these variables from the RPCs sent to the server from the owner of the cube, and then translates these updates into a new transform for the cube. Since the server is the owner of all the cubes in the sense that it is the one that instantiates everything, in the OnSerializeNetworkView, stream.IsWriting is true, so it sends out its position over the network. All other cubes receive this updated position.

Here’s the completed code.

Part 2 coming soon…

Previous comments:

  • voxel (@voxel)  On October 29, 2011 at 5:23 pm

    This is the most sane, clearly written unity networking tutorial I’ve come across. Thanks very much :)

  • Gordon Jennings  On November 8, 2011 at 9:01 am

    Hi, I’m not sure what I’m doing wrong, but I’ve got the authoritative version built and the only slight differences are that I have a singleton-like object that is used for checking input (i like to keep input checking modular and separate instead of having scattered input checks throughout my code). I’ve also had to add the network view to the prefab via code in it’s Awake because unity won’t let me drag via the inspector for some reason. (I actually prefer the code to dragging, less the artists can screw up).

    These two adjustments should not effect any of the network code.

    Anyway, only the server player can move. I’ve tried various combinations of using the editor, built exes, and browser builds just in case there was some sort of network and/or input confusion with the programs running on the same machine.

    I’d like to send you my project so you may look at it. It’s likely just a slight typo, I’ve followed your tutorial(s) from top to bottom of this page. The first couple worked out fine, but then the last 2 I was only able to move the server player.

    Here is a link to download my project:
    http://www.gordonjennings.net/temp/test.zip

    Between 8am and 4:30pm (GMT-4) you can contact me at: gordon.jennings@armyelearning.ca

    Outside of those hours you can contact me at: gordon@gordonjennings.net

    You’re help is much appreciated.

    • imtriggerhappy9  On December 14, 2011 at 12:52 am

      I think you need to finish them up… just keep going throught the tutorials. The last script is what you are looking for.

  • imtriggerhappy9  On November 28, 2011 at 4:00 pm

    i completely agree with the first comment thank you very much!

  • Ben  On January 26, 2012 at 7:21 am

    This was really useful in learning unity networking with C#, Thank you very much :D

  • Valrak  On February 4, 2012 at 11:20 am

    This was really helpful, thank you, but I can’t seem to find part 2 (the link on the post only leads to part 1), any help?

  • Darren  On February 23, 2012 at 6:05 am

    This is great, however, where is part 2, as i was hoping to see this as a fps (as thats where im having problems) Thanks again though, good article

  • John Chalmers  On October 27, 2012 at 10:02 pm

    Sorry my brain is so slow but how would you send rotation details over the network can someone show me ?

  • irwin  On November 3, 2012 at 2:09 am

    Hmm… I can’t seems to use any function like Network.InitializeServer(…) in C# Script. Is it because I’m developing an iOS app, using Unity iOS Basic? Or is there some mistake I’m doing? I can use the Network classes in Javascript though…

  • irwin  On November 3, 2012 at 4:42 am

    ignore the previous post, it was just some silly mistake I made, it works fine now

  • John Chalmers  On November 24, 2012 at 7:02 pm

    This is probably the best networking tutorial on Unity3D C#, can you finish it with Masterservers and NATpunchthrough?

  • Oru  On December 29, 2012 at 8:32 pm

    Great Tutorial! Easy to understand and follow!
    I’ve got some questions:

    1.Right now, the code doesn’t include a way to destroy a player’s cube when he disconnects. I think I should use the function OnPlayerDisconnect() to trigger the removal of his cube but I’m not sure where to put the “Destroy Cube” function. Should this function be an [RPC]?

    2. If I’m a client and I disconnect and reconnect to the server, the number of cubes that instantiates under my control keep increasing everytime. E.g: 3 cubes instantiate at once of my 2nd connect. Why is that and how can I fix it?

    I’m looking forward to the 2nd part of this tutorial :D

  • neologiac  On January 9, 2013 at 5:36 am

    thanks a lot, was really helpful.

  • abreu20011  On January 27, 2013 at 10:02 am

    Thanks!!! Very, very thanks. A clear and nice tutorial about networking :)