Crea un juego multijugador en Unity usando PUN 2

¿Alguna vez te has preguntado qué se necesita para crear un juego multijugador dentro de Unity?

A diferencia de los juegos para un solo jugador, los juegos multijugador requieren un servidor remoto que desempeña el papel de puente, permitiendo que los clientes del juego se comuniquen entre sí.

Hoy en día numerosos servicios se encargan del alojamiento de servidores. Uno de esos servicios es Photon Network, que usaremos para este tutorial.

PUN 2 es la última versión de su API que se ha mejorado mucho en comparación con la versión anterior.

En esta publicación, descargaremos los archivos necesarios, configuraremos Photon AppID y programaremos un ejemplo multijugador simple.

Unity versión utilizada en este tutorial: Unity 2018.3.0f2 (64 bits)

Parte 1: Configuración de PUN 2

El primer paso es descargar un paquete PUN 2 desde el Asset Store. Contiene todos los scripts y archivos necesarios para la integración multijugador.

  • Abra su proyecto Unity y luego vaya a Asset Store: (Ventana -> General -> AssetStore) o presione Ctrl+9
  • Busque "PUN 2- Gratis" y luego haga clic en el primer resultado o haga clic aquí
  • Importe el paquete PUN 2 una vez finalizada la descarga

  • Después de importar el paquete, debe crear una ID de aplicación Photon, esto se hace en su sitio web: https://www.photonengine.com/
  • Cree una nueva cuenta (o inicie sesión en su cuenta existente)
  • Vaya a la página de Aplicaciones haciendo clic en el ícono de perfil y luego "Your Applications" o siga este enlace: https://dashboard.photonengine.com/en-US/PublicCloud
  • En la página de Aplicaciones, haga clic en "Create new app"

  • En la página de creación, para Tipo de fotón seleccione "Photon Realtime" y para Nombre, escriba cualquier nombre y luego haga clic "Create"

Como puede ver, la aplicación tiene por defecto el plan gratuito. Puedes leer más sobre los planes de precios aquí

  • Una vez creada la aplicación, copie el ID de la aplicación ubicado debajo del nombre de la aplicación.

  • Vuelva a su proyecto Unity y luego vaya a Ventana -> Redes Photon Unity -> Asistente PUN
  • En PUN Wizard, haga clic en "Setup Project", pegue su ID de aplicación y luego haga clic en "Setup Project"

  • ¡El PUN 2 ya está listo!

Parte 2: Crear un juego multijugador

Ahora pasemos a la parte en la que realmente creamos un juego multijugador.

La forma en que se maneja el modo multijugador en PUN 2 es:

  • Primero, nos conectamos a la Región Photon (por ejemplo, EE. UU. Este, Europa, Asia, etc.), que también se conoce como Lobby.
  • Una vez en el Lobby, solicitamos todas las Salas que se crean en la Región, y luego podemos unirnos a una de las Salas o crear nuestra propia Sala.
  • Después de unirnos a la sala, solicitamos una lista de los jugadores conectados a la sala y creamos una instancia de sus instancias de jugador, que luego se sincronizan con sus instancias locales a través de PhotonView.
  • Cuando alguien abandona la sala, su instancia se destruye y se elimina de la lista de jugadores.

1. Configurar un vestíbulo

Comencemos creando una escena del lobby que contendrá la lógica del lobby (explorar salas existentes, crear salas nuevas, etc.):

  • Crea un nuevo script C# y llámalo PUN2_GameLobby
  • Crea una nueva escena y llámala "GameLobby"
  • En la escena GameLobby crea un nuevo GameObject. Llámalo "_GameLobby" y asígnale el script PUN2_GameLobby

Ahora abre el script PUN2_GameLobby:

Primero, importamos los espacios de nombres de Photon agregando las siguientes líneas al comienzo del script:

using Photon.Pun;
using Photon.Realtime;

Además, antes de continuar, debemos reemplazar el MonoBehaviour predeterminado con MonoBehaviourPunCallbacks. Este paso es necesario para poder utilizar las devoluciones de llamada de Photon:

public class PUN2_GameLobby : MonoBehaviourPunCallbacks

A continuación, creamos las variables necesarias:

    //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;

Luego llamamos ConnectUsingSettings() en el vacío Start(). Esto significa que tan pronto como se abre el juego, se conecta al servidor Photon:

    // 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 si una conexión a Photon fue exitosa, necesitamos implementar estas devoluciones de llamada: 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;
    }

La siguiente es la parte de la interfaz de usuario, donde se realizan la navegación y la creación de la 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...");
        }
    }

Y por último, implementamos otras 4 devoluciones de llamada: OnCreateRoomFailed(short returnCode, string message), OnJoinRoomFailed(short returnCode, string message), OnCreatedRoom(), y OnJoinedRoom().

Estas devoluciones de llamada se utilizan para determinar si nos unimos/creamos la sala o si hubo algún problema durante la conexión.

Aquí está el 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. Creando una casa prefabricada de reproductor

En los juegos multijugador, la instancia del jugador tiene 2 lados: local y remoto

Una instancia local es controlada localmente (por nosotros).

Una instancia remota, por otro lado, es una representación local de lo que está haciendo el otro jugador. Nuestras aportaciones no deberían afectarlo.

Para determinar si la instancia es local o remota utilizamos un componente PhotonView.

PhotonView actúa como un mensajero que recibe y envía los valores que deben sincronizarse, por ejemplo, posición y rotación.

Entonces, comencemos creando la instancia del reproductor (si ya tiene la instancia del reproductor lista, puede omitir este paso).

En mi caso, la instancia del Jugador será un Cubo simple que se mueve con las teclas W y S y se gira con las teclas A y D.

Aquí hay un script de controlador simple:

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);
        }
    }
}

El siguiente paso es agregar un componente PhotonView.

  • Agregue un componente PhotonView a la instancia del reproductor.
  • Cree un nuevo script C# y llámelo PUN2_PlayerSync (este script se utilizará para comunicarse a través de PhotonView).

Abra el script PUN2_PlayerSync:

En PUN2_PlayerSync lo primero que debemos hacer es agregar un espacio de nombres Photon.Pun y reemplazar MonoBehaviour con MonoBehaviourPun y también agregar la interfaz IPunObservable.

MonoBehaviourPun es necesario para poder utilizar la variable photonView almacenada en caché, en lugar de utilizar GetComponent<PhotonView>().

using UnityEngine;
using Photon.Pun;

public class PUN2_PlayerSync : MonoBehaviourPun, IPunObservable

Después de eso, podemos pasar a crear todas las variables necesarias:

    //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;

Luego, en el vacío Start(), verificamos si el reproductor es Local o 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);
            }
        }
    }

La sincronización real se realiza a través de la devolución de llamada de 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();
        }
    }

En este caso, solo enviamos la Posición y Rotación del reproductor, pero puedes usar el ejemplo anterior para enviar cualquier valor que sea necesario sincronizar a través de la red, a alta frecuencia.

Los valores recibidos luego se aplican en la actualización vacía():

    // 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);
        }
    }
}

Aquí está el 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);
        }
    }
}

Ahora asignemos un script recién creado:

  • Adjunte el script PUN2_PlayerSync a PlayerInstance.
  • Arrastre y suelte PUN2_PlayerSync en los componentes observados de PhotonView.
  • Asigne SimplePlayerController a "Local Scripts" y asigne los GameObjects (que desea desactivar para jugadores remotos) al "Local Objects"

  • Guarde PlayerInstance en Prefab y muévalo a la carpeta llamada Recursos (si no existe dicha carpeta, cree una). Este paso es necesario para poder generar Objetos multijugador a través de la Red.

3. Creando un nivel de juego

GameLevel es una escena que se carga después de unirse a la sala y es donde ocurre toda la acción.

  • Cree una nueva escena y llámela "GameLevel" (O si desea mantener un nombre diferente, asegúrese de cambiar el nombre en esta línea PhotonNetwork.LoadLevel("GameLevel"); en PUN2_GameLobby.cs).

En mi caso, usaré una escena simple con un avión:

  • Ahora cree un nuevo script y llámelo PUN2_RoomController (este script manejará la lógica dentro de la Sala, como generar jugadores, mostrar la lista de jugadores, etc.).

Abra el script PUN2_RoomController:

Al igual que PUN2_GameLobby, comenzamos agregando espacios de nombres Photon y reemplazando MonoBehaviour con MonoBehaviourPunCallbacks:

using UnityEngine;
using Photon.Pun;

public class PUN2_RoomController : MonoBehaviourPunCallbacks

Ahora agreguemos las variables necesarias:

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

Para crear una instancia del prefabricado Player 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);
    }

Y una interfaz de usuario sencilla con un botón "Leave Room" y algunos elementos adicionales como el nombre de la sala y la lista de jugadores 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 otra devolución de llamada de PhotonNetwork llamada OnLeftRoom() que se llama cuando salimos de la Sala:

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

Aquí está el 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");
    }
}
  • Crea un nuevo GameObject en la escena 'GameLevel' y llámalo "_RoomController"
  • Adjunte el script PUN2_RoomController al objeto _RoomController
  • Asigne la casa prefabricada PlayerInstance y una transformación SpawnPoint y luego guarde la escena.

  • Agregue MainMenu y GameLevel a la configuración de compilación.

4. Hacer una compilación de prueba

Ahora es el momento de crear una compilación y probarla:

¡Todo funciona como se esperaba!

Prima

RPC

En PUN 2, RPC significa Llamada a procedimiento remoto, se usa para llamar a una función en clientes remotos que están en la misma habitación (puede leer más sobre esto aquí).

Los RPC tienen muchos usos, por ejemplo, digamos que necesitas enviar un mensaje de chat a todos los jugadores en la sala. Con los RPC, es fácil hacerlo:

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

Observe el [PunRPC] antes de la función. Este atributo es necesario si planea llamar a la función a través de RPC.

Para llamar a las funciones marcadas como RPC, necesita un PhotonView. Llamada de ejemplo:

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

Consejo profesional: si reemplaza MonoBehaviour en su script con MonoBehaviourPun o MonoBehaviourPunCallbacks puede omitir PhotonView.Get() y usar photonView.RPC() directamente.

Propiedades personalizadas

En PUN 2, las propiedades personalizadas son una tabla hash que se puede asignar a un jugador o a la sala.

Esto es útil cuando necesitas configurar datos persistentes que no necesitan cambiarse con frecuencia (por ejemplo, nombre del equipo del jugador, modo de juego en sala, etc.).

Primero, debe definir una Hashtable, lo cual se hace agregando la siguiente línea al comienzo del script:

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

El siguiente ejemplo establece las propiedades de la habitación denominadas "GameMode" y "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"]);

Las propiedades del reproductor se configuran de manera similar:

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

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

Para eliminar una propiedad específica simplemente establezca su valor en nulo.

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

Tutoriales adicionales:

Sincronizar cuerpos rígidos a través de la red usando PUN 2

PUN 2 Agregar chat de sala