Faça um jogo multijogador no Unity usando PUN 2

Já se perguntou o que é necessário para criar um jogo multijogador dentro de Unity?

Ao contrário dos jogos single-player, os jogos multiplayer requerem um servidor remoto que desempenha o papel de ponte, permitindo que os clientes do jogo se comuniquem entre si.

Hoje em dia inúmeros serviços cuidam da hospedagem de servidores. Um desses serviços é Photon Network, que usaremos neste tutorial.

PUN 2 é a versão mais recente de sua API que foi bastante melhorada em comparação com a versão legada.

Nesta postagem, iremos fazer o download dos arquivos necessários, configurar o Photon AppID e programar um exemplo simples de multijogador.

Unity versão usada neste tutorial: Unity 2018.3.0f2 (64 bits)

Parte 1: Configurando o PUN 2

O primeiro passo é baixar um pacote PUN 2 do Asset Store. Ele contém todos os scripts e arquivos necessários para integração multijogador.

  • Abra seu projeto Unity e vá para Asset Store: (Janela -> Geral -> AssetStore) ou pressione Ctrl+9
  • Procure por "PUN 2- Free" e clique no primeiro resultado ou clique aqui
  • Importe o pacote PUN 2 após a conclusão do download

  • Na página de criação, para Tipo de fóton selecione "Photon Realtime" e para Nome, digite qualquer nome e clique "Create"

Como você pode ver, o padrão do aplicativo é o plano Gratuito. Você pode ler mais sobre os planos de preços aqui

  • Depois que o aplicativo for criado, copie o ID do aplicativo localizado sob o nome do aplicativo

  • Volte para o seu projeto Unity e vá para Janela -> Photon Unity Networking -> PUN Wizard
  • No PUN Wizard, clique em "Setup Project", cole seu ID do aplicativo e clique em "Setup Project"

  • O PUN 2 já está pronto!

Parte 2: Criando um jogo multijogador

Agora vamos para a parte em que criamos um jogo multijogador.

A forma como o modo multijogador é tratado no PUN 2 é:

  • Primeiro, nos conectamos à região Photon (ex. Leste dos EUA, Europa, Ásia, etc.), que também é conhecida como Lobby.
  • Uma vez no Lobby, solicitamos todas as Salas que são criadas na Região, e então podemos ingressar em uma das Salas ou criar a nossa própria Sala.
  • Após entrar na sala, solicitamos uma lista dos jogadores conectados à sala e instanciamos suas instâncias de Player, que são então sincronizadas com suas instâncias locais através do PhotonView.
  • Quando alguém sai da Sala, sua instância é destruída e ele é removido da Lista de Jogadores.

1. Configurando um lobby

Vamos começar criando uma cena do Lobby que conterá a lógica do Lobby (Navegar pelas salas existentes, criar novas salas, etc.):

  • Crie um novo script C# e chame-o de PUN2_GameLobby
  • Crie uma nova cena e chame-a "GameLobby"
  • Na cena GameLobby crie um novo GameObject. Chame-o de "_GameLobby" e atribua o script PUN2_GameLobby a ele

Agora abra o script PUN2_GameLobby:

Primeiro, importamos os namespaces Photon adicionando as linhas abaixo no início do script:

using Photon.Pun;
using Photon.Realtime;

Além disso, antes de continuar, precisamos substituir o MonoBehaviour padrão por MonoBehaviourPunCallbacks. Esta etapa é necessária para poder usar retornos de chamada do Photon:

public class PUN2_GameLobby : MonoBehaviourPunCallbacks

A seguir, criamos as variáveis ​​necessárias:

    //Our player name
    string playerName = "Player 1";
    //Users are separated from each other by gameversion (which allows you to make breaking changes).
    string gameVersion = "0.9";
    //The list of created rooms
    List<RoomInfo> createdRooms = new List<RoomInfo>();
    //Use this name when creating a Room
    string roomName = "Room 1";
    Vector2 roomListScroll = Vector2.zero;
    bool joiningRoom = false;

Em seguida, chamamos ConnectUsingSettings() no void Start(). Isso significa que assim que o jogo for aberto, ele se conectará ao Photon Server:

    // Use this for initialization
    void Start()
    {
        //This makes sure we can use PhotonNetwork.LoadLevel() on the master client and all clients in the same room sync their level automatically
        PhotonNetwork.AutomaticallySyncScene = true;

        if (!PhotonNetwork.IsConnected)
        {
            //Set the App version before connecting
            PhotonNetwork.PhotonServerSettings.AppSettings.AppVersion = gameVersion;
            // Connect to the photon master-server. We use the settings saved in PhotonServerSettings (a .asset file in this project)
            PhotonNetwork.ConnectUsingSettings();
        }
    }

Para saber se uma conexão com Photon foi bem-sucedida, precisamos implementar estes retornos de chamada: OnDisconnected(DisconnectCause cause), OnConnectedToMaster(), OnRoomListUpdate(List<RoomInfo> roomList)

    public override void OnDisconnected(DisconnectCause cause)
    {
        Debug.Log("OnFailedToConnectToPhoton. StatusCode: " + cause.ToString() + " ServerAddress: " + PhotonNetwork.ServerAddress);
    }

    public override void OnConnectedToMaster()
    {
        Debug.Log("OnConnectedToMaster");
        //After we connected to Master server, join the Lobby
        PhotonNetwork.JoinLobby(TypedLobby.Default);
    }

    public override void OnRoomListUpdate(List<RoomInfo> roomList)
    {
        Debug.Log("We have received the Room list");
        //After this callback, update the room list
        createdRooms = roomList;
    }

A seguir está a parte da UI, onde são feitas a navegação e a criação da sala:

    void OnGUI()
    {
        GUI.Window(0, new Rect(Screen.width / 2 - 450, Screen.height / 2 - 200, 900, 400), LobbyWindow, "Lobby");
    }

    void LobbyWindow(int index)
    {
        //Connection Status and Room creation Button
        GUILayout.BeginHorizontal();

        GUILayout.Label("Status: " + PhotonNetwork.NetworkClientState);

        if (joiningRoom || !PhotonNetwork.IsConnected || PhotonNetwork.NetworkClientState != ClientState.JoinedLobby)
        {
            GUI.enabled = false;
        }

        GUILayout.FlexibleSpace();

        //Room name text field
        roomName = GUILayout.TextField(roomName, GUILayout.Width(250));

        if (GUILayout.Button("Create Room", GUILayout.Width(125)))
        {
            if (roomName != "")
            {
                joiningRoom = true;

                RoomOptions roomOptions = new RoomOptions();
                roomOptions.IsOpen = true;
                roomOptions.IsVisible = true;
                roomOptions.MaxPlayers = (byte)10; //Set any number

                PhotonNetwork.JoinOrCreateRoom(roomName, roomOptions, TypedLobby.Default);
            }
        }

        GUILayout.EndHorizontal();

        //Scroll through available rooms
        roomListScroll = GUILayout.BeginScrollView(roomListScroll, true, true);

        if (createdRooms.Count == 0)
        {
            GUILayout.Label("No Rooms were created yet...");
        }
        else
        {
            for (int i = 0; i < createdRooms.Count; i++)
            {
                GUILayout.BeginHorizontal("box");
                GUILayout.Label(createdRooms[i].Name, GUILayout.Width(400));
                GUILayout.Label(createdRooms[i].PlayerCount + "/" + createdRooms[i].MaxPlayers);

                GUILayout.FlexibleSpace();

                if (GUILayout.Button("Join Room"))
                {
                    joiningRoom = true;

                    //Set our Player name
                    PhotonNetwork.NickName = playerName;

                    //Join the Room
                    PhotonNetwork.JoinRoom(createdRooms[i].Name);
                }
                GUILayout.EndHorizontal();
            }
        }

        GUILayout.EndScrollView();

        //Set player name and Refresh Room button
        GUILayout.BeginHorizontal();

        GUILayout.Label("Player Name: ", GUILayout.Width(85));
        //Player name text field
        playerName = GUILayout.TextField(playerName, GUILayout.Width(250));

        GUILayout.FlexibleSpace();

        GUI.enabled = (PhotonNetwork.NetworkClientState == ClientState.JoinedLobby || PhotonNetwork.NetworkClientState == ClientState.Disconnected) && !joiningRoom;
        if (GUILayout.Button("Refresh", GUILayout.Width(100)))
        {
            if (PhotonNetwork.IsConnected)
            {
                //Re-join Lobby to get the latest Room list
                PhotonNetwork.JoinLobby(TypedLobby.Default);
            }
            else
            {
                //We are not connected, estabilish a new connection
                PhotonNetwork.ConnectUsingSettings();
            }
        }

        GUILayout.EndHorizontal();

        if (joiningRoom)
        {
            GUI.enabled = true;
            GUI.Label(new Rect(900 / 2 - 50, 400 / 2 - 10, 100, 20), "Connecting...");
        }
    }

E por último, implementamos outros 4 retornos de chamada: OnCreateRoomFailed(short returnCode, string message), OnJoinRoomFailed(short returnCode, string message), OnCreatedRoom(), e OnJoinedRoom().

Esses retornos de chamada são usados ​​para determinar se entramos/criamos a sala ou se houve algum problema durante a conexão.

Aqui está o script PUN2_GameLobby.cs final:

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

public class PUN2_GameLobby : MonoBehaviourPunCallbacks
{

    //Our player name
    string playerName = "Player 1";
    //Users are separated from each other by gameversion (which allows you to make breaking changes).
    string gameVersion = "0.9";
    //The list of created rooms
    List<RoomInfo> createdRooms = new List<RoomInfo>();
    //Use this name when creating a Room
    string roomName = "Room 1";
    Vector2 roomListScroll = Vector2.zero;
    bool joiningRoom = false;

    // Use this for initialization
    void Start()
    {
        //This makes sure we can use PhotonNetwork.LoadLevel() on the master client and all clients in the same room sync their level automatically
        PhotonNetwork.AutomaticallySyncScene = true;

        if (!PhotonNetwork.IsConnected)
        {
            //Set the App version before connecting
            PhotonNetwork.PhotonServerSettings.AppSettings.AppVersion = gameVersion;
            // Connect to the photon master-server. We use the settings saved in PhotonServerSettings (a .asset file in this project)
            PhotonNetwork.ConnectUsingSettings();
        }
    }

    public override void OnDisconnected(DisconnectCause cause)
    {
        Debug.Log("OnFailedToConnectToPhoton. StatusCode: " + cause.ToString() + " ServerAddress: " + PhotonNetwork.ServerAddress);
    }

    public override void OnConnectedToMaster()
    {
        Debug.Log("OnConnectedToMaster");
        //After we connected to Master server, join the Lobby
        PhotonNetwork.JoinLobby(TypedLobby.Default);
    }

    public override void OnRoomListUpdate(List<RoomInfo> roomList)
    {
        Debug.Log("We have received the Room list");
        //After this callback, update the room list
        createdRooms = roomList;
    }

    void OnGUI()
    {
        GUI.Window(0, new Rect(Screen.width / 2 - 450, Screen.height / 2 - 200, 900, 400), LobbyWindow, "Lobby");
    }

    void LobbyWindow(int index)
    {
        //Connection Status and Room creation Button
        GUILayout.BeginHorizontal();

        GUILayout.Label("Status: " + PhotonNetwork.NetworkClientState);

        if (joiningRoom || !PhotonNetwork.IsConnected || PhotonNetwork.NetworkClientState != ClientState.JoinedLobby)
        {
            GUI.enabled = false;
        }

        GUILayout.FlexibleSpace();

        //Room name text field
        roomName = GUILayout.TextField(roomName, GUILayout.Width(250));

        if (GUILayout.Button("Create Room", GUILayout.Width(125)))
        {
            if (roomName != "")
            {
                joiningRoom = true;

                RoomOptions roomOptions = new RoomOptions();
                roomOptions.IsOpen = true;
                roomOptions.IsVisible = true;
                roomOptions.MaxPlayers = (byte)10; //Set any number

                PhotonNetwork.JoinOrCreateRoom(roomName, roomOptions, TypedLobby.Default);
            }
        }

        GUILayout.EndHorizontal();

        //Scroll through available rooms
        roomListScroll = GUILayout.BeginScrollView(roomListScroll, true, true);

        if (createdRooms.Count == 0)
        {
            GUILayout.Label("No Rooms were created yet...");
        }
        else
        {
            for (int i = 0; i < createdRooms.Count; i++)
            {
                GUILayout.BeginHorizontal("box");
                GUILayout.Label(createdRooms[i].Name, GUILayout.Width(400));
                GUILayout.Label(createdRooms[i].PlayerCount + "/" + createdRooms[i].MaxPlayers);

                GUILayout.FlexibleSpace();

                if (GUILayout.Button("Join Room"))
                {
                    joiningRoom = true;

                    //Set our Player name
                    PhotonNetwork.NickName = playerName;

                    //Join the Room
                    PhotonNetwork.JoinRoom(createdRooms[i].Name);
                }
                GUILayout.EndHorizontal();
            }
        }

        GUILayout.EndScrollView();

        //Set player name and Refresh Room button
        GUILayout.BeginHorizontal();

        GUILayout.Label("Player Name: ", GUILayout.Width(85));
        //Player name text field
        playerName = GUILayout.TextField(playerName, GUILayout.Width(250));

        GUILayout.FlexibleSpace();

        GUI.enabled = (PhotonNetwork.NetworkClientState == ClientState.JoinedLobby || PhotonNetwork.NetworkClientState == ClientState.Disconnected) && !joiningRoom;
        if (GUILayout.Button("Refresh", GUILayout.Width(100)))
        {
            if (PhotonNetwork.IsConnected)
            {
                //Re-join Lobby to get the latest Room list
                PhotonNetwork.JoinLobby(TypedLobby.Default);
            }
            else
            {
                //We are not connected, estabilish a new connection
                PhotonNetwork.ConnectUsingSettings();
            }
        }

        GUILayout.EndHorizontal();

        if (joiningRoom)
        {
            GUI.enabled = true;
            GUI.Label(new Rect(900 / 2 - 50, 400 / 2 - 10, 100, 20), "Connecting...");
        }
    }

    public override void OnCreateRoomFailed(short returnCode, string message)
    {
        Debug.Log("OnCreateRoomFailed got called. This can happen if the room exists (even if not visible). Try another room name.");
        joiningRoom = false;
    }

    public override void OnJoinRoomFailed(short returnCode, string message)
    {
        Debug.Log("OnJoinRoomFailed got called. This can happen if the room is not existing or full or closed.");
        joiningRoom = false;
    }

    public override void OnJoinRandomFailed(short returnCode, string message)
    {
        Debug.Log("OnJoinRandomFailed got called. This can happen if the room is not existing or full or closed.");
        joiningRoom = false;
    }

    public override void OnCreatedRoom()
    {
        Debug.Log("OnCreatedRoom");
        //Set our player name
        PhotonNetwork.NickName = playerName;
        //Load the Scene called GameLevel (Make sure it's added to build settings)
        PhotonNetwork.LoadLevel("GameLevel");
    }

    public override void OnJoinedRoom()
    {
        Debug.Log("OnJoinedRoom");
    }
}

2. Criando um Player pré-fabricado

Em jogos Multiplayer, a instância Player possui 2 lados: Local e Remoto

Uma instância local é controlada localmente (por nós).

Uma instância remota, por outro lado, é uma representação local do que o outro jogador está fazendo. Não deve ser afetado por nossa contribuição.

Para determinar se a instância é Local ou Remota, usamos um componente PhotonView.

PhotonView atua como um mensageiro que recebe e envia os valores que precisam ser sincronizados, por exemplo, posição e rotação.

Então, vamos começar criando a instância do player (se você já tiver a instância do player pronta, pode pular esta etapa).

No meu caso, a instância do Player será um cubo simples que é movido com as teclas W e S e girado com as teclas A e D.

Aqui está um script de controlador simples:

SimplePlayerController.cs

using UnityEngine;

public class SimplePlayerController : MonoBehaviour
{

    // Update is called once per frame
    void Update()
    {
        //Move Front/Back
        if (Input.GetKey(KeyCode.W))
        {
            transform.Translate(transform.forward * Time.deltaTime * 2.45f, Space.World);
        }
        else if (Input.GetKey(KeyCode.S))
        {
            transform.Translate(-transform.forward * Time.deltaTime * 2.45f, Space.World);
        }

        //Rotate Left/Right
        if (Input.GetKey(KeyCode.A))
        {
            transform.Rotate(new Vector3(0, -14, 0) * Time.deltaTime * 4.5f, Space.Self);
        }
        else if (Input.GetKey(KeyCode.D))
        {
            transform.Rotate(new Vector3(0, 14, 0) * Time.deltaTime * 4.5f, Space.Self);
        }
    }
}

A próxima etapa é adicionar um componente PhotonView.

  • Adicione um componente PhotonView à instância do Player.
  • Crie um novo script C# e chame-o de PUN2_PlayerSync (este script será usado para comunicação através do PhotonView).

Abra o script PUN2_PlayerSync:

Em PUN2_PlayerSync, a primeira coisa que precisamos fazer é adicionar um namespace Photon.Pun e substituir MonoBehaviour por MonoBehaviourPun e também adicionar a interface IPunObservable.

MonoBehaviourPun é necessário para poder usar a variável photonView armazenada em cache, em vez de usar GetComponent<PhotonView>().

using UnityEngine;
using Photon.Pun;

public class PUN2_PlayerSync : MonoBehaviourPun, IPunObservable

Depois disso, podemos passar a criar todas as variáveis ​​necessárias:

    //List of the scripts that should only be active for the local player (ex. PlayerController, MouseLook etc.)
    public MonoBehaviour[] localScripts;
    //List of the GameObjects that should only be active for the local player (ex. Camera, AudioListener etc.)
    public GameObject[] localObjects;
    //Values that will be synced over network
    Vector3 latestPos;
    Quaternion latestRot;

Então no void Start(), verificamos se o player é Local ou Remoto usando photonView.IsMine:

    // Use this for initialization
    void Start()
    {
        if (photonView.IsMine)
        {
            //Player is local
        }
        else
        {
            //Player is Remote, deactivate the scripts and object that should only be enabled for the local player
            for (int i = 0; i < localScripts.Length; i++)
            {
                localScripts[i].enabled = false;
            }
            for (int i = 0; i < localObjects.Length; i++)
            {
                localObjects[i].SetActive(false);
            }
        }
    }

A sincronização real é feita através do retorno de chamada do PhotonView: OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info):

    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.IsWriting)
        {
            //We own this player: send the others our data
            stream.SendNext(transform.position);
            stream.SendNext(transform.rotation);
        }
        else
        {
            //Network player, receive data
            latestPos = (Vector3)stream.ReceiveNext();
            latestRot = (Quaternion)stream.ReceiveNext();
        }
    }

Neste caso, enviamos apenas a Posição e Rotação do player, mas você pode usar o exemplo acima para enviar qualquer valor que seja necessário para ser sincronizado pela rede, em alta frequência.

Os valores recebidos são então aplicados no void Update():

    // Update is called once per frame
    void Update()
    {
        if (!photonView.IsMine)
        {
            //Update remote player (smooth this, this looks good, at the cost of some accuracy)
            transform.position = Vector3.Lerp(transform.position, latestPos, Time.deltaTime * 5);
            transform.rotation = Quaternion.Lerp(transform.rotation, latestRot, Time.deltaTime * 5);
        }
    }
}

Aqui está o script PUN2_PlayerSync.cs final:

using UnityEngine;
using Photon.Pun;

public class PUN2_PlayerSync : MonoBehaviourPun, IPunObservable
{

    //List of the scripts that should only be active for the local player (ex. PlayerController, MouseLook etc.)
    public MonoBehaviour[] localScripts;
    //List of the GameObjects that should only be active for the local player (ex. Camera, AudioListener etc.)
    public GameObject[] localObjects;
    //Values that will be synced over network
    Vector3 latestPos;
    Quaternion latestRot;

    // Use this for initialization
    void Start()
    {
        if (photonView.IsMine)
        {
            //Player is local
        }
        else
        {
            //Player is Remote, deactivate the scripts and object that should only be enabled for the local player
            for (int i = 0; i < localScripts.Length; i++)
            {
                localScripts[i].enabled = false;
            }
            for (int i = 0; i < localObjects.Length; i++)
            {
                localObjects[i].SetActive(false);
            }
        }
    }

    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.IsWriting)
        {
            //We own this player: send the others our data
            stream.SendNext(transform.position);
            stream.SendNext(transform.rotation);
        }
        else
        {
            //Network player, receive data
            latestPos = (Vector3)stream.ReceiveNext();
            latestRot = (Quaternion)stream.ReceiveNext();
        }
    }

    // Update is called once per frame
    void Update()
    {
        if (!photonView.IsMine)
        {
            //Update remote player (smooth this, this looks good, at the cost of some accuracy)
            transform.position = Vector3.Lerp(transform.position, latestPos, Time.deltaTime * 5);
            transform.rotation = Quaternion.Lerp(transform.rotation, latestRot, Time.deltaTime * 5);
        }
    }
}

Agora vamos atribuir um script recém-criado:

  • Anexe o script PUN2_PlayerSync ao PlayerInstance.
  • Arraste e solte PUN2_PlayerSync nos componentes observados do PhotonView.
  • Atribua o SimplePlayerController a "Local Scripts" e atribua os GameObjects (que você deseja que sejam desativados para jogadores remotos) ao "Local Objects"

  • Salve o PlayerInstance no Prefab e mova-o para a pasta chamada Resources (se essa pasta não existir, crie uma). Esta etapa é necessária para poder gerar objetos multijogador na rede.

3. Criando um nível de jogo

GameLevel é uma Cena que é carregada após entrar na Sala e é onde toda a ação acontece.

  • Crie uma nova cena e chame-a de "GameLevel" (ou se quiser manter um nome diferente, certifique-se de alterar o nome nesta linha PhotonNetwork.LoadLevel("GameLevel"); no PUN2_GameLobby.cs).

No meu caso, usarei uma Cena simples com Plano:

  • Agora crie um novo script e chame-o de PUN2_RoomController (este script irá lidar com a lógica dentro da Room, como gerar os jogadores, mostrar a lista de jogadores, etc.).

Abra o script PUN2_RoomController:

Assim como PUN2_GameLobby, começamos adicionando namespaces Photon e substituindo MonoBehaviour por MonoBehaviourPunCallbacks:

using UnityEngine;
using Photon.Pun;

public class PUN2_RoomController : MonoBehaviourPunCallbacks

Agora vamos adicionar as variáveis ​​necessárias:

    //Player instance prefab, must be located in the Resources folder
    public GameObject playerPrefab;
    //Player spawn point
    public Transform spawnPoint;

Para instanciar o Player pré-fabricado, estamos usando PhotonNetwork.Instantiate:

    // Use this for initialization
    void Start()
    {
        //In case we started this demo with the wrong scene being active, simply load the menu scene
        if (PhotonNetwork.CurrentRoom == null)
        {
            Debug.Log("Is not in the room, returning back to Lobby");
            UnityEngine.SceneManagement.SceneManager.LoadScene("GameLobby");
            return;
        }

        //We're in a room. spawn a character for the local player. it gets synced by using PhotonNetwork.Instantiate
        PhotonNetwork.Instantiate(playerPrefab.name, spawnPoint.position, Quaternion.identity, 0);
    }

E uma UI simples com um botão "Leave Room" e alguns elementos adicionais, como o nome da sala e a lista de jogadores conectados:

    void OnGUI()
    {
        if (PhotonNetwork.CurrentRoom == null)
            return;

        //Leave this Room
        if (GUI.Button(new Rect(5, 5, 125, 25), "Leave Room"))
        {
            PhotonNetwork.LeaveRoom();
        }

        //Show the Room name
        GUI.Label(new Rect(135, 5, 200, 25), PhotonNetwork.CurrentRoom.Name);

        //Show the list of the players connected to this Room
        for (int i = 0; i < PhotonNetwork.PlayerList.Length; i++)
        {
            //Show if this player is a Master Client. There can only be one Master Client per Room so use this to define the authoritative logic etc.)
            string isMasterClient = (PhotonNetwork.PlayerList[i].IsMasterClient ? ": MasterClient" : "");
            GUI.Label(new Rect(5, 35 + 30 * i, 200, 25), PhotonNetwork.PlayerList[i].NickName + isMasterClient);
        }
    }

Finalmente, implementamos outro callback PhotonNetwork chamado OnLeftRoom() que é chamado quando saímos da Room:

    public override void OnLeftRoom()
    {
        //We have left the Room, return back to the GameLobby
        UnityEngine.SceneManagement.SceneManager.LoadScene("GameLobby");
    }

Aqui está o script PUN2_RoomController.cs final:

using UnityEngine;
using Photon.Pun;

public class PUN2_RoomController : MonoBehaviourPunCallbacks
{

    //Player instance prefab, must be located in the Resources folder
    public GameObject playerPrefab;
    //Player spawn point
    public Transform spawnPoint;

    // Use this for initialization
    void Start()
    {
        //In case we started this demo with the wrong scene being active, simply load the menu scene
        if (PhotonNetwork.CurrentRoom == null)
        {
            Debug.Log("Is not in the room, returning back to Lobby");
            UnityEngine.SceneManagement.SceneManager.LoadScene("GameLobby");
            return;
        }

        //We're in a room. spawn a character for the local player. it gets synced by using PhotonNetwork.Instantiate
        PhotonNetwork.Instantiate(playerPrefab.name, spawnPoint.position, Quaternion.identity, 0);
    }

    void OnGUI()
    {
        if (PhotonNetwork.CurrentRoom == null)
            return;

        //Leave this Room
        if (GUI.Button(new Rect(5, 5, 125, 25), "Leave Room"))
        {
            PhotonNetwork.LeaveRoom();
        }

        //Show the Room name
        GUI.Label(new Rect(135, 5, 200, 25), PhotonNetwork.CurrentRoom.Name);

        //Show the list of the players connected to this Room
        for (int i = 0; i < PhotonNetwork.PlayerList.Length; i++)
        {
            //Show if this player is a Master Client. There can only be one Master Client per Room so use this to define the authoritative logic etc.)
            string isMasterClient = (PhotonNetwork.PlayerList[i].IsMasterClient ? ": MasterClient" : "");
            GUI.Label(new Rect(5, 35 + 30 * i, 200, 25), PhotonNetwork.PlayerList[i].NickName + isMasterClient);
        }
    }

    public override void OnLeftRoom()
    {
        //We have left the Room, return back to the GameLobby
        UnityEngine.SceneManagement.SceneManager.LoadScene("GameLobby");
    }
}
  • Crie um novo GameObject na cena 'GameLevel' e chame-o "_RoomController"
  • Anexe o script PUN2_RoomController ao objeto _RoomController
  • Atribua o pré-fabricado PlayerInstance e uma SpawnPoint Transform a ele e salve a cena

  • Adicione MainMenu e GameLevel às configurações de compilação.

4. Fazendo uma compilação de teste

Agora é hora de fazer uma compilação e testá-la:

Tudo funciona como esperado!

Bônus

RPC

No PUN 2, RPC significa Remote Procedure Call, é usado para chamar uma função em clientes remotos que estão na mesma sala (você pode ler mais sobre isso aqui).

Os RPCs têm muitos usos, por exemplo, digamos que você precise enviar uma mensagem de bate-papo para todos os jogadores na sala. Com RPCs, é fácil fazer isso:

[PunRPC]
void ChatMessage(string senderName, string messageText)
{
    Debug.Log(string.Format("{0}: {1}", senderName, messageText));
}

Observe o [PunRPC] antes da função. Este atributo é necessário se você planeja chamar a função por meio de RPCs.

Para chamar as funções marcadas como RPC, você precisa de um PhotonView. Chamada de exemplo:

PhotonView photonView = PhotonView.Get(this);
photonView.RPC("ChatMessage", RpcTarget.All, PhotonNetwork.playerName, "Some message");

Dica profissional: se você substituir MonoBehaviour em seu script por MonoBehaviourPun ou MonoBehaviourPunCallbacks você pode pular PhotonView.Get() e usar photonView.RPC() diretamente.

Propriedades personalizadas

No PUN 2, Propriedades Personalizadas é uma Hashtable que pode ser atribuída a um Jogador ou à Sala.

Isso é útil quando você precisa definir dados persistentes que não precisam ser alterados com frequência (por exemplo, nome da equipe do jogador, modo de jogo na sala, etc.).

Primeiro você deve definir uma Hashtable, o que é feito adicionando a linha abaixo no início do script:

//Replace default Hashtables with Photon hashtables
using Hashtable = ExitGames.Client.Photon.Hashtable;

O exemplo abaixo define as propriedades da sala chamadas "GameMode" e "AnotherProperty":

        //Set Room properties (Only Master Client is allowed to set Room properties)
        if (PhotonNetwork.IsMasterClient)
        {
            Hashtable setRoomProperties = new Hashtable();
            setRoomProperties.Add("GameMode", "FFA");
            setRoomProperties.Add("AnotherProperty", "Test");
            PhotonNetwork.CurrentRoom.SetCustomProperties(setRoomProperties);
        }

        //Will print "FFA"
        print((string)PhotonNetwork.CurrentRoom.CustomProperties["GameMode"]);
        //Will print "Test"
        print((string)PhotonNetwork.CurrentRoom.CustomProperties["AnotherProperty"]);

As propriedades do player são definidas de forma semelhante:

            Hashtable setPlayerProperties = new Hashtable();
            setPlayerProperties.Add("PlayerHP", (float)100);
            PhotonNetwork.LocalPlayer.SetCustomProperties(setPlayerProperties);

            print((float)PhotonNetwork.LocalPlayer.CustomProperties["PlayerHP"]);

Para remover uma propriedade específica basta definir seu valor como nulo.

            Hashtable setPlayerProperties = new Hashtable();
            setPlayerProperties.Add("PlayerHP", null);
            PhotonNetwork.LocalPlayer.SetCustomProperties(setPlayerProperties);

Tutoriais adicionais:

Sincronize Rigidbodies pela rede usando PUN 2

PUN 2 Adicionando bate-papo na sala

Fonte
📁PUN2Guide.unitypackage14.00 MB
Artigos sugeridos
Sincronizar corpos rígidos pela rede usando PUN 2
Unity Adicionando Bate-papo Multijogador às Salas PUN 2
Compressão de dados multijogador e manipulação de bits
Faça um jogo de carros multijogador com PUN 2
Photon Network (Clássico) Guia do Iniciante
Construindo jogos multijogador em rede no Unity
Introdução ao Photon Fusion 2 no Unity