Book Image

Unity Multiplayer Games

By : Alan R. Stagner
Book Image

Unity Multiplayer Games

By: Alan R. Stagner

Overview of this book

Unity is a game development engine that is fully integrated with a complete set of intuitive tools and rapid workflows used to create interactive 3D content. Multiplayer games have long been a staple of video games, and online multiplayer games have seen an explosion in popularity in recent years. Unity provides a unique platform for independent developers to create the most in-demand multiplayer experiences, from relaxing social MMOs to adrenaline-pumping competitive shooters. A practical guide to writing a variety of online multiplayer games with the Unity game engine, using a multitude of networking middleware from player-hosted games to standalone dedicated servers to cloud multiplayer technology. You can create a wide variety of online games with the Unity 4 as well as Unity 3 Engine. You will learn all the skills needed to make any multiplayer game you can think of using this practical guide. We break down complex multiplayer games into basic components, for different kinds of games, whether they be large multi-user environments or small 8-player action games. You will get started by learning networking technologies for a variety of situations with a Pong game, and also host a game server and learn to connect to it.Then, we will show you how to structure your game logic to work in a multiplayer environment. We will cover how to implement client-side game logic for player-hosted games and server-side game logic for MMO-style games, as well as how to deal with network latency, unreliability, and security. You will then gain an understanding of the Photon Server while creating a star collector game; and later, the Player.IO by creating a multiplayer RTS prototype game. You will also learn using PubNub with Unity by creating a chatbox application. Unity Multiplayer Games will help you learn how to use the most popular networking middleware available for Unity, from peer-oriented setups to dedicated server technology.
Table of Contents (14 chapters)
Unity Multiplayer Games
Credits
About the Author
About the Reviewers
www.PacktPub.com
Preface
Index

Creating a multiplayer Pong game


Now that we've covered the basics of using Unity Networking, we're going to apply them to creating a multiplayer Pong clone.

The game will play pretty much as standard Pong. Players can choose their name, and then view a list of open servers (full rooms will not be shown). Players can also host their own game.

Once in a game, players bounce a ball back and forth until it hits the opponent's side. Players get one point for this, and the ball will reset and continue bouncing. When a player hits 10 points, the winner is called, the scores are reset, and the game continues. While in a match with no other players, the server will inform the user to wait. If a player leaves, the match is reset (if the host leaves, the other player is automatically disconnected).

Preparing the Field

First, create a cube (by navigating to GameObject | Create Other | Cube) and scale it to 1 x 1 x 4. Name it Paddle and set the Tag to Player. Check the Is Trigger box on the collider.

Our ball will detect when it hits the trigger zone on the player paddle, and reverse direction. We use triggers because we don't necessarily want to simulate the ball realistically with the Unity physics engine (we get far less control over the ball's physics, and it may not behave exactly as we would like).

We will also line our playing field in trigger boxes. For these you can duplicate the paddle four times and form a large rectangle outlining the playing field. The actual size doesn't matter so much, as long as the ball has room to move around. We will add two more tags for these boundaries: Boundary and Goal. The two boxes on the top and bottom of the field are tagged as Boundary, the two boxes on the left and right are tagged as Goal.

When the ball hits a trigger tagged Boundary, it reverses its velocity along the z axis. When the ball hits a trigger tagged Player, it reverses its velocity along the x axis. And when a ball hits a trigger tagged Goal, the corresponding player gets a point and the ball resets.

Let's finish up the playing field before writing our code:

  1. Firstly, set the camera to Orthographic and position it at (0, 10, 0). Rotate it 90 degrees along the x axis until it points straight down, and change its Orthographic Size to a value large enough to frame the playing field (in my case, I set it to 15). Set the camera's background color to black.

  2. Create a directional light that points straight down. This will illuminate the paddles and ball to make them pure white.

  3. Finally, duplicate the player paddle and move it to the other half of the field.

The Ball script

Now we're going to create the Ball script. We'll add the multiplayer code later, for now this is offline only:

using UnityEngine;
using System.Collections;

public class Ball : MonoBehavior
{
  // the speed the ball starts with
  public float StartSpeed = 5f;

  // the maximum speed of the ball
  public float MaxSpeed = 20f;

  // how much faster the ball gets with each bounce
  public float SpeedIncrease = 0.25f;

  // the current speed of the ball
  private float currentSpeed;

  // the current direction of travel
  private Vector2 currentDir;

  // whether or not the ball is resetting
  private bool resetting = false;

  void Start()
  {
    // initialize starting speed
    currentSpeed = StartSpeed;

    // initialize direction
    currentDir = Random.insideUnitCircle.normalized;
  }

  void Update()
  {
    // don't move the ball if it's resetting
    if( resetting )
      return;

    // move the ball in the current direction
    Vector2 moveDir = currentDir * currentSpeed * Time.deltaTime;
    transform.Translate( new Vector3( moveDir.x, 0f, moveDir.y ) );
  }

  void OnTriggerEnter( Collider other )
  {
    if( other.tag == "Boundary" )
    {
      // vertical boundary, reverse Y direction
      currentDir.y *= -1;
    }
    else if( other.tag == "Player" )
    {
      // player paddle, reverse X direction
      currentDir.x *= -1;
    }
    else if( other.tag == "Goal" )
    {
      // reset the ball
      StartCoroutine( resetBall() );
      // inform goal of the score
      other.SendMessage( "GetPoint", SendMessageOptions.DontRequireReceiver );
    }

    // increase speed
    currentSpeed += SpeedIncrease;
    
    // clamp speed to maximum
    currentSpeed = Mathf.Clamp( currentSpeed, StartSpeed, MaxSpeed );
  }

  IEnumerator resetBall()
  {
    // reset position, speed, and direction
    resetting = true;
    transform.position = Vector3.zero;
    
    currentDir = Vector3.zero;
    currentSpeed = 0f;
    // wait for 3 seconds before starting the round
    yield return new WaitForSeconds( 3f );

Start();

    resetting = false;
  }
}

To create the ball, as before we'll create a cube. It will have the default scale of 1 x 1 x 1. Set the position to origin (0, 0, 0). Add a rigidbody component to the cube, untick the Use Gravity checkbox, and tick the Is Kinematic checkbox. The Rigidbody component is used to let our ball get the OnTriggerEnter events. Is Kinematic is enabled because we're controlling the ball ourselves, rather than using Unity's physics engine.

Add the new Ball component that we just created and test the game. It should look something like this:

You should see the ball bouncing around the field. If it hits either side, it will move back to the center of the field, pause for 3 seconds, and then begin moving again. This should happen fairly quickly, because the paddles aren't usable yet (the ball will often bounce right past them).

The Paddle script

Let's add player control to the mix. Note that at the moment player paddles will both move in tandem, with the same controls. This is OK, later we'll disable the player input based on whether or not the network view belongs to the local client (this is what the AcceptsInput field is for):

using UnityEngine;
using System.Collections;

public class Paddle : MonoBehavior
{
  // how fast the paddle can move
  public float MoveSpeed = 10f;

  // how far up and down the paddle can move
  public float MoveRange = 10f;

  // whether this paddle can accept player input
  public bool AcceptsInput = true;

  void Update()
  {
    // does not accept input, abort
    if( !AcceptsInput )
      return;

    //get user input
    float input = Input.GetAxis( "Vertical" );
    
    // move paddle
    Vector3 pos = transform.position;
    pos.z += input * MoveSpeed * Time.deltaTime;

    // clamp paddle position
    pos.z = Mathf.Clamp( pos.z, -MoveRange, MoveRange );

    // set position
    transform.position = pos;
  }
}

You can now move the paddles up and down, and bounce the ball back and forth. The ball will slowly pick up speed as it bounces, until it hits either of the goals. When that happens, the round resets.

Keeping score

What we're going to do now is create a scorekeeper. The scorekeeper will keep track of both players' scores, and will later keep track of other things, such as whether we're waiting for another player to join:

using UnityEngine;
using System.Collections;

public class Scorekeeper : MonoBehavior
{
  // the maximum score a player can reach
  public int ScoreLimit = 10;

  // Player 1's score
  private int p1Score = 0;

  // Player 2's score
  private int p2Score = 0;

  // give the appropriate player a point
  public void AddScore( int player )
  {
    // player 1
    if( player == 1 )
    {
      p1Score++;
    }
    // player 2
    else if( player == 2 )
    {
      p2Score++;
    }

    // check if either player reached the score limit
    if( p1Score >= ScoreLimit || p2Score >= ScoreLimit )
    {
      // player 1 has a better score than player 2
      if( p1score > p2score )
        Debug.Log( "Player 1 wins" );
      // player 2 has a better score than player 1
      if( p2score > p1score )
        Debug.Log( "Player 2 wins" );
      // both players have the same score - tie
      else
        Debug.Log( "Players are tied" );

      // reset scores and start over
      p1Score = 0;
      p2Score = 0;
    }
  }
}

Now our scorekeeper can keep score for each player, let's make the goals and add points with a Goal script. It's a very simple script, which reacts to the GetPoint message sent from the ball upon collision to give the other player a point:

using UnityEngine;
using System.Collections;

public class Goal : MonoBehavior
{
  // the player who gets a point for this goal, 1 or 2
  public int Player = 1;

  // the Scorekeeper
  public Scorekeeper scorekeeper;

  public void GetPoint()
  {
    // when the ball collides with this goal, give the player a point
    scorekeeper.AddScore( Player );
  }
}

Attach this script to both goals. For player 1's goal, set the Player to 2 (player 2 gets a point when the ball lands in player 1's goal), for player 2's goal, set the Player to 1 (player 1 gets a point when the ball lands in player 2's goal).

The game is almost completely functional now (aside from multiplayer). One problem is that we can't tell that points are being given until the game ends, so let's add a score display.

Displaying the score to the player

Create two 3D Text objects as children of the scorekeeper. Name them p1Score and P2Score, and position them on each side of the field:

Let's make the scorekeeper display the player scores:

using UnityEngine;
using System.Collections;

public class Scorekeeper : MonoBehavior
{
  // the maximum score a player can reach
  public int ScoreLimit = 10;

  // the display test for player 1's score
  public TextMesh Player1ScoreDisplay;

  // the display text for player 2's score
  public TextMesh Player2ScoreDisplay;

  // Player 1's score
  private int p1Score = 0;

  // Player 2's score
  private int p2Score = 0;

  // give the appropriate player a point
  public void AddScore( int player )
  {
    // player 1
    if( player == 1 )
    {
      p1Score++;
    }
    // player 2
    else if( player == 2 )
    {
      p2Score++;
    }

    // check if either player reached the score limit
    if( p1Score >= ScoreLimit || p2Score >= ScoreLimit )
    {
      // player 1 has a better score than player 2
      if( p1Score > p2Score )
        Debug.Log( "Player 1 wins" );
      // player 2 has a better score than player 1
      if( p2Score > p1Score )
        Debug.Log( "Player 2 wins" );
      // both players have the same score - tie
      else
        Debug.Log( "Players are tied" );

      // reset scores and start over
      p1Score = 0;
      p2Score = 0;
    }

    // display each player's score
    Player1ScoreDisplay.text = p1Score.ToString();
 
   Player2ScoreDisplay.text = p2Score.ToString();
  }
}

The score is now displayed properly when a player gets a point. Be sure to give it a test run—the ball should bounce around the field, and you should be able to deflect the ball with the paddle. If the ball hits player 1's goal, player 2 should get 1 point, and vice versa. If one player gets 10 points, both scores should reset to zero, the ball should move back to the center of the screen, and the game should restart.

With the most important gameplay elements complete, we can start working on multiplayer networking.

Networking the game

For testing purposes, let's launch a network game as soon as the level is launched:

using UnityEngine;
using System.Collections;

public class RequireNetwork : MonoBehavior
{
  void Awake()
  {
    if( Network.peerType == NetworkPeerType.Disconnected )
      Network.InitializeServer( 1, 25005, true );
  }
}

If we start this level without hosting a server first, it will automatically do so for us in ensuring that the networked code still works.

Now we can start converting our code to work in multiplayer.

Let's start by networking the paddle code:

using UnityEngine;
using System.Collections;

public class Paddle : MonoBehavior
{
  // how fast the paddle can move
  public float MoveSpeed = 10f;

  // how far up and down the paddle can move
  public float MoveRange = 10f;

  // whether this paddle can accept player input
  public bool AcceptsInput = true;
  // the position read from the network
  // used for interpolation
  private Vector3 readNetworkPos;

  void Start()
  {
    // if this is our paddle, it accepts input
    // otherwise, if it is someone else's paddle, it does not
    AcceptsInput = networkView.isMine;
  }

  void Update()
  {
    // does not accept input, interpolate network pos
    if( !AcceptsInput )
    {
      transform.position = Vector3.Lerp( transform.position, readNetworkPos, 10f * Time.deltaTime );

      // don't use player input
      return;
    }

    //get user input
    float input = Input.GetAxis( "Vertical" );
    
    // move paddle
    Vector3 pos = transform.position;
    pos.z += input * MoveSpeed * Time.deltaTime;

    // clamp paddle position
    pos.z = Mathf.Clamp( pos.z, -MoveRange, MoveRange );

    // set position
    transform.position = pos;
  }

  void OnSerializeNetworkView( BitStream stream )
  {
    // writing information, push current paddle position
    if( stream.isWriting )
    {
      Vector3 pos = transform.position;
      stream.Serialize( ref pos );
    }
    // reading information, read paddle position
    else
    {
      Vector3 pos = Vector3.zero;
      stream.Serialize( ref pos );
      readNetworkPos = pos;
    }
  }
}

The paddle will detect whether it is owned by the local player or not. If not, it will not accept player input, instead it will interpolate its position to the last read position value over the network.

By default, network views will serialize the attached transform. This is OK for testing, but should not be used for production. Without any interpolation, the movement will appear very laggy and jerky, as positions are sent a fixed number of times per second (15 by default in Unity Networking) in order to save on bandwidth, so snapping to the position 15 times per second will look jerky. In order to solve this, rather than instantly snapping to the new position we smoothly interpolate towards it. In this case, we use the frame delta multiplied by a number (larger is faster, smaller is slower), which produces an easing motion; the object starts quickly approaching the target value, slowing down as it gets closer.

When serializing, it either reads the position and stores it, or it sends the current transform position, depending on whether the stream is for reading or for writing.

Now, add a Network View to one of your paddles, drag the panel component attached to the Paddle into the Observed slot, and make it a prefab by dragging it into your Project pane.

Next, delete the paddles in the scene, and create two empty game objects where the paddles used to be positioned. These will be the starting points for each paddle when spawned.

Spawning paddles

Next, let's make the scorekeeper spawn these paddles. The scorekeeper, upon a player connecting, will send an RPC to them to spawn a paddle:

using UnityEngine;
using System.Collections;

public class Scorekeeper : MonoBehavior
{
  // the maximum score a player can reach
  public int ScoreLimit = 10;

  // the start points for each player paddle
  public Transform SpawnP1;
  public Transform SpawnP2;

  // the paddle prefab
  public GameObject paddlePrefab;

  // the display test for player 1's score
  public TextMesh Player1ScoreDisplay;

  // the display text for player 2's score
  public TextMesh Player2ScoreDisplay;

  // Player 1's score
  private int p1Score = 0;

  // Player 2's score
  private int p2Score = 0;

  void Start()
  {
    if( Network.isServer )
    {
      // server doesn't trigger OnPlayerConnected, manually spawn
      Network.Instantiate( paddlePrefab, SpawnP1.position, Quaternion.identity, 0 );
    }
  }

void OnPlayerConnected( NetworkPlayer player )
  {
    // when a player joins, tell them to spawn
    networkView.RPC( "net_DoSpawn", player, SpawnP2.position );
  }

  [RPC]
  void net_DoSpawn( Vector3 position )
  {
    // spawn the player paddle
    Network.Instantiate( paddlePrefab, position, Quaternion.identity, 0 );
  }

  // give the appropriate player a point
  public void AddScore( int player )
  {
    // player 1
    if( player == 1 )
    {
      p1Score++;
    }
    // player 2
    else if( player == 2 )
    {
      p2Score++;
    }

    // check if either player reached the score limit
    if( p1Score >= ScoreLimit || p2Score >= ScoreLimit )
    {
      // player 1 has a better score than player 2
      if( p1Score > p2Score )
        Debug.Log( "Player 1 wins" );
      // player 2 has a better score than player 1
      if( p2Score > p1Score )
        Debug.Log( "Player 2 wins" );
      // both players have the same score - tie
      else
        Debug.Log( "Players are tied" );

      // reset scores and start over
      p1Score = 0;
      p2Score = 0;
    }

    // display each player's score
    Player1ScoreDisplay.text = p1Score.ToString();
    Player2ScoreDisplay.text = p2Score.ToString();
  }
}

At the moment, when you start the game, one paddle spawns for player 1, but player 2 is missing (there's nobody else playing). However, the ball eventually flies off toward player 2's side, and gives player 1 a free point.

The networked ball

Let's keep the ball frozen in place when there's nobody to play against, or if we aren't the server. We're also going to add networked movement to our ball:

using UnityEngine;
using System.Collections;

public class Ball : MonoBehavior
{
  // the speed the ball starts with
  public float StartSpeed = 5f;

  // the maximum speed of the ball
  public float MaxSpeed = 20f;

  // how much faster the ball gets with each bounce
  public float SpeedIncrease = 0.25f;

  // the current speed of the ball
  private float currentSpeed;

  // the current direction of travel
  private Vector2 currentDir;

  // whether or not the ball is resetting
  private bool resetting = false;

  void Start()
  {
    // initialize starting speed
    currentSpeed = StartSpeed;

    // initialize direction
    currentDir = Random.insideUnitCircle.normalized;
  }

  void Update()
  {
    // don't move the ball if it's resetting
    if( resetting )
      return;

    // don't move the ball if there's nobody to play with
    if( Network.connections.Length == 0 )
      return;

    // move the ball in the current direction
    Vector2 moveDir = currentDir * currentSpeed * Time.deltaTime;
    transform.Translate( new Vector3( moveDir.x, 0f, moveDir.y ) );
  }

  void OnTriggerEnter( Collider other )
  {
    // bounce off the top and bottom walls
    if( other.tag == "Boundary" )
    {
      // vertical boundary, reverse Y direction
      currentDir.y *= -1;
    }
    // bounce off the player paddle
    else if( other.tag == "Player" )
    {
      // player paddle, reverse X direction
      currentDir.x *= -1;
    }
    // if we hit a goal, and we are the server, give the appropriate player a point
    else if( other.tag == "Goal" && Network.isServer )
    {
      // reset the ball
      StartCoroutine( resetBall() );
      // inform goal of the score
      other.SendMessage( "GetPoint", SendMessageOptions.DontRequireReceiver );
    }

    // increase speed
    currentSpeed += SpeedIncrease;
    
    // clamp speed to maximum
    currentSpeed = Mathf.Clamp( currentSpeed, StartSpeed, MaxSpeed );
  }
  IEnumerator resetBall()
  {
    // reset position, speed, and direction
    resetting = true;
    transform.position = Vector3.zero;

    currentDir = Vector3.zero;
    currentSpeed = 0f;

    // wait for 3 seconds before starting the round
    yield return new WaitForSeconds( 3f );

    Start();

    resetting = false;
  }

  void OnSerializeNetworkView( BitStream stream )
  {
    //write position, direction, and speed to network
    if( stream.isWriting )
    {
      Vector3 pos = transform.position;  
      Vector3 dir = currentDir;
      float speed = currentSpeed;
      stream.Serialize( ref pos );
      stream.Serialize( ref dir );
      stream.Serialize( ref speed );
    }
    // read position, direction, and speed from network
    else
    {
      Vector3 pos = Vector3.zero;
      Vector3 dir = Vector3.zero;
      float speed = 0f;
      stream.Serialize( ref pos );
      stream.Serialize( ref dir );
      stream.Serialize( ref speed );
      transform.position = pos;
      currentDir = dir;
      currentSpeed = speed;  
    }
  }
}

The ball will stay put if there's nobody to play against, and if someone we're playing against leaves, the ball will reset to the middle of the field. The ball will also work correctly on multiple machines at once (it is simulated on the server, and position/velocity is relayed to clients). Add NetworkView to the ball and have it observe the Ball component.

Networked scorekeeping

There is one final piece of the puzzle that is keeping score. We're going to convert our AddScore function to use an RPC, and if a player leaves we will also reset the scores:

using UnityEngine;
using System.Collections;

public class Scorekeeper : MonoBehavior
{
  // the maximum score a player can reach
  public int ScoreLimit = 10;

  // the start points for each player paddle
  public Transform SpawnP1;
  public Transform SpawnP2;

  // the paddle prefab
  public GameObject paddlePrefab;

  // the display test for player 1's score
  public TextMesh Player1ScoreDisplay;

  // the display text for player 2's score
  public TextMesh Player2ScoreDisplay;

  // Player 1's score
  private int p1Score = 0;

  // Player 2's score
  private int p2Score = 0;

  void Start()
  {
    if( Network.isServer )
    {
      // server doesn't trigger OnPlayerConnected, manually spawn
      Network.Instantiate( paddlePrefab, SpawnP1.position, Quaternion.identity, 0 );

      // nobody has joined yet, display "Waiting..." for player 2
      Player2ScoreDisplay.text = "Waiting...";
    }
  }

void OnPlayerConnected( NetworkPlayer player )
  {
    // when a player joins, tell them to spawn
    networkView.RPC( "net_DoSpawn", player, SpawnP2.position );

    // change player 2's score display from "waiting..." to "0"
    Player2ScoreDisplay.text = "0";
  }

  void OnPlayerDisconnected( NetworkPlayer player )
  {
    // player 2 left, reset scores
    p1Score = 0;
    p2Score = 0;

    // display each player's scores
    // display "Waiting..." for player 2
    Player1ScoreDisplay.text = p1Score.ToString();
    Player2ScoreDisplay.text = "Waiting...";
  }

  void OnDisconnectedFromServer( NetworkDisconnection cause )
  {
    // go back to the main menu
    Application.LoadLevel( "Menu" );
  }

  [RPC]
  void net_DoSpawn( Vector3 position )
  {
    // spawn the player paddle
    Network.Instantiate( paddlePrefab, position, Quaternion.identity, 0 );
  }
  // call an RPC to give the player a point
  public void AddScore( int player )
  {
    networkView.RPC( "net_AddScore", RPCMode.All, player );
  }

  // give the appropriate player a point
  [RPC]
  public void net_AddScore( int player )
  {
    // player 1
    if( player == 1 )
    {
      p1Score++;
    }
    // player 2
    else if( player == 2 )
    {
      p2Score++;
    }

    // check if either player reached the score limit
    if( p1Score >= ScoreLimit || p2Score >= ScoreLimit )
    {
      // player 1 has a better score than player 2
      if( p1Score > p2Score )
        Debug.Log( "Player 1 wins" );
      // player 2 has a better score than player 1
      if( p2Score > p1Score )
        Debug.Log( "Player 2 wins" );
      // both players have the same score - tie
      else
        Debug.Log( "Players are tied" );

      // reset scores and start over
      p1Score = 0;
      p2Score = 0;
    }

    // display each player's score
    Player1ScoreDisplay.text = p1Score.ToString();
    Player2ScoreDisplay.text = p2Score.ToString();
  }
}

Our game is fully networked at this point. The only problem is that we do not yet have a way to connect to the game. Let's write a simple direct connect dialog which allows players to enter an IP address to join.

Note

With direct IP connect, note that NAT punch-through is not possible. When you use the Master Server, you can pass either HostData or GUID of a host which will perform NAT punch-through.

The Connect screen

The following script shows the player IP and Port entry fields, and the Connect and Host buttons. The player can directly connect to an IP and Port, or start a server on the given Port. By using direct connect we don't need to rely on a master server, as players directly connect to games via IP. If you wanted to, you could easily create a lobby screen for this instead of using direct connect (allowing players to browse a list of running servers instead of manually typing IP address). To keep things simpler, we'll omit the lobby screen in this example:

using UnityEngine;
using System.Collections;

public class ConnectToGame : MonoBehavior
{
  private string ip = "";
  private int port = 25005;

  void OnGUI()
  {
    // let the user enter IP address
    GUILayout.Label( "IP Address" );
    ip = GUILayout.TextField( ip, GUILayout.Width( 200f ) );

    // let the user enter port number
    // port is an integer, so only numbers are allowed
    GUILayout.Label( "Port" );
    string port_str = GUILayout.TextField( port.ToString(), GUILayout.Width( 100f ) );
    int port_num = port;
    if( int.TryParse( port_str, out port_num ) )
      port = port_num;
    // connect to the IP and port
    if( GUILayout.Button( "Connect", GUILayout.Width( 100f ) ) )
    {
      Network.Connect( ip, port );
    }

    // host a server on the given port, only allow 1 incoming connection (one other player)
    if( GUILayout.Button( "Host", GUILayout.Width( 100f ) ) )
    {
      Network.InitializeServer( 1, port, true );
    }
  }

  void OnConnectedToServer()
  {
    Debug.Log( "Connected to server" );
    // this is the NetworkLevelLoader we wrote earlier in the chapter – pauses the network, loads the level, waits for the level to finish, and then unpauses the network
    NetworkLevelLoader.Instance.LoadLevel( "Game" );
  }

  void OnServerInitialized()
  {
    Debug.Log( "Server initialized" );
    NetworkLevelLoader.Instance.LoadLevel( "Game" );
  }
}

With this, we now have a complete, fully functional multiplayer Pong game. Players can host games, as well as join them if they know the IP.

When in a game as the host, the game will wait for another player to show up before starting the game. If the other player leaves, the game will reset and wait again. As a player, if the host leaves it goes back to the main menu.