Stwórz grę wieloosobową w Unity, używając PUN 2

Czy zastanawiałeś się kiedyś, czego potrzeba, aby stworzyć grę wieloosobową w Unity?

W przeciwieństwie do gier dla jednego gracza, gry dla wielu graczy wymagają zdalnego serwera, który pełni rolę mostu, umożliwiając klientom gry komunikację między sobą.

Obecnie hostingiem serwerów zajmuje się wiele usług. Jedną z takich usług jest Photon Network, z której będziemy korzystać w tym samouczku.

PUN 2 to najnowsza wersja ich API, która została znacznie ulepszona w porównaniu do starszej wersji.

W tym poście omówimy pobieranie niezbędnych plików, konfigurowanie Photon AppID i programowanie prostego przykładu gry wieloosobowej.

Unity wersja używana w tym samouczku: Unity 2018.3.0f2 (64-bitowa)

Część 1: Konfigurowanie PUN 2

Pierwszym krokiem jest pobranie pakietu PUN 2 z Asset Store. Zawiera wszystkie skrypty i pliki potrzebne do integracji gry wieloosobowej.

  • Otwórz swój projekt Unity, a następnie przejdź do Asset Store: (Okno -> Ogólne -> AssetStore) lub naciśnij Ctrl+9
  • Wyszukaj „PUN 2- Free”, a następnie kliknij pierwszy wynik lub kliknij tutaj
  • Zaimportuj pakiet PUN 2 po zakończeniu pobierania

  • Po zaimportowaniu pakietu musisz utworzyć identyfikator aplikacji Photon, można to zrobić na ich stronie internetowej: https://www.photonengine.com/
  • Utwórz nowe konto (lub zaloguj się na istniejące konto)
  • Przejdź do strony aplikacji, klikając ikonę profilu, a następnie "Your Applications" lub kliknij ten link: https://dashboard.photonengine.com/en-US/PublicCloud
  • Na stronie Aplikacje kliknij "Create new app"

  • Na stronie tworzenia w polu Typ fotonu wybierz "Photon Realtime", a w polu Nazwa wpisz dowolną nazwę i kliknij "Create"

Jak widać, aplikacja domyślnie korzysta z planu Free. Więcej informacji na temat planów cenowych znajdziesz tutaj

  • Po utworzeniu aplikacji skopiuj identyfikator aplikacji znajdujący się pod nazwą aplikacji

  • Wróć do swojego projektu Unity, a następnie przejdź do Okno -> Photon Unity Sieć -> Kreator PUN
  • W kreatorze PUN kliknij "Setup Project", wklej identyfikator aplikacji, a następnie kliknij "Setup Project"

  • PUN 2 jest już gotowy!

Część 2: Tworzenie gry wieloosobowej

Przejdźmy teraz do części, w której właściwie tworzymy grę wieloosobową.

Sposób obsługi trybu wieloosobowego w PUN 2 jest następujący:

  • Najpierw łączymy się z Regionem Photon (np. USA Wschód, Europa, Azja itp.), który jest również znany jako Lobby.
  • Będąc w Lobby, prosimy o wszystkie Pokoje utworzone w Regionie, a następnie możemy albo dołączyć do jednego z Pokoi, albo stworzyć własny Pokój.
  • Po dołączeniu do pokoju prosimy o listę graczy podłączonych do pokoju i tworzymy instancje ich graczy, które następnie są synchronizowane z ich lokalnymi instancjami za pomocą PhotonView.
  • Kiedy ktoś opuści Pokój, jego instancja zostaje zniszczona, a osoba ta zostaje usunięta z Listy Graczy.

1. Konfiguracja lobby

Zacznijmy od stworzenia sceny Lobby, która będzie zawierać logikę Lobby (przeglądanie istniejących pokoi, tworzenie nowych itp.):

  • Utwórz nowy skrypt C# i nadaj mu nazwę PUN2_GameLobby
  • Utwórz nową scenę i nazwij ją "GameLobby"
  • W scenie GameLobby utwórz nowy obiekt GameObject. Nazwij go "_GameLobby" i przypisz do niego skrypt PUN2_GameLobby

Teraz otwórz skrypt PUN2_GameLobby:

Najpierw importujemy przestrzenie nazw Photon, dodając poniższe linie na początku skryptu:

using Photon.Pun;
using Photon.Realtime;

Przed kontynuowaniem musimy zastąpić domyślne MonoBehaviour przez MonoBehaviourPunCallbacks. Ten krok jest niezbędny, aby móc korzystać z wywołań zwrotnych Photon:

public class PUN2_GameLobby : MonoBehaviourPunCallbacks

Następnie tworzymy niezbędne zmienne:

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

Następnie wywołujemy ConnectUsingSettings() w pustej przestrzeni Start(). Oznacza to, że gdy tylko gra się otworzy, łączy się z serwerem 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();
        }
    }

Aby wiedzieć, czy połączenie z Photonem powiodło się, musimy zaimplementować następujące wywołania zwrotne: OnDisconnected(Przyczyna rozłączenia), 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;
    }

Następna jest część interfejsu użytkownika, w której odbywa się przeglądanie pokoju i tworzenie pokoju:

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

Na koniec implementujemy kolejne 4 wywołania zwrotne: OnCreateRoomFailed(krótki kod powrotu, wiadomość tekstowa), OnJoinRoomFailed(krótki kod powrotu, wiadomość tekstowa), OnCreatedRoom() i OnJoinedRoom().

Te wywołania zwrotne służą do ustalenia, czy dołączyliśmy do pokoju/utworzyliśmy pokój lub czy wystąpiły jakieś problemy podczas połączenia.

Oto ostateczny skrypt 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. Tworzenie prefabrykatu odtwarzacza

W grach wieloosobowych instancja Gracza ma dwie strony: lokalną i zdalną

Instancja lokalna jest kontrolowana lokalnie (przez nas).

Z kolei zdalna instancja jest lokalną reprezentacją tego, co robi drugi gracz. Nasz wkład nie powinien mieć na to wpływu.

Aby określić, czy instancja jest lokalna czy zdalna, używamy komponentu PhotonView.

PhotonView działa jako komunikator, który odbiera i wysyła wartości wymagające synchronizacji, na przykład położenie i obrót.

Zacznijmy więc od utworzenia instancji gracza (jeśli masz już gotową instancję gracza, możesz pominąć ten krok).

W moim przypadku instancja Gracza będzie prostą kostką, którą przesuwa się klawiszami W i S i obraca za pomocą klawiszy A i D.

Oto prosty skrypt kontrolera:

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

Następnym krokiem jest dodanie komponentu PhotonView.

  • Dodaj komponent PhotonView do instancji gracza.
  • Utwórz nowy skrypt C# i nadaj mu nazwę PUN2_PlayerSync (ten skrypt będzie używany do komunikacji poprzez PhotonView).

Otwórz skrypt PUN2_PlayerSync:

W PUN2_PlayerSync pierwszą rzeczą, którą musimy zrobić, to dodać przestrzeń nazw Photon.Pun i zastąpić MonoBehaviour przez MonoBehaviourPun, a także dodać interfejs IPunObservable.

Aby móc używać buforowanej zmiennej photonView, zamiast używać GetComponent<PhotonView>(), konieczne jest użycie MonoBehaviourPun.

using UnityEngine;
using Photon.Pun;

public class PUN2_PlayerSync : MonoBehaviourPun, IPunObservable

Następnie możemy przystąpić do tworzenia wszystkich niezbędnych zmiennych:

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

Następnie w void Start() sprawdzamy, czy odtwarzacz jest lokalny czy zdalny, używając 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);
            }
        }
    }

Rzeczywista synchronizacja odbywa się poprzez wywołanie zwrotne PhotonView: OnPhotonSerializeView(strumień PhotonStream, informacje o 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();
        }
    }

W tym przypadku wysyłamy tylko pozycję i rotację odtwarzacza, ale możesz skorzystać z powyższego przykładu, aby wysłać dowolną wartość potrzebną do synchronizacji przez sieć z dużą częstotliwością.

Otrzymane wartości są następnie stosowane w 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);
        }
    }
}

Oto ostateczny skrypt 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);
        }
    }
}

Przypiszmy teraz nowo utworzonemu skryptowi:

  • Dołącz skrypt PUN2_PlayerSync do PlayerInstance.
  • Przeciągnij i upuść PUN2_PlayerSync do obserwowanych komponentów PhotonView.
  • Przypisz SimplePlayerController do "Local Scripts" i przypisz GameObjects (które chcesz dezaktywować dla zdalnych graczy) do "Local Objects"

  • Zapisz PlayerInstance w Prefab i przenieś go do folderu o nazwie Resources (jeśli nie ma takiego folderu, utwórz go). Ten krok jest niezbędny, aby móc tworzyć obiekty dla wielu graczy w sieci.

3. Tworzenie poziomu gry

GameLevel to scena, która ładuje się po dołączeniu do Pokoju i to tam dzieje się cała akcja.

  • Utwórz nową Scenę i nadaj jej nazwę "GameLevel" (lub jeśli chcesz zachować inną nazwę, pamiętaj o zmianie nazwy w tej linii PhotonNetwork.LoadLevel("GameLevel"); w PUN2_GameLobby.cs).

W moim przypadku użyję prostej sceny z płaszczyzną:

  • Teraz utwórz nowy skrypt i nazwij go PUN2_RoomController (ten skrypt zajmie się logiką panującą w Pokoju, np. spawnowaniem graczy, wyświetlaniem listy graczy itp.).

Otwórz skrypt PUN2_RoomController:

Podobnie jak PUN2_GameLobby zaczynamy od dodania przestrzeni nazw Photon i zastąpienia MonoBehaviour przez MonoBehaviourPunCallbacks:

using UnityEngine;
using Photon.Pun;

public class PUN2_RoomController : MonoBehaviourPunCallbacks

Dodajmy teraz niezbędne zmienne:

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

Aby utworzyć prefabrykat Playera, używamy 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);
    }

Oraz prosty interfejs użytkownika z przyciskiem "Leave Room" i kilkoma dodatkowymi elementami, takimi jak nazwa pokoju i lista podłączonych graczy:

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

Na koniec implementujemy kolejne wywołanie zwrotne PhotonNetwork o nazwie OnLeftRoom(), które jest wywoływane, gdy opuszczamy pokój:

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

Oto ostateczny skrypt 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");
    }
}
  • Utwórz nowy obiekt GameObject w scenie 'GameLevel' i wywołaj go "_RoomController"
  • Dołącz skrypt PUN2_RoomController do obiektu _RoomController
  • Przypisz prefabrykat PlayerInstance i transformację SpawnPoint, a następnie zapisz scenę

  • Dodaj zarówno MainMenu, jak i GameLevel do ustawień kompilacji.

4. Tworzenie kompilacji testowej

Teraz czas na wykonanie kompilacji i przetestowanie jej:

Wszystko działa zgodnie z oczekiwaniami!

Premia

RPC

W PUN 2 RPC oznacza zdalne wywołanie procedury i służy do wywoływania funkcji na zdalnych klientach znajdujących się w tym samym pokoju (więcej na ten temat możesz przeczytać tutaj).

RPC mają wiele zastosowań, powiedzmy, że chcesz wysłać wiadomość na czacie do wszystkich graczy w Pokoju. Dzięki RPC jest to łatwe:

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

Zwróć uwagę na [PunRPC] przed funkcją. Ten atrybut jest niezbędny, jeśli planujesz wywołać funkcję za pośrednictwem wywołań RPC.

Do wywołania funkcji oznaczonych jako RPC potrzebny jest PhotonView. Przykładowe wywołanie:

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

Wskazówka dla profesjonalistów: Jeśli zastąpisz MonoBehaviour w swoim skrypcie przez MonoBehaviourPun lub MonoBehaviourPunCallbacks, możesz pominąć PhotonView.Get() i użyć bezpośrednio photonView.RPC().

Właściwości niestandardowe

W PUN 2 właściwości niestandardowe to tablica mieszająca, którą można przypisać do gracza lub pokoju.

Jest to przydatne, gdy chcesz ustawić trwałe dane, które nie muszą być często zmieniane (np. nazwa drużyny gracza, tryb gry w pomieszczeniu itp.).

Najpierw musisz zdefiniować Hashtable, co można zrobić, dodając poniższą linię na początku skryptu:

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

Poniższy przykład ustawia właściwości Room o nazwach "GameMode" i "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"]);

Właściwości odtwarzacza ustawia się podobnie:

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

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

Aby usunąć konkretną właściwość, po prostu ustaw jej wartość na null.

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

Dodatkowe tutoriale:

Synchronizuj sztywne ciała przez sieć za pomocą PUN 2

PUN 2 Dodawanie czatu w pokoju

Źródło
📁PUN2Guide.unitypackage14.00 MB
Sugerowane artykuły
Synchronizuj sztywne ciała przez sieć za pomocą PUN 2
Unity Dodanie czatu wieloosobowego do pokoi PUN 2
Kompresja danych i manipulacja bitami w trybie wieloosobowym
Stwórz wieloosobową grę samochodową w PUN 2
Przewodnik dla początkujących sieci Photon (klasyczny).
Tworzenie wieloosobowych gier sieciowych w Unity
Samouczek dotyczący tablicy liderów Unity Online