This document describes steps to setup a new multiplayer project from nothing using the new networking system. This step-by-step process is generic, but can be customized for many types of multiplayer games once it is started.
To get started, create a new empty Unity project.
The first step is to create a NetworkManager object in the project:
For more details, see Using the NetworkManager.
The next step is to setup the Unity Prefab that represents the player in the game. By default, the NetworkManager instantiates an object for each player by cloning the player prefab. In this example, the player object will be a simple cube.
See Player Objects.
Once the player prefab is created, it must be registered with the network system.
Now is a good time to save the project for the first time. From the menu File -> Save Project, save the project. You should also save the scene. Lets call this scene the “offline” scene.
The first piece of game functionality is to move the player object. This will first be done without any networking, so it will only work in a single-player mode.
using UnityEngine;
public class PlayerMove : MonoBehaviour
{
void Update()
{
var x = Input.GetAxis("Horizontal")*0.1f;
var z = Input.GetAxis("Vertical")*0.1f;
transform.Translate(x, 0, z);
}
}
This hooks up the cube to be controlled by the arrow keys or a controller pad. The cube only moves on the client right now - it is not networked.
Save the project again.
Enter play mode in the editor by clicking the play button. You should see the NetworkManagerHUD default user interface:
Press “Host” to start the game as the host of the game. This will cause a player object to be created, and the HUD will change to show the server is active. This game is running as a “host” - which is a server and a client in the same process.
See Network Concepts.
Pressing the arrow keys should make the player cube object move around.
Exit play mode by pressing the stop button in the editor.
using UnityEngine;
using UnityEngine.Networking;
public class PlayerMove : NetworkBehaviour
{
void Update()
{
if (!isLocalPlayer)
return;
var x = Input.GetAxis("Horizontal")*0.1f;
var z = Input.GetAxis("Vertical")*0.1f;
transform.Translate(x, 0, z);
}
}
The cubes in the game are currently all white, so the user cannot tell which one is their cube. To identify the player, we will make the cube of the local player red.
public override void OnStartLocalPlayer()
{
GetComponent<MeshRenderer>().material.color = Color.red;
}
This function is only called on the local player on their client. This will make the user see their cube as red. The OnStartLocalPlayer function is a good place to do initialization that is only for the local player, such as configuring cameras and input.
There are also other useful virtual functions on the NetworkBehaviour base class. See Spawning.
A common feature in multiplayer games is to have player fire bullets. This section adds non-networked bullets to the example. Networking for bullets is added in the next section.
using UnityEngine;
using UnityEngine.Networking;
public class PlayerMove : NetworkBehaviour
{
public GameObject bulletPrefab;
public override void OnStartLocalPlayer()
{
GetComponent<MeshRenderer>().material.color = Color.red;
}
void Update()
{
if (!isLocalPlayer)
return;
var x = Input.GetAxis("Horizontal")*0.1f;
var z = Input.GetAxis("Vertical")*0.1f;
transform.Translate(x, 0, z);
if (Input.GetKeyDown(KeyCode.Space))
{
Fire();
}
}
void Fire()
{
// create the bullet object from the bullet prefab
var bullet = (GameObject)Instantiate(
bulletPrefab,
transform.position - transform.forward,
Quaternion.identity);
// make the bullet move away in front of the player
bullet.GetComponent<Rigidbody>().velocity = -transform.forward*4;
// make bullet disappear after 2 seconds
Destroy(bullet, 2.0f);
}
}
This section adds networking to the bullets in the example.
using UnityEngine;
using UnityEngine.Networking;
public class PlayerMove : NetworkBehaviour
{
public GameObject bulletPrefab;
public override void OnStartLocalPlayer()
{
GetComponent<MeshRenderer>().material.color = Color.red;
}
[Command]
void CmdFire()
{
// This [Command] code is run on the server!
// create the bullet object locally
var bullet = (GameObject)Instantiate(
bulletPrefab,
transform.position - transform.forward,
Quaternion.identity);
bullet.GetComponent<Rigidbody>().velocity = -transform.forward*4;
// spawn the bullet on the clients
NetworkServer.Spawn(bullet);
// when the bullet is destroyed on the server it will automaticaly be destroyed on clients
Destroy(bullet, 2.0f);
}
void Update()
{
if (!isLocalPlayer)
return;
var x = Input.GetAxis("Horizontal")*0.1f;
var z = Input.GetAxis("Vertical")*0.1f;
transform.Translate(x, 0, z);
if (Input.GetKeyDown(KeyCode.Space))
{
// Command function is called from the client, but invoked on the server
CmdFire();
}
}
}
This code uses a [Command] to fire the bullet on the server. For more information see Networked Actions.
This adds a collision handler so that bullets will disappear when they hit a player cube object.
using UnityEngine;
public class Bullet : MonoBehaviour
{
void OnCollisionEnter(Collision collision)
{
var hit = collision.gameObject;
var hitPlayer = hit.GetComponent<PlayerMove>();
if (hitPlayer != null)
{
Destroy(gameObject);
}
}
}
Now when a bullet hits a player object it will be destroyed. When the bullet on the server is destroyed, since it is a spawned object managed by the network, it will be destroyed on clients too.
A common feature related to bullets is that the player object has a “health” property that starts at a full value and then is reduced when the player takes damage from a bullet hitting them. This section adds non-networked health to the player object.
using UnityEngine;
public class Combat : MonoBehaviour
{
public const int maxHealth = 100;
public int health = maxHealth;
public void TakeDamage(int amount)
{
health -= amount;
if (health <= 0)
{
health = 0;
Debug.Log("Dead!");
}
}
}
The bullet script needs to be updated to call the TakeDamage function on a hit. * Open the bullet script * Add a call to TakeDamage() from the Combat script in the collision handler function
using UnityEngine;
public class Bullet : MonoBehaviour
{
void OnCollisionEnter(Collision collision)
{
var hit = collision.gameObject;
var hitPlayer = hit.GetComponent<PlayerMove>();
if (hitPlayer != null)
{
var combat = hit.GetComponent<Combat>();
combat.TakeDamage(10);
Destroy(gameObject);
}
}
}
This will make health on the player object go down when hit by a bullet. But you cannot see this happening in the game. We need to add a simple health bar.
This is a lot of code that uses the old GUI system. This is not very relevant for networking so we’ll just use it without explaination for now.
using UnityEngine;
using System.Collections;
public class HealthBar : MonoBehaviour
{
GUIStyle healthStyle;
GUIStyle backStyle;
Combat combat;
void Awake()
{
combat = GetComponent<Combat>();
}
void OnGUI()
{
InitStyles();
// Draw a Health Bar
Vector3 pos = Camera.main.WorldToScreenPoint(transform.position);
// draw health bar background
GUI.color = Color.grey;
GUI.backgroundColor = Color.grey;
GUI.Box(new Rect(pos.x-26, Screen.height - pos.y + 20, Combat.maxHealth/2, 7), ".", backStyle);
// draw health bar amount
GUI.color = Color.green;
GUI.backgroundColor = Color.green;
GUI.Box(new Rect(pos.x-25, Screen.height - pos.y + 21, combat.health/2, 5), ".", healthStyle);
}
void InitStyles()
{
if( healthStyle == null )
{
healthStyle = new GUIStyle( GUI.skin.box );
healthStyle.normal.background = MakeTex( 2, 2, new Color( 0f, 1f, 0f, 1.0f ) );
}
if( backStyle == null )
{
backStyle = new GUIStyle( GUI.skin.box );
backStyle.normal.background = MakeTex( 2, 2, new Color( 0f, 0f, 0f, 1.0f ) );
}
}
Texture2D MakeTex( int width, int height, Color col )
{
Color[] pix = new Color[width * height];
for( int i = 0; i < pix.Length; ++i )
{
pix[ i ] = col;
}
Texture2D result = new Texture2D( width, height );
result.SetPixels( pix );
result.Apply();
return result;
}
}
Changes to health are being applied everywhere now - independently on the client and host. This allows health to look different for different players. Health should only be applied on the server and the changes replicated to clients. We call this “server authority” for health.
For more information on SyncVars, see State Synchronization.
using UnityEngine;
using UnityEngine.Networking;
public class Combat : NetworkBehaviour
{
public const int maxHealth = 100;
[SyncVar]
public int health = maxHealth;
public void TakeDamage(int amount)
{
if (!isServer)
return;
health -= amount;
if (health <= 0)
{
health = 0;
Debug.Log("Dead!");
}
}
}
Currently, nothing currently happens when the health of a player reaches zero except a log message. To make it more of a game, when health reaches zero, the player should be teleported back to the starting location with full health.
using UnityEngine;
using UnityEngine.Networking;
public class Combat : NetworkBehaviour
{
public const int maxHealth = 100;
[SyncVar]
public int health = maxHealth;
public void TakeDamage(int amount)
{
if (!isServer)
return;
health -= amount;
if (health <= 0)
{
health = maxHealth;
// called on the server, will be invoked on the clients
RpcRespawn();
}
}
[ClientRpc]
void RpcRespawn()
{
if (isLocalPlayer)
{
// move back to zero location
transform.position = Vector3.zero;
}
}
}
In this game, the client controls the position of the player object - the player object has “local authority” on the client. If the server just set the player’s position to the start position, it would be overridden by the client, since the client has authority. To avoid this, the server tells the owning client to move the player object to the start position.
While player objects are spawned when client connect to the host, most games have non-player objects that exist in the game world, such as enemies. In this section a spawner is added that creates non-player objects that can be shot and killed.
using UnityEngine;
using UnityEngine.Networking;
public class EnemySpawner : NetworkBehaviour {
public GameObject enemyPrefab;
public int numEnemies;
public override void OnStartServer()
{
for (int i=0; i < numEnemies; i++)
{
var pos = new Vector3(
Random.Range(-8.0f, 8.0f),
0.2f,
Random.Range(-8.0f, 8.0f)
);
var rotation = Quaternion.Euler( Random.Range(0,180), Random.Range(0,180), Random.Range(0,180));
var enemy = (GameObject)Instantiate(enemyPrefab, pos, rotation);
NetworkServer.Spawn(enemy);
}
}
}
Now create an Enemy prefab:
The bullet script was setup to only work for players. Now update the bullet script to work with any object that has the Combat script on it:
using UnityEngine;
public class Bullet : MonoBehaviour
{
void OnCollisionEnter(Collision collision)
{
var hit = collision.gameObject;
var hitCombat = hit.GetComponent<Combat>();
if (hitCombat != null)
{
hitCombat.TakeDamage(10);
Destroy(gameObject);
}
}
}
Hookup the EnemySpawner with the Enemy object:
Test Enemies:
While the enemies can be shot by bullets and their health goes down, then respawn like players. Enemies should be destroyed when their health reaches zero instead of respawning.
using UnityEngine;
using UnityEngine.Networking;
public class Combat : NetworkBehaviour
{
public const int maxHealth = 100;
public bool destroyOnDeath;
[SyncVar]
public int health = maxHealth;
public void TakeDamage(int amount)
{
if (!isServer)
return;
health -= amount;
if (health <= 0)
{
if (destroyOnDeath)
{
Destroy(gameObject);
}
else
{
health = maxHealth;
// called on the server, will be invoked on the clients
RpcRespawn();
}
}
}
[ClientRpc]
void RpcRespawn()
{
if (isLocalPlayer)
{
// move back to zero location
transform.position = Vector3.zero;
}
}
}
Now the enemy will be destroyed when health reaches zero, but players will respawn.
Players currently all appear at the zero point when they are created. This means that they are potentially on top of each other. Player should spawn at different locations. The NetworkStartPosition component can be used to do this.
Create a new empty GameObject
Rename the object to “Pos1”
Choose the Add Component button and add the NetworkStartPosition component
Move the Pos1 object to the position (–3,0,0)
Create a second empty GameObject
Rename the object to “Pos2”
Choose the Add Component button and add the NetworkStartPosition component
Move the Pos2 object to the position (3,0,0)
Find the NetworkManager and select it.
Open the “Spawn Info” foldout
Change the “Player Spawn Method” to “Round Robin”
Build and run the game
Player objects should now be created at the locations of the Pos1 and Pos2 objects instead of at zero.