Lav et multiplayer-spil i Unity ved hjælp af PUN 2

Har du nogensinde spekuleret på, hvad der skal til for at skabe et multiplayer-spil i Unity?

I modsætning til single-player-spil kræver multiplayer-spil en fjernserver, der spiller rollen som broen, så spilklienter kan kommunikere med hinanden.

I dag tager mange tjenester sig af serverhosting. En sådan tjeneste er Photon Network, som vi vil bruge til denne tutorial.

PUN 2 er den seneste udgivelse af deres API, som er blevet væsentligt forbedret i forhold til den ældre version.

I dette indlæg kører vi gennem download af de nødvendige filer, opsætning af Photon AppID og programmering af et simpelt multiplayer-eksempel.

Unity version brugt i denne øvelse: Unity 2018.3.0f2 (64-bit)

Del 1: Opsætning af PUN 2

Det første trin er at downloade en PUN 2-pakke fra Asset Store. Den indeholder alle de scripts og filer, der kræves til multiplayer-integration.

  • Åbn dit Unity-projekt og gå derefter til Asset Store: (Vindue -> Generelt -> AssetStore) eller tryk på Ctrl+9
  • Søg efter "PUN 2- Free" og klik derefter på det første resultat eller klik her
  • Importer PUN 2-pakken, efter at overførslen er færdig

  • På oprettelsessiden, for Photon Type, vælg "Photon Realtime" og for navnet, skriv et hvilket som helst navn og klik derefter "Create"

Som du kan se, er applikationen som standard den gratis plan. Du kan læse mere om prisplaner her

  • Når applikationen er oprettet, skal du kopiere app-id'et, der er placeret under app-navnet

  • Gå tilbage til dit Unity-projekt og gå derefter til Window -> Photon Unity Networking -> PUN Wizard
  • Klik på "Setup Project" i PUN Wizard, indsæt dit app-id og klik derefter "Setup Project"

  • PUN 2 er nu klar!

Del 2: Oprettelse af et multiplayer-spil

Lad os nu gå til den del, hvor vi faktisk skaber et multiplayer-spil.

Måden multiplayer håndteres i PUN 2 er:

  • Først forbinder vi os til Photon-regionen (f.eks. USA Øst, Europa, Asien osv.), som også er kendt som lobbyen.
  • Når vi først er i lobbyen, anmoder vi om alle de rum, der er oprettet i regionen, og så kan vi enten tilslutte os et af lokalerne eller oprette vores eget lokale.
  • Når vi har tilmeldt os rummet, anmoder vi om en liste over de spillere, der er forbundet til rummet og instansierer deres afspillerforekomster, som derefter synkroniseres med deres lokale forekomster gennem PhotonView.
  • Når nogen forlader rummet, bliver deres instans ødelagt, og de fjernes fra spillerlisten.

1. Oprettelse af en lobby

Lad os starte med at oprette en lobbyscene, der vil indeholde lobbylogik (gennemgang af eksisterende rum, oprettelse af nye rum osv.):

  • Opret et nyt C#-script og kald det PUN2_GameLobby
  • Opret en ny scene og kald den "GameLobby"
  • Opret et nyt GameObject i GameLobby-scenen. Kald det "_GameLobby" og tildel PUN2_GameLobby-scriptet til det

Åbn nu PUN2_GameLobby-scriptet:

Først importerer vi Photon-navnerummene ved at tilføje linjerne nedenfor i begyndelsen af ​​scriptet:

using Photon.Pun;
using Photon.Realtime;

Før vi fortsætter, skal vi også erstatte standard MonoBehaviour med MonoBehaviourPunCallbacks. Dette trin er nødvendigt for at kunne bruge Photon-tilbagekald:

public class PUN2_GameLobby : MonoBehaviourPunCallbacks

Dernæst opretter vi de nødvendige variabler:

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

Så kalder vi ConnectUsingSettings() i det tomme Start(). Det betyder, at så snart spillet åbner, opretter det forbindelse til 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();
        }
    }

For at vide, om en forbindelse til Photon var vellykket, skal vi implementere disse tilbagekald: 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;
    }

Dernæst er UI-delen, hvor rumbrowsing og rumoprettelse udføres:

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

Og til sidst implementerer vi yderligere 4 tilbagekald: OnCreateRoomFailed(short returnCode, string message), OnJoinRoomFailed(short returnCode, string message), OnCreatedRoom(), og OnJoinedRoom().

Disse tilbagekald bruges til at afgøre, om vi sluttede os til/oprettede rummet, eller om der var problemer under forbindelsen.

Her er det endelige PUN2_GameLobby.cs-script:

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. Oprettelse af en Player prefab

I multiplayer-spil har Player-instansen 2 sider: Lokal og Remote

En lokal instans kontrolleres lokalt (af os).

En fjerninstans er på den anden side en lokal repræsentation af, hvad den anden spiller gør. Det burde være upåvirket af vores input.

For at afgøre, om instansen er lokal eller ekstern, bruger vi en PhotonView-komponent.

PhotonView fungerer som en messenger, der modtager og sender de værdier, der skal synkroniseres, for eksempel position og rotation.

Så lad os starte med at oprette afspillerinstansen (hvis du allerede har din afspillerinstans klar, kan du springe dette trin over).

I mit tilfælde vil Player-instansen være en simpel terning, der flyttes med W- og S-tasterne og roteres med A- og D-tasterne.

Her er et simpelt controller-script:

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

Det næste trin er at tilføje en PhotonView-komponent.

  • Tilføj en PhotonView-komponent til Player Instance.
  • Opret et nyt C# script, og kald det PUN2_PlayerSync (dette script vil blive brugt til at kommunikere gennem PhotonView).

Åbn PUN2_PlayerSync script:

I PUN2_PlayerSync er det første, vi skal gøre, at tilføje et Photon.Pun-navneområde og erstatte MonoBehaviour med MonoBehaviourPun og også tilføje IPunObservable-grænseflade.

MonoBehaviourPun er nødvendig for at kunne bruge den cachelagrede photonView-variabel i stedet for at bruge GetComponent<PhotonView>().

using UnityEngine;
using Photon.Pun;

public class PUN2_PlayerSync : MonoBehaviourPun, IPunObservable

Derefter kan vi flytte for at oprette alle de nødvendige 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;

Derefter tjekker vi i det tomme Start() om afspilleren er lokal eller fjern ved at bruge 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);
            }
        }
    }

Selve synkroniseringen udføres gennem PhotonViews tilbagekald: OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info):

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

I dette tilfælde sender vi kun spillerens position og rotation, men du kan bruge eksemplet ovenfor til at sende enhver værdi, der er nødvendig for at blive synkroniseret over netværket, ved en høj frekvens.

Modtagne værdier anvendes derefter i den 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);
        }
    }
}

Her er det endelige PUN2_PlayerSync.cs-script:

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

Lad os nu tildele et nyoprettet script:

  • Vedhæft PUN2_PlayerSync-scriptet til PlayerInstance.
  • Træk og slip PUN2_PlayerSync i PhotonView Observed Components.
  • Tildel SimplePlayerController til "Local Scripts" og tildel GameObjects (som du ønsker skal deaktiveres for Remote-spillere) til "Local Objects"

  • Gem PlayerInstance til Prefab og flyt den til mappen kaldet Ressourcer (Hvis der ikke er en sådan mappe, opret en). Dette trin er nødvendigt for at kunne skabe multiplayer-objekter over netværket.

3. Oprettelse af et spilniveau

GameLevel er en scene, der indlæses efter at have tilmeldt sig rummet, og det er her al handlingen sker.

  • Opret en ny Scene og kald den "GameLevel" (Eller hvis du vil beholde et andet navn, så sørg for at ændre navnet på denne linje PhotonNetwork.LoadLevel("GameLevel"); på PUN2_GameLobby.cs).

I mit tilfælde vil jeg bruge en simpel scene med et fly:

  • Opret nu et nyt script og kald det PUN2_RoomController (Dette script vil håndtere logikken inde i rummet, som at skabe spillerne, vise spillerlisten osv.).

Åbn PUN2_RoomController script:

På samme måde som PUN2_GameLobby begynder vi med at tilføje Photon-navneområder og erstatte MonoBehaviour med MonoBehaviourPunCallbacks:

using UnityEngine;
using Photon.Pun;

public class PUN2_RoomController : MonoBehaviourPunCallbacks

Lad os nu tilføje de nødvendige variabler:

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

For at instantiere afspillerens præfabrikerede bruger 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);
    }

Og en simpel brugergrænseflade med en "Leave Room"-knap og nogle ekstra elementer såsom værelsesnavn og listen over tilsluttede spillere:

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

Til sidst implementerer vi endnu et PhotonNetwork callback kaldet OnLeftRoom(), som kaldes, når vi forlader rummet:

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

Her er det endelige PUN2_RoomController.cs-script:

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");
    }
}
  • Opret et nyt GameObject i 'GameLevel'-scenen og kald det "_RoomController"
  • Vedhæft PUN2_RoomController-scriptet til _RoomController-objektet
  • Tildel PlayerInstance-præfabrikken og en SpawnPoint-transformation til den, og gem derefter scenen

  • Tilføj både MainMenu og GameLevel til Build-indstillingerne.

4. Lav en test Build

Nu er det tid til at lave en build og teste den:

Alt fungerer som forventet!

Bonus

RPC

I PUN 2 står RPC for Remote Procedure Call, det bruges til at kalde en funktion på Remote-klienter, der er i samme rum (Du kan læse mere om det her).

RPC'er har mange anvendelsesmuligheder, lad os f.eks. sige, at du skal sende en chatbesked til alle spillerne i rummet. Med RPC'er er det nemt at gøre det:

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

Bemærk [PunRPC] før funktionen. Denne egenskab er nødvendig, hvis du planlægger at kalde funktionen via RPC'er.

For at kalde de funktioner, der er markeret som RPC, skal du bruge en PhotonView. Eksempel opkald:

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

Pro tip: Hvis du erstatter MonoBehaviour i dit script med MonoBehaviourPun eller MonoBehaviourPunCallbacks, kan du springe PhotonView.Get() over og bruge photonView.RPC() direkte.

Brugerdefinerede egenskaber

I PUN 2 er brugerdefinerede egenskaber en hashtabel, der kan tildeles en spiller eller rummet.

Dette er nyttigt, når du har brug for at indstille vedvarende data, der ikke skal ændres ofte (f.eks. spillerholdsnavn, rumspiltilstand osv.).

Først skal du definere en hashtabel, hvilket gøres ved at tilføje linjen nedenfor i begyndelsen af ​​scriptet:

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

Eksemplet nedenfor angiver rumegenskaberne kaldet "GameMode" og "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"]);

Spilleregenskaber er indstillet på samme måde:

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

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

For at fjerne en specifik egenskab skal du blot sætte dens værdi til null.

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

Yderligere tutorials:

Synkroniser rigidbodies over netværk ved hjælp af PUN 2

PUN 2 Tilføjelse af rumchat

Kilde
📁PUN2Guide.unitypackage14.00 MB
Foreslåede artikler
Synkroniser rigidbodies over netværk ved hjælp af PUN 2
Unity tilføjer multiplayer-chat til PUN 2-rummene
Multiplayer datakomprimering og bitmanipulation
Lav et multiplayer bilspil med PUN 2
Photon Network (klassisk) begyndervejledning
Opbygning af multiplayer-netværksspil i Unity
PUN 2 Lagkompensation