Skapa ett multiplayer-spel i Unity med PUN 2

Har du någonsin undrat vad som krävs för att skapa ett multiplayer-spel i Unity?

Till skillnad från enspelarspel kräver spel för flera spelare en fjärrserver som spelar rollen som bryggan, vilket gör att spelklienter kan kommunicera med varandra.

Nuförtiden tar många tjänster hand om serverhosting. En sådan tjänst är Photon Network, som vi kommer att använda för denna handledning.

PUN 2 är den senaste versionen av deras API som har förbättrats avsevärt jämfört med den äldre versionen.

I det här inlägget kommer vi att köra genom att ladda ner de nödvändiga filerna, ställa in Photon AppID och programmera ett enkelt multiplayer-exempel.

Unity version som används i denna handledning: Unity 2018.3.0f2 (64-bitars)

Del 1: Ställa in PUN 2

Det första steget är att ladda ner ett PUN 2-paket från Asset Store. Den innehåller alla skript och filer som krävs för integration med flera spelare.

  • Öppna ditt Unity-projekt och gå sedan till Asset Store: (Fönster -> Allmänt -> AssetStore) eller tryck på Ctrl+9
  • Sök efter "PUN 2- Free" och klicka sedan på det första resultatet eller klicka här
  • Importera PUN 2-paketet efter att nedladdningen är klar

  • Efter att paketet har importerats måste du skapa ett Photon App ID, detta görs på deras hemsida: https://www.photonengine.com/
  • Skapa ett nytt konto (eller logga in på ditt befintliga konto)
  • Gå till applikationssidan genom att klicka på profilikonen och sedan "Your Applications" eller följ denna länk: https://dashboard.photonengine.com/en-US/PublicCloud
  • Klicka på sidan Applikationer "Create new app"

  • På sidan för att skapa, för Photon Type välj "Photon Realtime" och för Namn, skriv valfritt namn och klicka sedan "Create"

Som du kan se använder applikationen gratisplanen som standard. Du kan läsa mer om prisplaner här

  • När applikationen har skapats kopierar du app-ID:t under appnamnet

  • Gå tillbaka till ditt Unity-projekt och gå sedan till Fönster -> Foton Unity Nätverk -> PUN Wizard
  • Klicka på "Setup Project" i PUN Wizard, klistra in ditt app-ID och klicka sedan "Setup Project"

  • PUN 2 är nu klar!

Del 2: Skapa ett spel för flera spelare

Låt oss nu gå till delen där vi faktiskt skapar ett spel för flera spelare.

Sättet multiplayer hanteras i PUN 2 är:

  • Först ansluter vi till Photon Region (ex. USA East, Europa, Asien, etc.) som också är känd som lobbyn.
  • Väl i lobbyn begär vi alla rum som skapas i regionen och sedan kan vi antingen gå med i ett av rummen eller skapa vårt eget rum.
  • Efter att ha gått med i rummet begär vi en lista över spelarna som är anslutna till rummet och instansierar deras spelarinstanser, som sedan synkroniseras med deras lokala instanser via PhotonView.
  • När någon lämnar rummet förstörs deras instans och de tas bort från spelarlistan.

1. Skapa en lobby

Låt oss börja med att skapa en lobbyscen som kommer att innehålla lobbylogik (bläddra i befintliga rum, skapa nya rum, etc.):

  • Skapa ett nytt C#-skript och kalla det PUN2_GameLobby
  • Skapa en ny scen och kalla den "GameLobby"
  • Skapa ett nytt GameObject i GameLobby-scenen. Kalla det "_GameLobby" och tilldela PUN2_GameLobby-skriptet till det

Öppna nu PUN2_GameLobby-skriptet:

Först importerar vi Photon-namnområdena genom att lägga till raderna nedan i början av skriptet:

using Photon.Pun;
using Photon.Realtime;

Innan vi fortsätter måste vi ersätta standardmonobeteendet med MonoBehaviourPunCallbacks. Detta steg är nödvändigt för att kunna använda Photon callbacks:

public class PUN2_GameLobby : MonoBehaviourPunCallbacks

Därefter skapar vi de nödvändiga variablerna:

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

Sedan anropar vi ConnectUsingSettings() i tomrummet Start(). Det betyder att så snart spelet öppnas ansluts det till Photon Server:

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

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

För att veta om en anslutning till Photon lyckades, måste vi implementera dessa callbacks: OnDisconnected(DisconnectCause orsak), 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;
    }

Nästa är UI-delen, där rumssökning och skapande av rum görs:

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

Och slutligen implementerar vi ytterligare fyra callbacks: OnCreateRoomFailed(short returnCode, strängmeddelande), OnJoinRoomFailed(short returnCode, strängmeddelande), OnCreatedRoom(), och OnJoinedRoom().

Dessa återuppringningar används för att avgöra om vi gick med/skapade rummet eller om det fanns några problem under anslutningen.

Här är det sista PUN2_GameLobby.cs-skriptet:

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. Skapa en prefab för spelare

I spel med flera spelare har Player-instansen två sidor: Lokal och Fjärr

En lokal instans kontrolleras lokalt (av oss).

En avlägsen instans, å andra sidan, är en lokal representation av vad den andra spelaren gör. Det bör vara opåverkat av vår input.

För att avgöra om instansen är lokal eller fjärrstyrd använder vi en PhotonView-komponent.

PhotonView fungerar som en budbärare som tar emot och skickar de värden som behöver synkas, till exempel position och rotation.

Så låt oss börja med att skapa spelarinstansen (om du redan har din spelarinstans redo kan du hoppa över det här steget).

I mitt fall kommer Player-instansen att vara en enkel kub som flyttas med W- och S-tangenterna och roteras med A- och D-tangenterna.

Här är ett enkelt kontrollskript:

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

Nästa steg är att lägga till en PhotonView-komponent.

  • Lägg till en PhotonView-komponent till Player Instance.
  • Skapa ett nytt C#-skript och kalla det PUN2_PlayerSync (detta skript kommer att användas för att kommunicera genom PhotonView).

Öppna PUN2_PlayerSync-skript:

I PUN2_PlayerSync är det första vi behöver göra att lägga till ett Photon.Pun-namnområde och ersätta MonoBehaviour med MonoBehaviourPun och även lägga till IPunObservable-gränssnittet.

MonoBehaviourPun är nödvändigt för att kunna använda den cachade photonView-variabeln, istället för att använda GetComponent<PhotonView>().

using UnityEngine;
using Photon.Pun;

public class PUN2_PlayerSync : MonoBehaviourPun, IPunObservable

Efter det kan vi flytta för att skapa alla nödvändiga variabler:

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

Sedan i tomrummet Start(), kontrollerar vi om spelaren är lokal eller fjärrstyrd genom att använda 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);
            }
        }
    }

Själva synkroniseringen görs genom PhotonViews återuppringning: OnPhotonSerializeView(PhotonStream-ström, 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();
        }
    }

I det här fallet skickar vi bara spelaren Position och Rotation, men du kan använda exemplet ovan för att skicka valfritt värde som behövs för att synkroniseras över nätverket, med hög frekvens.

Mottagna värden tillämpas sedan i 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);
        }
    }
}

Här är det sista PUN2_PlayerSync.cs-skriptet:

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

Låt oss nu tilldela ett nyskapat skript:

  • Bifoga PUN2_PlayerSync-skriptet till PlayerInstance.
  • Dra och släpp PUN2_PlayerSync i PhotonView Observed Components.
  • Tilldela SimplePlayerController till "Local Scripts" och tilldela GameObjects (som du vill ska inaktiveras för fjärrspelare) till "Local Objects"

  • Spara PlayerInstance till Prefab och flytta den till mappen som heter Resources (Om det inte finns någon sådan mapp, skapa en). Detta steg är nödvändigt för att kunna skapa flerspelarobjekt över nätverket.

3. Skapa en spelnivå

GameLevel är en scen som laddas efter att ha gått med i rummet och det är där all action sker.

  • Skapa en ny scen och kalla den "GameLevel" (Eller om du vill behålla ett annat namn, se till att ändra namnet på den här raden PhotonNetwork.LoadLevel("GameLevel"); på PUN2_GameLobby.cs).

I mitt fall kommer jag att använda en enkel scen med ett plan:

  • Skapa nu ett nytt skript och kalla det PUN2_RoomController (Detta skript kommer att hantera logiken i rummet, som att skapa spelarna, visa spelarlistan, etc.).

Öppna PUN2_RoomController-skript:

Samma som PUN2_GameLobby börjar vi med att lägga till Photon namespaces och ersätta MonoBehaviour med MonoBehaviourPunCallbacks:

using UnityEngine;
using Photon.Pun;

public class PUN2_RoomController : MonoBehaviourPunCallbacks

Låt oss nu lägga till de nödvändiga variablerna:

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

För att instansiera Player prefab använder vi 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);
    }

Och ett enkelt användargränssnitt med en "Leave Room"-knapp och några ytterligare element som rumsnamn och listan över anslutna spelare:

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

Slutligen implementerar vi en annan PhotonNetwork callback som heter OnLeftRoom() som anropas när vi lämnar rummet:

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

Här är det sista PUN2_RoomController.cs-skriptet:

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");
    }
}
  • Skapa ett nytt GameObject i 'GameLevel'-scenen och anrop det "_RoomController"
  • Bifoga PUN2_RoomController-skriptet till _RoomController-objektet
  • Tilldela PlayerInstance prefab och en SpawnPoint Transform till den och spara sedan scenen

  • Lägg till både MainMenu och GameLevel i bygginställningarna.

4. Göra ett testbygge

Nu är det dags att bygga och testa den:

Allt fungerar som förväntat!

Bonus

RPC

I PUN 2 står RPC för Remote Procedure Call, det används för att anropa en funktion på Remote-klienter som finns i samma rum (du kan läsa mer om det här).

RPC:er har många användningsområden, låt oss till exempel säga att du behöver skicka ett chattmeddelande till alla spelare i rummet. Med RPC:er är det enkelt att göra det:

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

Lägg märke till [PunRPC] före funktionen. Detta attribut är nödvändigt om du planerar att anropa funktionen via RPC:er.

För att anropa funktionerna markerade som RPC behöver du en PhotonView. Exempelsamtal:

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

Proffstips: Om du ersätter MonoBehaviour i ditt skript med MonoBehaviourPun eller MonoBehaviourPunCallbacks kan du hoppa över PhotonView.Get() och använda photonView.RPC() direkt.

Anpassade egenskaper

I PUN 2 är anpassade egenskaper en hashtabell som kan tilldelas en spelare eller rummet.

Detta är användbart när du behöver ställa in beständiga data som inte behöver ändras ofta (t.ex. spelarens lagnamn, rumsspelsläge, etc.).

Först måste du definiera en hashtabell, vilket görs genom att lägga till raden nedan i början av skriptet:

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

Exemplet nedan ställer in rumsegenskaperna som heter "GameMode" och "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"]);

Spelaregenskaper ställs in på liknande sätt:

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

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

För att ta bort en specifik egenskap sätt bara dess värde till null.

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

Ytterligare tutorials:

Synkronisera stela kroppar över nätverk med PUN 2

PUN 2 Adding Room Chat

Källa
📁PUN2Guide.unitypackage14.00 MB
Föreslagna artiklar
Synkronisera stela kroppar över nätverk med PUN 2
Unity Lägger till flerspelarchatt till PUN 2-rummen
Multiplayer-datakomprimering och bitmanipulation
Gör ett bilspel för flera spelare med PUN 2
Bygga nätverksspel för flera spelare i Unity
Unity Login System med PHP och MySQL
Photon Network (Classic) Nybörjarguide