Introduction into Photon Unity Networking

Photon Unity Networking (PUN) is a Unity package for multiplayer games. It provides authentication options, matchmaking and fast, reliable in-game communication through Exit Games Photon backend.

In this tutorial we will use PUN for simple two player network game. Photon Unity Networking is available on Unity Assets Store. Download and import the package in Unity

After installation, Setup Wizard asks for Photon account email or already register application ID. Cloud with the "Free Plan" is free and without obligation. There is only one step needed to setup Photon plugin, so just enter email address and the Wizard will complete project setup

Let’s setup our scene. There are two boxes with attached Rigidbody and BoxCollider2D components for physics and collision calculations. This will be our simple scene to test multiplayer experience.

Next step we will create NetworkManager script to handle connection between different devices

As we already saved our AppId in the PhotonServerSettings file while Photon Setup, we can now connect by calling PhotonNetwork.ConnectUsingSettings. The gameVersion parameter should be any short string to identify this version.

After connection established we can connect to room JoinOrCreateRoom or create new one if no free rooms are available. There are room options we can specify, in our case we will make public room with players count limited by 2

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon;

public class NetworkManager : PunBehaviour
{
    const int totalPlayers = 2;
    const string roomName = "BoxRoom";

    void Start()
    {
        PhotonNetwork.ConnectUsingSettings("0.1");
    }

    public override void OnConnectedToMaster()
    {
        RoomOptions roomOptions = new RoomOptions() { IsOpen = true, MaxPlayers = totalPlayers };

        PhotonNetwork.JoinOrCreateRoom(roomName, roomOptions, null);
    }

    public override void OnJoinedRoom()
    {
        if (PhotonNetwork.playerList.Length == totalPlayers)
        {
            // Start game
        }
    }

    public override void OnPhotonPlayerConnected(PhotonPlayer newPlayer)
    {
        if (PhotonNetwork.playerList.Length == totalPlayers)
        {
            // Start game
        }
    }
}

At this point we need some place to manage game state. Let’s create empty object with new component attached - GameManager. It will be singleton pattern, there is simplest implementation below:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    #region Singleton

    public static GameManager Instance = null;

    private void Awake()
    {
        if (Instance != null)
        {
            Destroy(gameObject);
        }

        Instance = this;

        DontDestroyOnLoad(gameObject);
    }

    #endregion
}

We need IsPaused property to control game execution. Add code below to GameManager class

public bool IsPaused { get; set; }

private void Start()
{
    // Waiting for players
    IsPaused = true;
}

Game will be paused from start till two players connected. At some point NetworkManager will set IsPaused to false and game boxes will start to fall

// Start game
GameManager.Instance.IsPaused = false;

Now we need a script to control boxes falling inside a scene. Create NetworkBox component and attach to existing boxes.

First of all, to make this class network-enabled change base class from MonoBehaviour to Photon.MonoBehaviour. Also we will need sync data between all networked instances of PhotonView, so we need to implement IPunObservable interface. There is empty NetworkBox class that we created

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NetworkBox : Photon.MonoBehaviour, IPunObservable
{
    void Start()
    {
        
    }
    
    void Update()
    {
        
    }

    #region IPunObservable implementation

    void IPunObservable.OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        throw new System.NotImplementedException();
    }

    #endregion
}

Next step is to add and setup PhotonView component of our boxes. NetworkBox component should be added to Observed Components list of PhotonView. It handles all network communications for our objects.

Currently, our boxes are falling on each device independently, but we need a way to sync their positions. Usually only master client calculates movements and sends new positions to other devices, which interpolate them and move smoothly.

Rigidbody2D rigidbody2d;

void Start()
{
    rigidbody2d = GetComponent<Rigidbody2D>();

    rigidbody2d.simulated = false;
}

void Update()
{
    if (GameManager.Instance.IsPaused)
        return;

    rigidbody2d.simulated = photonView.isMine;
}

OnPhotonSerializeView responds to data synchronization and called several times per second for both sender and receiver. To differentiate what is going now, there is stream.isWriting boolean property exists.

void IPunObservable.OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
    if (stream.isWriting)
    {
        stream.SendNext(transform.position);
        stream.SendNext(transform.rotation);

    }
    else
    {
        transform.position = (Vector3)stream.ReceiveNext();
        transform.rotation = (Quaternion)stream.ReceiveNext();
    }
}

There is a comparison of two instances running side by side. Master game is on the right side. You can see that client game (left side) has small lag and boxes are falling not so smooth. We can interpolate position changes, so boxes will fall more smoothly, but master-client lag will still exists due to network latency.

When new position is received in OnPhotonSerializeView, we save it and next frames we will use it to smoothly interpolate between old and new positions

void IPunObservable.OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
    if (stream.isWriting)
    {
        stream.SendNext(transform.position);
        stream.SendNext(transform.rotation);

    }
    else
    {
        newPosition = (Vector3)stream.ReceiveNext();
        newRotation = (Quaternion)stream.ReceiveNext();
    }
}

The main difference is transform.position and transform.rotation assignment. It’s applied for client devices only and calculated with Lerp (linear interpolation) methods.

There is estimatedSpeed const, which controls speed of movement during interpolation. If objects are moving with constant and known speed, we can interpolate more precisely. But in current example, we just empirically found this value launching game few times.

Vector3 newPosition;
Quaternion newRotation;

const float estimatedSpeed = 10;

void Start()
{
    newPosition = transform.position;
    newRotation = transform.rotation;

    rigidbody2d = GetComponent<Rigidbody2D>();

    rigidbody2d.simulated = false;
}

void Update()
{
    if (GameManager.Instance.IsPaused)
        return;

    rigidbody2d.simulated = PhotonNetwork.isMasterClient;

    if (!PhotonNetwork.isMasterClient)
    {
        transform.position = Vector3.Lerp(transform.position,
            newPosition,
            Time.deltaTime * estimatedSpeed);
        
        transform.rotation = Quaternion.Lerp(transform.rotation,
            newRotation,
            Time.deltaTime * estimatedSpeed);
    }
}

Side by side demonstration of final result. As you can see, client screen (left side) is running smoothly now.

Photon has built-in transform synchronization component. It’s called Photon Transform View and can be customized for particular usage scenario.

Replace out custom position synchronization with it and drag Photon Transform View to PhotonView Observed Components list and you are done with automatic position syncrhonization.

Conclusion

We built a simple prototype of multiplayer game in Unity using Photon Unity Networking. Our game founds random opponent, sync positions and rotations of objects and smooth movement with interpolation methods. You can build much more complicated game on top of it, adding additional game logic and network interactions.

Viktor Naryshkin iOS Developer