Realizza un gioco di auto multiplayer con PUN 2

Realizzare un gioco multiplayer in Unity è un compito complesso, ma fortunatamente diverse soluzioni semplificano il processo di sviluppo.

Una di queste soluzioni è Photon Network. Nello specifico, l'ultima versione della loro API chiamata PUN 2 si occupa dell'hosting del server e ti lascia libero di creare un gioco multiplayer nel modo che desideri.

In questo tutorial mostrerò come creare un semplice gioco di auto con sincronizzazione fisica utilizzando PUN 2.

Unity versione utilizzata in questo tutorial: Unity 2018.3.0f2 (64 bit)

Parte 1: configurazione del PUN 2

Il primo passo è scaricare un pacchetto PUN 2 da Asset Store. Contiene tutti gli script e i file necessari per l'integrazione multiplayer.

  • Apri il tuo progetto Unity quindi vai su Asset Store: (Finestra -> Generale -> AssetStore) o premi Ctrl+9
  • Cerca "PUN 2- Free" quindi fai clic sul primo risultato oppure fai clic qui
  • Importa il pacchetto PUN 2 al termine del download

  • Dopo che il pacchetto è stato importato, è necessario creare un ID app Photon, questa operazione viene eseguita sul loro sito Web: https://www.photonengine.com/
  • Crea un nuovo account (o accedi al tuo account esistente)
  • Vai alla pagina Applicazioni facendo clic sull'icona del profilo, quindi su "Your Applications" o segui questo collegamento: https://dashboard.photonengine.com/en-US/PublicCloud
  • Nella pagina Applicazioni fare clic su "Create new app"

  • Nella pagina di creazione, per Tipo fotone selezionare "Photon Realtime" e per Nome, digitare un nome qualsiasi, quindi fare clic "Create"

Come puoi vedere, l'applicazione utilizza per impostazione predefinita il piano gratuito. Puoi leggere ulteriori informazioni sui piani tariffari qui

  • Una volta creata l'applicazione, copiare l'ID app situato sotto il nome dell'app

  • Torna al tuo progetto Unity quindi vai su Finestra -> Photon Unity Networking -> PUN Wizard
  • Nella procedura guidata PUN, fai clic su "Setup Project", incolla l'ID dell'app, quindi fai clic su "Setup Project"

Il PUN 2 è ora pronto!

Parte 2: Creazione di un gioco di auto multiplayer

1. Impostazione di una lobby

Iniziamo creando una scena della lobby che conterrà la logica della lobby (esplorazione delle stanze esistenti, creazione di nuove stanze, ecc.):

  • Crea una nuova scena e chiamala "GameLobby"
  • Nella scena "GameLobby" crea un nuovo GameObject e chiamalo "_GameLobby"
  • Crea un nuovo script C# e chiamalo "PUN2_GameLobby", quindi collegalo all'oggetto "_GameLobby"
  • Incolla il codice qui sotto all'interno dello script "PUN2_GameLobby"

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 = "1.0";
    //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()
    {
        //Initialize Player name
        playerName = "Player " + Random.Range(111, 999);

        //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;
            PhotonNetwork.PhotonServerSettings.AppSettings.FixedRegion = "eu";
            // 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 Playground (Make sure it's added to build settings)
        PhotonNetwork.LoadLevel("Playground");
    }

    public override void OnJoinedRoom()
    {
        Debug.Log("OnJoinedRoom");
    }
}

2. Creazione di un prefabbricato per auto

Il prefabbricato per auto utilizzerà un semplice controller fisico.

  • Crea un nuovo GameObject e chiamalo "CarRoot"
  • Crea un nuovo cubo e spostalo all'interno dell'oggetto "CarRoot", quindi ingrandiscilo lungo gli assi Z e X

  • Crea un nuovo GameObject e chiamalo "wfl" (abbreviazione di Wheel Front Left)
  • Aggiungi il componente Wheel Collider all'oggetto "wfl" e imposta i valori dall'immagine seguente:

  • Crea un nuovo GameObject, rinominalo in "WheelTransform" quindi spostalo all'interno dell'oggetto "wfl"
  • Crea un nuovo cilindro, spostalo all'interno dell'oggetto "WheelTransform", quindi ruotalo e ridimensionalo finché non corrisponde alle dimensioni del Wheel Collider. Nel mio caso, la scala è (1, 0,17, 1)

  • Infine, duplica l'oggetto "wfl" 3 volte per il resto delle ruote e rinomina ciascun oggetto rispettivamente in "wfr" (Ruota anteriore destra), "wrr" (Ruota posteriore destra) e "wrl" (Ruota posteriore sinistra)

  • Crea un nuovo script, chiamalo "SC_CarController" quindi incolla al suo interno il codice seguente:

SC_CarController.cs

using UnityEngine;
using System.Collections;

public class SC_CarController : MonoBehaviour
{
    public WheelCollider WheelFL;
    public WheelCollider WheelFR;
    public WheelCollider WheelRL;
    public WheelCollider WheelRR;
    public Transform WheelFLTrans;
    public Transform WheelFRTrans;
    public Transform WheelRLTrans;
    public Transform WheelRRTrans;
    public float steeringAngle = 45;
    public float maxTorque = 1000;
    public  float maxBrakeTorque = 500;
    public Transform centerOfMass;

    float gravity = 9.8f;
    bool braked = false;
    Rigidbody rb;
    
    void Start()
    {
        rb = GetComponent<Rigidbody>();
        rb.centerOfMass = centerOfMass.transform.localPosition;
    }

    void FixedUpdate()
    {
        if (!braked)
        {
            WheelFL.brakeTorque = 0;
            WheelFR.brakeTorque = 0;
            WheelRL.brakeTorque = 0;
            WheelRR.brakeTorque = 0;
        }
        //Speed of car, Car will move as you will provide the input to it.

        WheelRR.motorTorque = maxTorque * Input.GetAxis("Vertical");
        WheelRL.motorTorque = maxTorque * Input.GetAxis("Vertical");

        //Changing car direction
        //Here we are changing the steer angle of the front tyres of the car so that we can change the car direction.
        WheelFL.steerAngle = steeringAngle * Input.GetAxis("Horizontal");
        WheelFR.steerAngle = steeringAngle * Input.GetAxis("Horizontal");
    }
    void Update()
    {
        HandBrake();

        //For tyre rotate
        WheelFLTrans.Rotate(WheelFL.rpm / 60 * 360 * Time.deltaTime, 0, 0);
        WheelFRTrans.Rotate(WheelFR.rpm / 60 * 360 * Time.deltaTime, 0, 0);
        WheelRLTrans.Rotate(WheelRL.rpm / 60 * 360 * Time.deltaTime, 0, 0);
        WheelRRTrans.Rotate(WheelRL.rpm / 60 * 360 * Time.deltaTime, 0, 0);
        //Changing tyre direction
        Vector3 temp = WheelFLTrans.localEulerAngles;
        Vector3 temp1 = WheelFRTrans.localEulerAngles;
        temp.y = WheelFL.steerAngle - (WheelFLTrans.localEulerAngles.z);
        WheelFLTrans.localEulerAngles = temp;
        temp1.y = WheelFR.steerAngle - WheelFRTrans.localEulerAngles.z;
        WheelFRTrans.localEulerAngles = temp1;
    }
    void HandBrake()
    {
        //Debug.Log("brakes " + braked);
        if (Input.GetButton("Jump"))
        {
            braked = true;
        }
        else
        {
            braked = false;
        }
        if (braked)
        {

            WheelRL.brakeTorque = maxBrakeTorque * 20;//0000;
            WheelRR.brakeTorque = maxBrakeTorque * 20;//0000;
            WheelRL.motorTorque = 0;
            WheelRR.motorTorque = 0;
        }
    }
}
  • Allega lo script SC_CarController all'oggetto "CarRoot"
  • Collega il componente Rigidbody all'oggetto "CarRoot" e modifica la sua massa su 1000
  • Assegna le variabili della ruota in SC_CarController (Wheel collider per le prime 4 variabili e WheelTransform per il resto delle 4)

  • Per la variabile Centro di massa crea un nuovo GameObject, chiamalo "CenterOfMass" e spostalo all'interno dell'oggetto "CarRoot"
  • Posiziona l'oggetto "CenterOfMass" al centro e leggermente verso il basso, in questo modo:

  • Infine, a scopo di test, sposta la fotocamera principale all'interno dell'oggetto "CarRoot" e puntala verso l'auto:

  • Crea un nuovo script, chiamalo "PUN2_CarSync" quindi incolla al suo interno il codice seguente:

PUN2_CarSync.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;

public class PUN2_CarSync : MonoBehaviourPun, IPunObservable
{
    public MonoBehaviour[] localScripts; //Scripts that should only be enabled for the local player (Ex. Car controller)
    public GameObject[] localObjects; //Objects that should only be active for the local player (Ex. Camera)
    public Transform[] wheels; //Car wheel transforms

    Rigidbody r;
    // Values that will be synced over network
    Vector3 latestPos;
    Quaternion latestRot;
    Vector3 latestVelocity;
    Vector3 latestAngularVelocity;
    Quaternion[] wheelRotations = new Quaternion[0];
    // Lag compensation
    float currentTime = 0;
    double currentPacketTime = 0;
    double lastPacketTime = 0;
    Vector3 positionAtLastPacket = Vector3.zero;
    Quaternion rotationAtLastPacket = Quaternion.identity;
    Vector3 velocityAtLastPacket = Vector3.zero;
    Vector3 angularVelocityAtLastPacket = Vector3.zero;

    // Use this for initialization
    void Awake()
    {
        r = GetComponent<Rigidbody>();
        r.isKinematic = !photonView.IsMine;
        for (int i = 0; i < localScripts.Length; i++)
        {
            localScripts[i].enabled = photonView.IsMine;
        }
        for (int i = 0; i < localObjects.Length; i++)
        {
            localObjects[i].SetActive(photonView.IsMine);
        }
    }

    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);
            stream.SendNext(r.velocity);
            stream.SendNext(r.angularVelocity);

            wheelRotations = new Quaternion[wheels.Length];
            for(int i = 0; i < wheels.Length; i++)
            {
                wheelRotations[i] = wheels[i].localRotation;
            }
            stream.SendNext(wheelRotations);
        }
        else
        {
            // Network player, receive data
            latestPos = (Vector3)stream.ReceiveNext();
            latestRot = (Quaternion)stream.ReceiveNext();
            latestVelocity = (Vector3)stream.ReceiveNext();
            latestAngularVelocity = (Vector3)stream.ReceiveNext();
            wheelRotations = (Quaternion[])stream.ReceiveNext();

            // Lag compensation
            currentTime = 0.0f;
            lastPacketTime = currentPacketTime;
            currentPacketTime = info.SentServerTime;
            positionAtLastPacket = transform.position;
            rotationAtLastPacket = transform.rotation;
            velocityAtLastPacket = r.velocity;
            angularVelocityAtLastPacket = r.angularVelocity;
        }
    }

    // Update is called once per frame
    void Update()
    {
        if (!photonView.IsMine)
        {
            // Lag compensation
            double timeToReachGoal = currentPacketTime - lastPacketTime;
            currentTime += Time.deltaTime;

            // Update car position and velocity
            transform.position = Vector3.Lerp(positionAtLastPacket, latestPos, (float)(currentTime / timeToReachGoal));
            transform.rotation = Quaternion.Lerp(rotationAtLastPacket, latestRot, (float)(currentTime / timeToReachGoal));
            r.velocity = Vector3.Lerp(velocityAtLastPacket, latestVelocity, (float)(currentTime / timeToReachGoal));
            r.angularVelocity = Vector3.Lerp(angularVelocityAtLastPacket, latestAngularVelocity, (float)(currentTime / timeToReachGoal));

            //Apply wheel rotation
            if(wheelRotations.Length == wheels.Length)
            {
                for (int i = 0; i < wheelRotations.Length; i++)
                {
                    wheels[i].localRotation = Quaternion.Lerp(wheels[i].localRotation, wheelRotations[i], Time.deltaTime * 6.5f);
                }
            }
        }
    }
}
  • Allega lo script PUN2_CarSync all'oggetto "CarRoot"
  • Collega il componente PhotonView all'oggetto "CarRoot"
  • In PUN2_CarSync assegnare lo script SC_CarController all'array Local Scripts
  • In PUN2_CarSync assegna la telecamera all'array Oggetti locali
  • Assegna oggetti WheelTransform all'array Wheels
  • Infine, assegna lo script PUN2_CarSync all'array Observed Components in Photon View
  • Salva l'oggetto "CarRoot" in Prefab e posizionalo in una cartella chiamata Risorse (è necessario per poter generare oggetti sulla rete)

3. Creazione di un livello di gioco

Il livello di gioco è una scena che viene caricata dopo essere entrati nella stanza, dove si svolge tutta l'azione.

  • Crea una nuova scena e chiamala "Playground" (o se vuoi mantenere un nome diverso, assicurati di cambiare il nome in questa riga PhotonNetwork.LoadLevel("Playground"); in PUN2_GameLobby.cs).

Nel mio caso, utilizzerò una scena semplice con un aereo e alcuni cubi:

  • Crea un nuovo script e chiamalo PUN2_RoomController (questo script gestirà la logica all'interno della Room, come generare i giocatori, mostrare l'elenco dei giocatori, ecc.) quindi incollare il codice seguente al suo interno:

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[] spawnPoints;

    // 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, spawnPoints[Random.Range(0, spawnPoints.Length - 1)].position, spawnPoints[Random.Range(0, spawnPoints.Length - 1)].rotation, 0);
    }

    void OnGUI()
    {
        if (PhotonNetwork.CurrentRoom == null)
            return;

        //Leave this Room
        if (GUI.Button(new Rect(5, 5, 125, 25), "Leave Room"))
        {
            PhotonNetwork.LeaveRoom();
        }

        //Show the Room name
        GUI.Label(new Rect(135, 5, 200, 25), PhotonNetwork.CurrentRoom.Name);

        //Show the list of the players connected to this Room
        for (int i = 0; i < PhotonNetwork.PlayerList.Length; i++)
        {
            //Show if this player is a Master Client. There can only be one Master Client per Room so use this to define the authoritative logic etc.)
            string isMasterClient = (PhotonNetwork.PlayerList[i].IsMasterClient ? ": MasterClient" : "");
            GUI.Label(new Rect(5, 35 + 30 * i, 200, 25), PhotonNetwork.PlayerList[i].NickName + isMasterClient);
        }
    }

    public override void OnLeftRoom()
    {
        //We have left the Room, return back to the GameLobby
        UnityEngine.SceneManagement.SceneManager.LoadScene("GameLobby");
    }
}
  • Crea un nuovo GameObject nella scena "Playground" e chiamalo "_RoomController"
  • Allega uno script PUN2_RoomController all'oggetto _RoomController
  • Assegna un prefabbricato per auto e uno SpawnPoint, quindi salva la scena

  • Aggiungi sia le scene della GameLobby che quelle del parco giochi alle impostazioni di costruzione:

4. Realizzazione di una build di prova

Ora è il momento di creare una build e testarla:

Sharp Coder Lettore video

Tutto funziona come previsto!

Articoli suggeriti
Crea un gioco multiplayer in Unity usando PUN 2
Sincronizza i corpi rigidi sulla rete usando PUN 2
Creazione di giochi in rete multiplayer in Unity
Unity Aggiunta della chat multigiocatore alle stanze di PUN 2
Photon Network (Classic) Guida per principianti
Compressione dei dati multigiocatore e manipolazione dei bit
Tutorial sulla classifica online di Unity