Создайте многопользовательскую игру в Unity, используя PUN 2.

Вы когда-нибудь задумывались, что нужно для создания многопользовательской игры внутри Unity?

В отличие от однопользовательских игр, многопользовательские игры требуют удаленного сервера, который играет роль моста, позволяя игровым клиентам общаться друг с другом.

В настоящее время хостингом серверов занимаются многочисленные службы. Одним из таких сервисов является Photon Network, который мы будем использовать в этом уроке.

PUN 2 — это последняя версия их API, которая была значительно улучшена по сравнению с устаревшей версией.

В этом посте мы будем загружать необходимые файлы, настраивать Photon AppID и программировать простой пример многопользовательской игры.

Unity версия, используемая в этом руководстве: Unity 2018.3.0f2 (64-разрядная версия)

Часть 1. Настройка PUN 2

Первым шагом является загрузка пакета PUN 2 с Asset Store. Он содержит все скрипты и файлы, необходимые для многопользовательской интеграции.

  • Откройте проект Unity, затем перейдите в Asset Store: (Окно -> Общие -> AssetStore) или нажмите Ctrl+9.
  • Найдите «PUN 2- Free», затем щелкните первый результат или нажмите здесь.
  • Импортируйте пакет PUN 2 после завершения загрузки.

  • После импорта пакета вам необходимо создать идентификатор приложения Photon. Это делается на их веб-сайте: https://www.photonengine.com/
  • Создайте новую учетную запись (или войдите в существующую учетную запись)
  • Перейдите на страницу «Приложения», щелкнув значок профиля, а затем "Your Applications" или перейдите по этой ссылке: https://dashboard.photonengine.com/en-US/PublicCloud
  • На странице «Приложения» нажмите "Create new app"

  • На странице создания в качестве типа фотона выберите "Photon Realtime", а в поле имени введите любое имя и нажмите кнопку "Create"

Как видите, приложение по умолчанию использует бесплатный план. Подробнее о тарифных планах можно прочитать здесь.

  • После создания приложения скопируйте идентификатор приложения, расположенный под именем приложения.

  • Вернитесь к своему проекту Unity, затем перейдите в Window -> Photon Unity Networking -> PUN Wizard.
  • В мастере PUN нажмите "Setup Project", вставьте свой идентификатор приложения, затем нажмите "Setup Project"

  • каламбур 2 готов!

Часть 2. Создание многопользовательской игры

Теперь давайте перейдем к той части, где мы фактически создаем многопользовательскую игру.

В PUN 2 многопользовательский режим реализован следующим образом:

  • Сначала мы подключаемся к Фотонному региону (например, Восток США, Европа, Азия и т. д.), который также известен как Лобби.
  • Попав в Лобби, мы запрашиваем все Комнаты, которые созданы в Регионе, а затем можем либо присоединиться к одной из Комнат, либо создать свою.
  • После присоединения к комнате мы запрашиваем список игроков, подключенных к комнате, и создаем экземпляры их экземпляров Player, которые затем синхронизируются с их локальными экземплярами через PhotonView.
  • Когда кто-то покидает комнату, его экземпляр уничтожается, и он удаляется из списка игроков.

1. Настройка лобби

Начнем с создания сцены лобби, которая будет содержать логику лобби (просмотр существующих комнат, создание новых комнат и т. д.):

  • Создайте новый скрипт C# и назовите его PUN2_GameLobby.
  • Создайте новую сцену и назовите ее "GameLobby"
  • В сцене GameLobby создайте новый GameObject. Назовите его "_GameLobby" и назначьте ему скрипт PUN2_GameLobby.

Теперь откройте скрипт PUN2_GameLobby:

Сначала мы импортируем пространства имен Photon, добавив в начало скрипта следующие строки:

using Photon.Pun;
using Photon.Realtime;

Также, прежде чем продолжить, нам нужно заменить MonoBehaviour по умолчанию на MonoBehaviourPunCallbacks. Этот шаг необходим, чтобы иметь возможность использовать обратные вызовы Photon:

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;

Затем мы вызываем ConnectUsingSettings() в void Start(). Это означает, что как только игра открывается, она подключается к серверу 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();
        }
    }

Чтобы узнать, было ли подключение к Photon успешным, нам нужно реализовать следующие обратные вызовы: 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;
    }

Далее следует часть пользовательского интерфейса, где выполняются просмотр комнаты и создание комнаты:

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

И, наконец, мы реализуем еще 4 обратных вызова: OnCreateRoomFailed(короткий returnCode, строковое сообщение), OnJoinRoomFailed(короткое returnCode, строковое сообщение), OnCreatedRoom() и OnJoinedRoom().

Эти обратные вызовы используются для определения того, присоединились ли мы к комнате/создали ее или возникли ли какие-либо проблемы во время соединения.

Вот окончательный вариант скрипта PUN2_GameLobby.cs:

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. Создание префаба Player

В многопользовательских играх экземпляр Player имеет две стороны: локальную и удаленную.

Локальный экземпляр контролируется локально (нами).

С другой стороны, удаленный экземпляр является локальным представлением того, что делает другой игрок. Наш ввод не должен влиять на него.

Чтобы определить, является ли экземпляр локальным или удаленным, мы используем компонент PhotonView.

PhotonView действует как мессенджер, который получает и отправляет значения, которые необходимо синхронизировать, например положение и вращение.

Итак, давайте начнем с создания экземпляра плеера (если у вас уже есть готовый экземпляр плеера, вы можете пропустить этот шаг).

В моем случае экземпляр Player будет простым кубом, который перемещается клавишами W и S и вращается клавишами A и D.

Вот простой скрипт контроллера:

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

Следующим шагом будет добавление компонента PhotonView.

  • Добавьте компонент PhotonView в экземпляр Player.
  • Создайте новый сценарий C# и назовите его PUN2_PlayerSync (этот сценарий будет использоваться для связи через PhotonView).

Откройте скрипт PUN2_PlayerSync:

В PUN2_PlayerSync первое, что нам нужно сделать, это добавить пространство имен Photon.Pun и заменить MonoBehaviour на MonoBehaviourPun, а также добавить интерфейс IPunObservable.

MonoBehaviourPun необходим для использования кэшированной переменной photonView вместо использования GetComponent<PhotonView>().

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;

Затем в void Start() мы проверяем, является ли игрок локальным или удаленным, используя 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);
            }
        }
    }

Фактическая синхронизация осуществляется через обратный вызов PhotonView: OnPhotonSerializeView(поток PhotonStream, информация PhotonMessageInfo):

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

В этом случае мы отправляем только позицию и вращение игрока, но вы можете использовать приведенный выше пример для отправки любого значения, которое необходимо синхронизировать по сети, с высокой частотой.

Полученные значения затем применяются в 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);
        }
    }
}

Вот окончательный вариант сценария PUN2_PlayerSync.cs:

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

Теперь назначим вновь созданный скрипт:

  • Прикрепите скрипт PUN2_PlayerSync к PlayerInstance.
  • Перетащите PUN2_PlayerSync в наблюдаемые компоненты PhotonView.
  • Назначьте SimplePlayerController для "Local Scripts" и назначьте GameObjects (которые вы хотите деактивировать для удаленных игроков) "Local Objects"

  • Сохраните PlayerInstance в Prefab и переместите его в папку Resources (если такой папки нет, создайте ее). Этот шаг необходим для создания многопользовательских объектов по сети.

3. Создание игрового уровня

GameLevel — это сцена, которая загружается после присоединения к комнате, и именно здесь происходят все действия.

  • Создайте новую сцену и назовите ее "GameLevel" (или, если вы хотите сохранить другое имя, обязательно измените имя в этой строке PhotonNetwork.LoadLevel("GameLevel"); в PUN2_GameLobby.cs).

В моем случае я буду использовать простую сцену с плоскостью:

  • Теперь создайте новый скрипт и назовите его PUN2_RoomController (этот скрипт будет обрабатывать логику внутри комнаты, например, создавать игроков, показывать список игроков и т. д.).

Откройте скрипт PUN2_RoomController:

Как и в случае с PUN2_GameLobby, мы начинаем с добавления пространств имен Photon и замены MonoBehaviour на MonoBehaviourPunCallbacks:

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;

Чтобы создать экземпляр префаба Player, мы используем 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);
    }

И простой пользовательский интерфейс с кнопкой "Leave Room" и некоторыми дополнительными элементами, такими как название комнаты и список подключенных игроков:

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

Наконец, мы реализуем еще один обратный вызов PhotonNetwork под названием OnLeftRoom(), который вызывается, когда мы покидаем комнату:

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

Вот окончательный вариант сценария PUN2_RoomController.cs:

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");
    }
}
  • Создайте новый GameObject в сцене 'GameLevel' и назовите его "_RoomController"
  • Прикрепите скрипт PUN2_RoomController к объекту _RoomController.
  • Назначьте ему префаб PlayerInstance и преобразование SpawnPoint, затем сохраните сцену.

  • Добавьте MainMenu и GameLevel в настройки сборки.

4. Делаем тестовую сборку

Теперь пришло время сделать сборку и протестировать ее:

Все работает так, как ожидалось!

Бонус

ПКП

В PUN 2 RPC означает удаленный вызов процедур, он используется для вызова функции на удаленных клиентах, находящихся в одной комнате (подробнее об этом можно прочитать здесь).

RPC имеют множество применений, например, скажем, вам нужно отправить сообщение в чат всем игрокам в комнате. С помощью RPC это легко сделать:

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

Обратите внимание на [PunRPC] перед функцией. Этот атрибут необходим, если вы планируете вызывать функцию через RPC.

Для вызова функций, отмеченных как RPC, вам понадобится PhotonView. Пример вызова:

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

Совет для профессионалов: если вы замените MonoBehaviour в своем скрипте на MonoBehaviourPun или MonoBehaviourPunCallbacks, вы можете пропустить PhotonView.Get() и использовать photonView.RPC() напрямую.

Пользовательские свойства

В PUN 2 пользовательские свойства — это хеш-таблица, которую можно назначить игроку или комнате.

Это полезно, когда вам нужно установить постоянные данные, которые не нужно часто менять (например, название команды игроков, режим игры в комнате и т. д.).

Во-первых, вам необходимо определить хеш-таблицу, для чего добавьте в начало скрипта следующую строку:

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

В приведенном ниже примере задаются свойства комнаты с именами "GameMode" и "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"]);

Свойства плеера задаются аналогично:

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

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

Чтобы удалить определенное свойство, просто установите его значение равным нулю.

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

Дополнительные уроки:

Синхронизация твердых тел по сети с помощью PUN 2

PUN 2 Добавление чата в комнате

Источник
📁PUN2Guide.unitypackage14.00 MB
Рекомендуемые статьи
Синхронизация твердых тел по сети с помощью PUN 2
Unity добавляет многопользовательский чат в комнаты PUN 2
Многопользовательское сжатие данных и битовая манипуляция
Создайте многопользовательскую автомобильную игру с помощью PUN 2
Photon Network (Classic) Руководство для начинающих
Создание многопользовательских сетевых игр в Unity
Учебное пособие по онлайн-таблице лидеров Unity