Multi Player Tutorial
Multi Player Tutorial
Multi Player Tutorial
1. Multiplayer Tutorial
1. Introduction
3
2. Getting Started
3
3. Creating Your First Client / Server Application
4
3.1 Preparing Scene
4
3.2 Creating Scripts & Adding Components
5
3.2.1 Server & Client
5
3.2.2 Scene & Objects Instantiation On The Network
8
3.3 Quick Overview
10
4. Implementing Multiplayer in Startrooper
11
4.1 Changing Scene
11
4.2 Integration
11
4.2.1 Integrating Objects & Scene
11
4.2.2 Server & Client
21
4.3.3 Final Polishing
37
4.3 Quick Overview
38
Multiplayer Tutorial
1. Introduction
The aim of this tutorial is to show the essence of Unity Networking. We will show
how to create basic and advanced network applications using Master Server/Client,
UDP Server/Client and Direct Connect. In this tutorial we will use Unity iPhone 1.6,
iPhone 3GS and the StarTrooper demo from the Unity web site.
2. Getting Started
There are a few things that you should know before you start.
iPhone or iPod.
• Important notes:
You can disable or enable Networking in: Edit -> Project Settings -> Player
-> Enable Unity Networking.
You can connect between different types of Unity targets. For example you
can connect from Unity on the desktop to Unity iPhone, from Unity Web
Player to Unity iPhone, etc.
Create a new Prefab and name it Player: Assets -> Create -> Prefab.
Create a new Cube GameObject: GameObject -> Create other -> Cube.
Drag your Cube from the Hierarchy to your Player Prefab in the Project and
then delete your Cube from the scene.
4
Create a new Plane and name it Ground: GameObject -> Create other ->
Plane. Your Plane parameters should be: Position (0,0,0), Rotation (0,0,0),
Scale (5,5,5).
Create a Directional Light: GameObject -> Create other -> Directional Light.
Light parameters: Position (0,15,0), Rotation (25,0,0), Scale (1,1,1) Shadows
-> Type -> Soft Shadows.
Finally, save your scene and name it MainGame: File -> Save Scene.
Create a new JavaScript file and name it ConnectionGUI: Assets -> Create ->
JavaScript.
Add this file (by dragging) to the Main Camera object in the Hierarchy, then
open the file and create some variables:
5
Now we will create an interface using Unity GUI for creating the server and
connecting to it:
function OnGUI () {
// Checking if you are connected to the server or not
if (Network.peerType == NetworkPeerType.Disconnected)
{
// If not connected
// Notify our objects that the level and the network is ready
for (var go : GameObject in FindObjectsOfType(GameObject))
{
go.SendMessage("OnNetworkLoadedLevel",
SendMessageOptions.DontRequireReceiver);
}
}
6
• Pay attention to the function below. This function is called every time someone
successfully connects. When this happens, we notify all objects that the scene and
the network are ready.
function OnConnectedToServer () {
// Notify our objects that the level and the network are ready
for (var go : GameObject in FindObjectsOfType(GameObject))
go.SendMessage("OnNetworkLoadedLevel",
SendMessageOptions.DontRequireReceiver);
}
• Now you can test your server and client. Edit your Player Settings (Edit -> Project
Settings -> Player) to set up your iPhone Bundle Identifier and switch the Default
Screen Orientation to Landscape Right. Build your project to the iPhone and create
a server in the Editor. Try to connect to the server using the IP address you can
find on the server screen — if everything goes OK you will see “Disconnect” button
and your IP address on both screens. Note that both applications must be on the
same network for everything to work.
7
3.2.2 Scene & Objects Instantiation On The Network
• Now we need to add the network components to your Player (Cube) and write code
to instantiate it:
When the component appears on your object change the State Synchroniza-
tion parameter to Reliable Delta Compressed. This is needed to show your
synchronized transformation to all users.
Add a Rigidbody to your Player Prefab: Select Prefab -> Component -> Phys-
ics -> Rigidbody.
Create a new empty GameObject and name it Spawn. Object parameters: Po-
sition (0,5,0), Rotation (0,0,0), Scale (1,1,1).
function OnNetworkLoadedLevel () {
// Instantiating SpaceCraft when Network is loaded
Network.Instantiate(SpaceCraft, transform.position, transform.rotation, 0);
}
8
Add this file to your Spawn object: Select Spawn Object -> Component ->
Scripts -> Instantiate.
Select your Spawn object and change the Player parameter to “Player (Trans-
form)” by selecting your prefab from the list.
• If you test your example you will see that the server and each connected user will
have their own Player (Cube). To be sure, we will create a simple test:
function OnGUI() {
if(GUI.Button(new Rect(20,100,50,50),"up"))
{
GameObject.Find("Player(Clone)").transform.position = new Vector3(0,5,0);
}
}
Build your project, create a server and connect to it. Now you can see that
everyone can move their own Player (Cube).
9
3.3 Quick Overview
Create a server.
Create a client.
10
4. Implementing Multiplayer in Startrooper
In this chapter you will learn how to convert the StarTrooper game from single
player to multiplayer. We will use complex components and three different con-
nection types: Direct Connect (that you have seen in chapter 4), MasterServer Con-
nection and UDP Broadcast connection. At the end of this chapter you will be able
to fly around and kill other users in multiplayer mode.
• Download StarTrooper from the Unity Web Site and make yourself familiar with it.
Create a new GameObject and name it Spawn: GameObject -> Create Empty.
Set Transform parameters: Position (0,30,11), Rotation (0,0,0), Scale (1,1,1).
4.2 Integration
Now we will make our main changes and integrate networking into the project.
Let's begin by preparing the scene and SpaceCraft for the network. When we are
done, we will create a server and client.
First, create two folders: "NetworkFIles" for new scripts and "Plugins"for C#
files that should be pre-compiled.
11
Do not worry much about this file, just use it where it is required.
You can use this script on any project you want, because it's suitable for all
Rigidbody objects.
using UnityEngine;
using System.Collections;
stream.Serialize(ref pos);
stream.Serialize(ref velocity);
stream.Serialize(ref rot);
stream.Serialize(ref angularVelocity);
}
// Read data from remote client
else
{
Vector3 pos = Vector3.zero;
Vector3 velocity = Vector3.zero;
Quaternion rot = Quaternion.identity;
12
Vector3 angularVelocity = Vector3.zero;
stream.Serialize(ref pos);
stream.Serialize(ref velocity);
stream.Serialize(ref rot);
stream.Serialize(ref angularVelocity);
// Update used slot count, however never exceed the buffer size
// Slots aren't actually freed so this just makes sure the buffer is
// filled up and that uninitalized slots aren't used.
m_TimestampCount = Mathf.Min(m_TimestampCount + 1,
m_BufferedState.Length);
13
// Go through buffer and find correct state to play back
for (int i=0;i<m_TimestampCount;i++)
{
if (m_BufferedState[i].timestamp <= interpolationTime || i ==
m_TimestampCount-1)
{
// The state one slot newer (<100ms) than the best playback state
State rhs = m_BufferedState[Mathf.Max(i-1, 0)];
// The best playback state (closest to 100 ms old (default time))
State lhs = m_BufferedState[i];
14
rigidbody.rotation = angularRotation * latest.rot;
rigidbody.velocity = latest.velocity;
rigidbody.angularVelocity = latest.angularVelocity;
}
}
}
}
Select your SpaceCraftFBX object in the Hierarchy (not in the Project files).
Create a new Prefab, name it SpaceCraft and move it to your Prefabs folder.
Drag your SpaceCraftFBX from the Hierarchy to your new Prefab (SpaceCraft)
in Project window.
Create the Tag "SpaceCraft" for your SpaceCraft Prefab: Edit -> Project Set-
tings -> Tags.
15
Select your SpaceCraft Prefab again and assign the Tag to it: click on "Un-
tagged" and select "SpaceCraft".
Add the file to the Spawn object in the Hierarchy and write this code to it:
function OnNetworkLoadedLevel () {
// Instantiating SpaceCraft when Network is loaded
Network.Instantiate(SpaceCraft, transform.position, transform.rotation, 0);
}
16
Debug.Log("Server destroying player");
Network.RemoveRPCs(player, 0);
Network.DestroyPlayerObjects(player);
}
Select the Spawn object in the Hierarchy and set the Space Craft parameter to
SpaceCraft (Transform) (select your SpaceCraft Prefab from list).
function LateUpdate () {
if(GameObject.FindWithTag("SpaceCraft"))
{
if (!target)
target = GameObject.FindWithTag("SpaceCraft").transform;
17
transform.position = pos;
// Always look at the target
transform.LookAt (target);
}
}
Add the Player Controls script to the Main Camera and open it to edit.
function Awake () {
if (horizontalOrientation)
{
iPhoneSettings.screenOrientation =
iPhoneScreenOrientation.LandscapeLeft;
}
else
{
iPhoneSettings.screenOrientation =
iPhoneScreenOrientation.Portrait;
}
guiSpeedElement = GameObject.Find("speed").transform;
guiSpeedElement.position = new Vector3 (0, normalizedSpeed, 0);
}
function FixedUpdate () {
if(GameObject.FindWithTag("SpaceCraft"))
{
GameObject.FindWithTag("SpaceCraft").rigidbody.AddRelativeForce(0, 0,
normalizedSpeed * (forwardForce*3));
18
if (horizontalOrientation)
{
var t : float = accelerator.x;
accelerator.x = -accelerator.y;
accelerator.y = t;
}
GameObject.FindWithTag("SpaceCraft").transform.rotation = Quaternion.Lerp
(transform.rotation, rot, sensitivity);
}
}
function Update () {
for (var evt : iPhoneTouch in iPhoneInput.touches)
{
if (evt.phase == iPhoneTouchPhase.Moved)
{
normalizedSpeed = evt.position.y / Screen.height;
guiSpeedElement.position = new Vector3 (0, normalizedSpeed, 0);
}
}
}
Add a Missile Launcher script to the Main Camera. Set the Missile parameter
to missilePrefab.
We will add a timer for shooting and some small changes as well.
function FixedUpdate() {
timer++;
}
19
function Update () {
if ((Input.GetMouseButtonDown (0))&&(timer>10))
{
// if SpaceCraft exists
if(GameObject.FindWithTag("SpaceCraft"))
{
var position : Vector3 = new Vector3(0, -0.2, 1) * 10.0;
position = GameObject.FindWithTag("SpaceCraft").transform.TransformPoint
(position);
// instantiating missile
var thisMissile : GameObject = Network.Instantiate (missile, position,
GameObject.FindWithTag("SpaceCraft").transform.rotation,0) as GameObject;
Physics.IgnoreCollision(thisMissile.collider,
GameObject.FindWithTag("SpaceCraft").collider);;
timer = 0;
}
}
}
if (collision.gameObject.tag == "SpaceCraft")
{
Instantiate (explosion, contact.point + (contact.normal * 5.0) ,
camera.main.transform.rotation);
collision.gameObject.transform.position =
GameObject.Find("Spawn").transform.position;
}
Destroy (gameObject);
}
20
}
}
function FixedUpdate () {
if(GameObject.FindWithTag("Missile"))
{
rigidbody.AddForce (transform.TransformDirection (Vector3.forward +
Vector3(0,0.1,0)) * 720.0);
}
}
Create a new scene and save it as ServerChoose: File -> New Scene, File ->
Save Scene. The scene will be used for choosing server type.
Create a new scene and save it as UDPServer. This scene will be used for UDP
Broadcast connection.
Create a new scene and save it as MasterServer. This scene will be used for
Master Server.
Create a new scene again, save as EmptyScene. We will use this scene when
player has disconnected to clear up leftovers before new connection.
Add all your scenes to the build settings: File -> Build Settings -> Add Open
Scene. Your scene "ServerChoose" should be first in the list, for example:
• PS: you can create a folder and put all scenes together in one place.
21
Open the ServerChoose scene.
Create a new JavaScript file and name it Menu. We will use this scene and this
script for choosing a type of server and connection.
Add this script to the Main Camera and add the following code:
function OnGUI() {
GUI.Label(new Rect((Screen.width/2)-80,(Screen.height/2)-130,200,50),"SELECT
CONNECTION TYPE");
GUI.Label(new Rect((Screen.width-220),(Screen.height-30),220,30),"STAR-TROOPER
MULTIPLAYER DEMO");
if(GUI.Button(new Rect((Screen.width/2)-100,(Screen.height/
2)-100,200,50),"Master Server Connection"))
{
Application.LoadLevel("MasterServer");
}
if(GUI.Button(new Rect((Screen.width/2)-100,(Screen.height/2)-40,200,50),"Direct
Connection"))
{
Application.LoadLevel("StarTrooper");
}
if(GUI.Button(new Rect((Screen.width/2)-100,(Screen.height/2)+20,200,50),"UDP
Connection"))
{
Application.LoadLevel("UDPServer");
}
}
Create a new JavaScript file and name it NetworkLevelLoad. We will use this
script for loading the StarTrooper scene and objects to the network.
// Keep track of the last level prefix (increment each time a new level loads)
private var lastLevelPrefix = 0;
function Awake () {
// Network level loading is done in a seperate channel.
DontDestroyOnLoad(this);
networkView.group = 1;
22
Application.LoadLevel("EmptyScene");
}
function OnGUI () {
// When network is running (server or client) then display the level "StarTrooper"
if (Network.peerType != NetworkPeerType.Disconnected)
{
if (GUI.Button(new Rect(350,10,100,30),"StarTrooper"))
{
// Make sure no old RPC calls are buffered and then send load level command
Network.RemoveRPCsInGroup(0);
Network.RemoveRPCsInGroup(1);
// Load level with incremented level prefix (for view IDs)
networkView.RPC( "LoadLevel", RPCMode.AllBuffered, "StarTrooper",
lastLevelPrefix + 1);
}
}
}
@RPC
function LoadLevel (level : String, levelPrefix : int) {
Debug.Log("Loading level " + level + " with prefix " + levelPrefix);
lastLevelPrefix = levelPrefix;
// There is no reason to send any more data over the network on the default
channel,
// because we are about to load the level, because all those objects will get deleted
anyway
Network.SetSendingEnabled(0, false);
// All network views loaded from a level will get a prefix into their NetworkViewID.
// This will prevent old updates from clients leaking into a newly created scene.
Network.SetLevelPrefix(levelPrefix);
Application.LoadLevel(level);
yield;
yield;
// Notify our objects that the level and the network is ready
23
var go : Transform[] = FindObjectsOfType(Transform);
var go_len = go.length;
function OnDisconnectedFromServer () {
Application.LoadLevel("EmptyScene");
}
@script RequireComponent(NetworkView)
• Ensure that this script has added a NetworkView automatically, otherwise add Net-
workLevelLoad from the menu (Component->Scripts->NetworkLevelLoad).
Create a new JavaScript file and name it MasterServerGUI. With this script we
will create a Master Server/Client and some GUI to use it.
Add this file to the ConnectionGUI object and open the script to edit:
DontDestroyOnLoad(this);
24
function OnFailedToConnect(info: NetworkConnectionError) {
Debug.Log(info);
}
function OnGUI () {
ShowGUI();
}
function Awake () {
// Start connection test
natCapable = Network.TestConnection();
// What kind of IP does this machine have? TestConnection also indicates this in the
// test results
if (Network.HavePublicAddress())
Debug.Log("This machine has a public IP address");
else
Debug.Log("This machine has a private IP address");
}
function Update() {
// If test is undetermined, keep running
if (!doneTesting) {
TestConnection();
}
}
function TestConnection() {
// Start/Poll the connection test, report the results in a label and react to the results
accordingly
natCapable = Network.TestConnection();
switch (natCapable) {
case ConnectionTesterStatus.Error:
testMessage = "Problem determining NAT capabilities";
doneTesting = true;
break;
case ConnectionTesterStatus.Undetermined:
testMessage = "Undetermined NAT capabilities";
doneTesting = false;
break;
case ConnectionTesterStatus.PrivateIPNoNATPunchthrough:
testMessage = "Cannot do NAT punchthrough, filtering NAT enabled hosts for
client connections,"
+" local LAN games only.";
filterNATHosts = true;
25
Network.useNat = true;
doneTesting = true;
break;
case ConnectionTesterStatus.PrivateIPHasNATPunchThrough:
if (probingPublicIP)
testMessage = "Non-connectable public IP address (port "+ serverPort +"
blocked),"
+" NAT punchthrough can circumvent the firewall.";
else
testMessage = "NAT punchthrough capable. Enabling NAT punchthrough
functionality.";
// NAT functionality is enabled in case a server is started,
// clients should enable this based on if the host requires it
Network.useNat = true;
doneTesting = true;
break;
case ConnectionTesterStatus.PublicIPIsConnectable:
testMessage = "Directly connectable public IP address.";
Network.useNat = false;
doneTesting = true;
break;
26
case ConnectionTesterStatus.PublicIPNoServerStarted:
testMessage = "Public IP address but server not initialized,"
+"it must be started to check server accessibility. Restart connection test
when ready.";
break;
default:
testMessage = "Error in test routine, got " + natCapable;
}
}
function ShowGUI() {
if (Network.peerType == NetworkPeerType.Disconnected)
{
// Start a new server
if (GUI.Button(new Rect(10,10,90,30),"Start Server"))
{
Network.InitializeServer(32, serverPort);
MasterServer.updateRate = 3;
MasterServer.RegisterHost(gameName, "stuff", "profas chat test");
}
// Refresh hosts
if (GUI.Button(new Rect(10,40,210,30),"Refresh available Servers")
|| Time.realtimeSinceStartup > lastHostListRequest + hostListRefreshTimeout)
{
MasterServer.ClearHostList();
MasterServer.RequestHostList (gameName);
lastHostListRequest = Time.realtimeSinceStartup;
Debug.Log("Refresh Click");
}
27
var name = element.gameName + " " + element.connectedPlayers + " / "
+ element.playerLimit;
var hostInfo;
hostInfo = "[";
// Here we display all IP addresses, there can be multiple in cases where
// internal LAN connections are being attempted. In the GUI we could just
display
// the first one in order not confuse the end user, but internally Unity will
// do a connection check on all IP addresses in the element.ip list, and
connect to the
// first valid one.
for (var host in element.ip)
{
hostInfo = hostInfo + host + ":" + element.port + " ";
}
hostInfo = hostInfo + "]";
if (GUI.Button(new Rect(20,(_cnt*50)+90,400,40),hostInfo.ToString()))
{
// Enable NAT functionality based on what the hosts if configured to
do
Network.useNat = element.useNat;
if (Network.useNat)
print("Using Nat punchthrough to connect");
else
print("Connecting directly to host");
Network.Connect(element.ip, element.port);
}
}
}
}
else
{
if (GUI.Button (new Rect(10,10,90,30),"Disconnect"))
{
Network.Disconnect();
MasterServer.UnregisterHost();
}
}
}
• Now you can test your project using the Master Server:
Go to Edit -> Project Settings -> Player and set up your iPhone Bundle Iden-
tifier and change the Default Screen Orientation to Landscape Left.
28
Build your project to the iPhone.
The server should appear on iPhone (if not, click Refresh available Servers).
29
• Now we need to create a second connection type - UDP Broadcast connection:
Assign the UDPConnectionGUI file to the UDPServer object. (Or assign it after
you write the following code).
using UnityEngine;
using System.Collections;
using System.Net;
using System.Net.Sockets;
using System.Threading;
30
}
try
{
while(continueLoop)
{
byte[] recData = client.Receive(ref receivePoint);
server_name = encode.GetString(recData);
if(connected)
{
server_name = "";
client.Close();
break;
}
}
} catch {}
}
31
server.Send(sendData,sendData.Length,ip_broadcast,System.Convert.ToInt32(port));
Thread.Sleep(100);
}
} catch {}
}
void OnGUI() {
if(!youServer)
{
if(GUI.Button(new Rect(10,10,100,30),"Start Server"))
{
youServer = true;
Network.InitializeServer(32, listenPort);
string ipaddress = Network.player.ipAddress.ToString();
ip = ipaddress;
client.Close();
server = new UdpClient(System.Convert.ToInt32(port));
receivePoint = new
IPEndPoint(IPAddress.Parse(ipaddress),System.Convert.ToInt32(port));
Thread startServer = new Thread(new ThreadStart(start_server));
startServer.Start();
}
if(server_name!="")
{
if(GUI.Button(new Rect(20,100,200,50),server_name))
{
connected = true;
Network.Connect(server_name, listenPort);
}
}
}
else
{
if(GUI.Button(new Rect(10,10,100,30),"Disconnect"))
{
Network.Disconnect();
youServer = false;
server.Close();
LoadClient();
}
}
}
}
32
• Now you can test this connection type:
Set your Player Settings (if you haven't changed them yet) as instructed
above.
Connect to the server from the iPhone when the IP address button appears.
Assign the ConnectionGUI.js file to the Main Camera and write the following:
function Awake() {
if (FindObjectOfType(MasterServerGUI))
this.enabled = false;
33
if(FindObjectOfType(UDPConnectionGUI))
this.enabled = false;
}
function OnGUI () {
if (Network.peerType == NetworkPeerType.Disconnected)
{
// If not connected
// Notify our objects that the level and the network is ready
for (var go : GameObject in FindObjectsOfType(GameObject))
{
go.SendMessage("OnNetworkLoadedLevel",
SendMessageOptions.DontRequireReceiver);
}
}
34
}
}
function OnConnectedToServer() {
// Notify our objects that the level and the network is ready
for (var go : GameObject in FindObjectsOfType(GameObject))
go.SendMessage("OnNetworkLoadedLevel",
SendMessageOptions.DontRequireReceiver);
}
function OnDisconnectedFromServer () {
if (this.enabled != false)
Application.LoadLevel(Application.loadedLevel);
else
{
var _NetworkLevelLoad : NetworkLevelLoad =
FindObjectOfType(NetworkLevelLoad);
_NetworkLevelLoad.OnDisconnectedFromServer();
}
}
Set your Player Settings (if you haven't changed them yet) as instructed
above.
Enter the IP address on the iPhone in the IP field. You can get the IP address
from the Editor Player screen. Click Connect.
35
We have finished our Client and Server with three
connection types.
36
4.3.3 Final Polishing
• Our project is almost finished now. To make our game more playable we need to
add some final polish:
Select the SpaceCraft Prefab and set the Transform Scale parameters to:
1.8,1.8,1.8.
Select the Main Camera in the Hierarchy and change the public parameters in
the Player Controls script: Turn Speed: 3, Max Turn Lean: 70, Max Tilt: 50,
Sensitivity: 0.5, Forward Force: 20.
In the Main Camera open MissileLauncher.js script. At line 16, change the
vector from Vector3(0,-0.2,1) to Vector3(0,-0.3,0.5).
Select the missilePrefab Prefab and set the Transform Scale parameters to:
8,8,8.
• PS: These last modifications are based only on my taste, so feel free to make your
own modifications or new features.
37
4.3 Quick Overview
38