Creați un joc cu mașini multiplayer cu PUN 2

Realizarea unui joc multiplayer în Unity este o sarcină complexă, dar, din fericire, mai multe soluții simplifică procesul de dezvoltare.

O astfel de soluție este Photon Network. Mai exact, cea mai recentă versiune a API-ului lor numită PUN 2 se ocupă de găzduirea serverului și vă lasă liber să creați un joc multiplayer așa cum doriți.

În acest tutorial, voi arăta cum să creați un joc simplu de mașini cu sincronizare fizică folosind PUN 2.

Unity versiune folosită în acest tutorial: Unity 2018.3.0f2 (64 de biți)

Partea 1: Configurarea PUN 2

Primul pas este descărcarea unui pachet PUN 2 de pe Asset Store. Conține toate scripturile și fișierele necesare pentru integrarea multiplayer.

  • Deschideți proiectul Unity, apoi accesați Asset Store: (Fereastra -> General -> AssetStore) sau apăsați Ctrl+9
  • Căutați "PUN 2- Free" apoi faceți clic pe primul rezultat sau faceți clic aici
  • Importați pachetul PUN 2 după ce descărcarea este terminată

  • După ce pachetul este importat, trebuie să creați un ID de aplicație Photon, acest lucru se face pe site-ul lor: https://www.photonengine.com/
  • Creați un cont nou (sau conectați-vă la contul dvs. existent)
  • Accesați pagina Aplicații făcând clic pe pictograma profilului, apoi pe "Your Applications" sau urmați acest link: https://dashboard.photonengine.com/en-US/PublicCloud
  • În pagina Aplicații, faceți clic "Create new app"

  • Pe pagina de creare, pentru Photon Type selectați "Photon Realtime" și pentru Name, introduceți orice nume, apoi faceți clic "Create"

După cum puteți vedea, aplicația este implicită la planul gratuit. Puteți citi mai multe despre planurile de prețuri aici

  • Odată ce aplicația este creată, copiați ID-ul aplicației aflat sub numele aplicației

  • Reveniți la proiectul dvs. Unity, apoi accesați Window -> Photon Unity Networking -> PUN Wizard
  • În PUN Wizard, faceți clic pe "Setup Project", inserați ID-ul aplicației, apoi faceți clic "Setup Project"

PUN 2 este acum gata!

Partea 2: Crearea unui joc cu mașini multiplayer

1. Crearea unui hol

Să începem prin a crea o scenă Lobby care va conține logica Lobby-ului (Rasfoirea camerelor existente, crearea de noi camere etc.):

  • Creați o scenă nouă și apelați-o "GameLobby"
  • În scena "GameLobby" creați un nou GameObject și apelați-l "_GameLobby"
  • Creați un nou script C# și numiți-l "PUN2_GameLobby" apoi atașați-l la obiectul "_GameLobby"
  • Lipiți codul de mai jos în scriptul "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. Crearea unui prefabricat auto

Prefabricatul mașinii va folosi un controler fizic simplu.

  • Creați un nou GameObject și apelați-l "CarRoot"
  • Creați un nou Cub și mutați-l în interiorul obiectului "CarRoot", apoi scalați-l de-a lungul axei Z și X

  • Creați un nou GameObject și denumiți-l "wfl" (abreviere pentru Wheel Front Left)
  • Adăugați componenta Wheel Collider la obiectul "wfl" și setați valorile din imaginea de mai jos:

  • Creați un nou GameObject, redenumiți-l în "WheelTransform" apoi mutați-l în interiorul obiectului "wfl"
  • Creați un nou Cilindru, mutați-l în interiorul obiectului "WheelTransform", apoi rotiți-l și scalați-l până când se potrivește cu dimensiunile Wheel Collider. În cazul meu, scara este (1, 0,17, 1)

  • În cele din urmă, duplicați obiectul "wfl" de 3 ori pentru restul roților și redenumiți fiecare obiect în "wfr" (Roată față dreapta), "wrr" (Roată spate dreapta), respectiv "wrl" (Roată spate stânga), respectiv

  • Creați un nou script, numiți-l "SC_CarController" apoi inserați codul de mai jos în el:

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;
        }
    }
}
  • Atașați scriptul SC_CarController la obiectul "CarRoot"
  • Atașați componenta Rigidbody la obiectul "CarRoot" și modificați-i masa la 1000
  • Atribuiți variabilele roții în SC_CarController (Clinizorul de roți pentru primele 4 variabile și WheelTransform pentru restul celor 4)

  • Pentru variabila Center of Mass, creați un nou GameObject, numiți-l "CenterOfMass" și mutați-l în interiorul obiectului "CarRoot"
  • Așezați obiectul "CenterOfMass" în mijloc și ușor în jos, astfel:

  • În cele din urmă, în scopuri de testare, mutați camera principală în interiorul obiectului "CarRoot" și îndreptați-o către mașină:

  • Creați un nou script, numiți-l "PUN2_CarSync" apoi inserați codul de mai jos în el:

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);
                }
            }
        }
    }
}
  • Atașați scriptul PUN2_CarSync la obiectul "CarRoot"
  • Atașați componenta PhotonView la obiectul "CarRoot"
  • În PUN2_CarSync, atribuiți script-ul SC_CarController matricei Local Scripts
  • În PUN2_CarSync, atribuiți Camera matricei de obiecte locale
  • Atribuiți obiecte WheelTransform matricei Wheels
  • În cele din urmă, atribuiți scriptul PUN2_CarSync matricei Componente observate din Photon View
  • Salvați obiectul "CarRoot" în Prefab și plasați-l într-un folder numit Resurse (acest lucru este necesar pentru a putea genera obiecte în rețea)

3. Crearea unui nivel de joc

Nivelul jocului este o scenă care se încarcă după alăturarea camerei, unde se întâmplă toată acțiunea.

  • Creați o scenă nouă și numiți-o "Playground" (Sau dacă doriți să păstrați un alt nume, asigurați-vă că schimbați numele în această linie PhotonNetwork.LoadLevel ("Playground"); la PUN2_GameLobby.cs).

În cazul meu, voi folosi o scenă simplă cu un avion și câteva cuburi:

  • Creați un nou script și numiți-l PUN2_RoomController (Acest script se va ocupa de logica din cameră, cum ar fi generarea jucătorilor, afișarea listei de jucători etc.), apoi inserați codul de mai jos în interiorul acestuia:

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ți un nou GameObject în scena "Playground" și apelați-l "_RoomController"
  • Atașați un script PUN2_RoomController la obiectul _RoomController
  • Atribuiți o mașină prefabricată și un SpawnPoints, apoi salvați scena

  • Adăugați atât Scene GameLobby, cât și Playground la setările de construcție:

4. Efectuarea unui test Build

Acum este timpul să faceți o construcție și să o testați:

Sharp Coder Video player

Totul funcționează conform așteptărilor!

Articole sugerate
Faceți un joc multiplayer în Unity folosind PUN 2
Unity Adăugând chat multiplayer în camerele PUN 2
Sincronizați corpuri rigide prin rețea folosind PUN 2
Crearea de jocuri multiplayer în rețea în Unity
Photon Network (Clasic) Ghid pentru începători
Comprimarea datelor multiplayer și manipularea biților
Tutorial online pentru clasament pentru Unity