Maak een multiplayer-autospel met PUN 2

Een multiplayergame maken in Unity is een complexe taak, maar gelukkig vereenvoudigen verschillende oplossingen het ontwikkelingsproces.

Eén zo'n oplossing is het Photon Network. Concreet zorgt de nieuwste release van hun API genaamd PUN 2 voor serverhosting en laat je de vrijheid om een ​​multiplayer-game te maken zoals jij dat wilt.

In deze tutorial laat ik zien hoe je een eenvoudig autospel kunt maken met natuurkundige synchronisatie met behulp van PUN 2.

Unity versie gebruikt in deze tutorial: Unity 2018.3.0f2 (64-bit)

Deel 1: PUN 2 instellen

De eerste stap is het downloaden van een PUN 2-pakket van de Asset Store. Het bevat alle scripts en bestanden die nodig zijn voor multiplayer-integratie.

  • Open uw Unity-project en ga vervolgens naar Asset Store: (Venster -> Algemeen -> AssetStore) of druk op Ctrl+9
  • Zoek naar "PUN 2- Free" en klik vervolgens op het eerste resultaat of klik hier
  • Importeer het PUN 2-pakket nadat het downloaden is voltooid

  • Nadat het pakket is geïmporteerd, moet u een Photon App ID aanmaken, dit doet u op hun website: https://www.photonengine.com/
  • Maak een nieuw account aan (of log in op uw bestaande account)
  • Ga naar de pagina Toepassingen door op het profielpictogram en vervolgens op "Your Applications" te klikken of volg deze link: https://dashboard.photonengine.com/en-US/PublicCloud
  • Klik op de pagina Toepassingen "Create new app"

  • Op de aanmaakpagina selecteert u bij Fotontype "Photon Realtime" en bij Naam typt u een willekeurige naam en klikt u vervolgens op "Create"

Zoals u kunt zien, is de applicatie standaard ingesteld op het gratis abonnement. U kunt hier meer lezen over prijsplannen

  • Zodra de applicatie is gemaakt, kopieert u de app-ID onder de app-naam

  • Ga terug naar uw Unity-project en ga vervolgens naar Venster -> Photon Unity Networking -> PUN Wizard
  • Klik in de PUN-wizard op "Setup Project", plak uw app-ID en klik vervolgens op "Setup Project"

De PUN 2 is nu klaar!

Deel 2: Een multiplayer-autospel maken

1. Een lobby opzetten

Laten we beginnen met het maken van een lobbyscène die lobbylogica bevat (door bestaande kamers bladeren, nieuwe kamers maken, enz.):

  • Maak een nieuwe scène en roep deze aan "GameLobby"
  • Maak in de "GameLobby"-scène een nieuw GameObject en roep het aan "_GameLobby"
  • Maak een nieuw C#-script en noem het "PUN2_GameLobby" en koppel het vervolgens aan het "_GameLobby"-object
  • Plak de onderstaande code in het "PUN2_GameLobby"-script

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. Een prefab auto maken

De Car prefab zal gebruik maken van een eenvoudige fysica-controller.

  • Maak een nieuw GameObject en noem het "CarRoot"
  • Maak een nieuwe kubus en verplaats deze binnen het "CarRoot"-object en schaal deze vervolgens omhoog langs de Z- en X-as

  • Maak een nieuw GameObject en noem het "wfl" (afkorting voor Wheel Front Left)
  • Voeg de component Wheel Collider toe aan het "wfl"-object en stel de waarden uit de onderstaande afbeelding in:

  • Maak een nieuw GameObject, hernoem het naar "WheelTransform" en verplaats het vervolgens binnen het "wfl"-object
  • Maak een nieuwe cilinder, verplaats deze binnen het "WheelTransform"-object en draai en schaal deze vervolgens omlaag totdat deze overeenkomt met de afmetingen van de Wheel Collider. In mijn geval is de schaal (1, 0,17, 1)

  • Dupliceer ten slotte het "wfl"-object 3 keer voor de rest van de wielen en hernoem elk object naar respectievelijk "wfr" (wiel rechtsvoor), "wrr" (wiel rechtsachter) en "wrl" (wiel linksachter).

  • Maak een nieuw script, noem het "SC_CarController" en plak de onderstaande code erin:

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;
        }
    }
}
  • Koppel het SC_CarController-script aan het "CarRoot"-object
  • Bevestig de Rigidbody-component aan het "CarRoot"-object en verander de massa in 1000
  • Wijs de wielvariabelen toe in SC_CarController (Wheel collider voor de eerste 4 variabelen en WheelTransform voor de rest van de 4)

  • Maak voor de Center of Mass-variabele een nieuw GameObject, noem het "CenterOfMass" en verplaats het binnen het "CarRoot"-object
  • Plaats het "CenterOfMass"-object in het midden en iets naar beneden, zoals dit:

  • Plaats ten slotte voor testdoeleinden de hoofdcamera in het "CarRoot"-object en richt deze op de auto:

  • Maak een nieuw script, noem het "PUN2_CarSync" en plak de onderstaande code erin:

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);
                }
            }
        }
    }
}
  • Koppel het PUN2_CarSync-script aan het "CarRoot"-object
  • Bevestig de PhotonView-component aan het "CarRoot"-object
  • Wijs in PUN2_CarSync het SC_CarController-script toe aan de Local Scripts-array
  • Wijs in PUN2_CarSync de camera toe aan de array Lokale objecten
  • Wijs WheelTransform-objecten toe aan de Wheels-array
  • Wijs ten slotte het PUN2_CarSync-script toe aan de array Observed Components in Photon View
  • Sla het "CarRoot"-object op in Prefab en plaats het in een map met de naam Resources (dit is nodig om objecten via het netwerk te kunnen spawnen)

3. Een spelniveau creëren

Spelniveau is een scène die wordt geladen nadat je naar de kamer bent gegaan, waar alle actie plaatsvindt.

  • Maak een nieuwe scène en noem deze "Playground" (of als je een andere naam wilt behouden, zorg er dan voor dat je de naam wijzigt in deze regel PhotonNetwork.LoadLevel("Playground"); in de PUN2_GameLobby.cs).

In mijn geval gebruik ik een eenvoudige scène met een vlak en enkele kubussen:

  • Maak een nieuw script en noem het PUN2_RoomController (dit script verwerkt de logica in de kamer, zoals het spawnen van de spelers, het tonen van de spelerslijst, enz.) en plak vervolgens de onderstaande code erin:

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");
    }
}
  • Maak een nieuw GameObject in de "Playground"-scène en noem het "_RoomController"
  • Voeg een PUN2_RoomController-script toe aan het _RoomController-object
  • Wijs een auto-prefab en spawnpunten toe en sla de scène op

  • Voeg zowel GameLobby- als Speeltuinscènes toe aan de Build-instellingen:

4. Een testbuild maken

Nu is het tijd om een ​​build te maken en deze te testen:

Sharp Coder Video speler

Alles werkt zoals verwacht!

Voorgestelde artikelen
Maak een multiplayergame in Unity met PUN 2
Synchroniseer starre lichamen via netwerk met PUN 2
Unity voegt multiplayer-chat toe aan de PUN 2-kamers
Unity Login-systeem met PHP en MySQL
PUN 2 vertragingscompensatie
Photon Network (klassiek) beginnershandleiding
Samen multiplayer-netwerkgames bouwen