PUN 2 ile Çok Oyunculu Araba Oyunu Yapın

Unity'te çok oyunculu bir oyun yapmak karmaşık bir iştir, ancak neyse ki çeşitli çözümler geliştirme sürecini basitleştirir.

Böyle bir çözüm Foton Ağı'tür. Özellikle, API'lerinin PUN 2 adı verilen en son sürümü, sunucu barındırma işini üstleniyor ve size istediğiniz gibi çok oyunculu bir oyun yapma özgürlüğü veriyor.

Bu derste PUN 2 kullanarak fizik senkronizasyonu ile basit bir araba oyununun nasıl oluşturulacağını göstereceğim.

Unity Bu eğitimde kullanılan sürüm: Unity 2018.3.0f2 (64-bit)

Bölüm 1: PUN 2'yi Kurma

İlk adım Asset Store'den PUN 2 paketini indirmektir. Çok oyunculu entegrasyon için gerekli tüm komut dosyalarını ve dosyaları içerir.

  • Unity projenizi açın, ardından Asset Store'e gidin (Pencere -> Genel -> AssetStore) veya Ctrl+9 tuşlarına basın
  • "PUN 2- Free"'yı arayın ve ardından ilk sonuca tıklayın veya burayı tıklayın
  • İndirme işlemi tamamlandıktan sonra PUN 2 paketini içe aktarın

  • Paket içe aktarıldıktan sonra bir Photon Uygulama Kimliği oluşturmanız gerekir; bu, web sitesinde yapılır: https://www.photonengine.com/
  • Yeni bir hesap oluşturun (veya mevcut hesabınıza giriş yapın)
  • Profil simgesine ve ardından "Your Applications"'e tıklayarak Uygulamalar sayfasına gidin veya şu bağlantıyı izleyin: https://dashboard.photonengine.com/en-US/PublicCloud
  • Uygulamalar sayfasında tıklayın "Create new app"

  • Oluşturma sayfasında, Foton Türü için "Photon Realtime"'ü seçin ve Ad için herhangi bir adı yazın ve ardından simgesine tıklayın. "Create"

Gördüğünüz gibi Uygulama varsayılan olarak Ücretsiz planı kullanıyor. Fiyatlandırma Planları hakkında daha fazla bilgiyi burada bulabilirsiniz

  • Uygulama oluşturulduktan sonra Uygulama adının altında bulunan Uygulama Kimliğini kopyalayın

  • Unity projenize geri dönün ve ardından Pencere -> Photon Unity Ağ İletişimi -> PUN Sihirbazı'na gidin
  • PUN Sihirbazı'nda "Setup Project"'a tıklayın, Uygulama Kimliğinizi yapıştırın ve ardından tıklayın "Setup Project"

PUN 2 artık hazır!

Bölüm 2: Çok Oyunculu Araba Oyunu Yaratmak

1. Lobi Kurma

Lobi mantığını içerecek bir Lobi sahnesi oluşturarak başlayalım (Mevcut odalara göz atmak, yeni odalar oluşturmak vb.):

  • Yeni bir Sahne oluşturun ve onu çağırın "GameLobby"
  • "GameLobby" sahnesinde yeni bir GameObject oluşturun ve onu çağırın "_GameLobby"
  • yeni bir C# betiği oluşturun ve onu "PUN2_GameLobby" olarak adlandırın, ardından onu "_GameLobby" nesnesine ekleyin
  • "PUN2_GameLobby" betiğinin içine aşağıdaki kodu yapıştırın

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. Araba Prefabrik Oluşturma

Araba prefabrik basit bir fizik kontrol cihazı kullanacak.

  • Yeni bir GameObject oluşturun ve onu çağırın "CarRoot"
  • Yeni bir Küp oluşturun ve onu "CarRoot" nesnesinin içine taşıyın, ardından Z ve X ekseni boyunca ölçeklendirin

  • Yeni bir GameObject oluşturun, onu "WheelTransform" olarak yeniden adlandırın ve ardından onu "wfl" nesnesinin içine taşıyın
  • Yeni bir Silindir oluşturun, onu "WheelTransform" nesnesinin içine taşıyın, ardından döndürün ve Wheel Collider boyutlarıyla eşleşene kadar küçültün. Benim durumumda ölçek (1, 0,17, 1)

  • Son olarak, "wfl" nesnesini tekerleklerin geri kalanı için 3 kez kopyalayın ve her nesneyi sırasıyla "wfr" (Tekerlek Ön Sağ), "wrr" (Tekerlek Arka Sağ) ve "wrl" (Tekerlek Arka Sol) olarak yeniden adlandırın.

  • Yeni bir komut dosyası oluşturun, onu "SC_CarController" olarak adlandırın ve içine aşağıdaki kodu yapıştırın:

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;
        }
    }
}
  • SC_CarController komut dosyasını "CarRoot" nesnesine ekleyin
  • Rigidbody bileşenini "CarRoot" nesnesine ekleyin ve kütlesini 1000 olarak değiştirin
  • SC_CarController'da tekerlek değişkenlerini atayın (İlk 4 değişken için Wheel collider ve geri kalan 4 değişken için WheelTransform)

  • Kütle Merkezi değişkeni için yeni bir GameObject oluşturun, onu "CenterOfMass" olarak adlandırın ve "CarRoot" nesnesinin içine taşıyın
  • "CenterOfMass" nesnesini şu şekilde ortaya ve biraz aşağıya yerleştirin:

  • Son olarak test amacıyla Ana Kamerayı "CarRoot" nesnesinin içine taşıyın ve arabaya doğrultun:

  • Yeni bir komut dosyası oluşturun, onu "PUN2_CarSync" olarak adlandırın ve içine aşağıdaki kodu yapıştırın:

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);
                }
            }
        }
    }
}
  • PUN2_CarSync komut dosyasını "CarRoot" nesnesine ekleyin
  • PhotonView bileşenini "CarRoot" nesnesine ekleyin
  • PUN2_CarSync'te SC_CarController komut dosyasını Yerel Komut Dosyaları dizisine atayın
  • PUN2_CarSync'te Kamerayı Yerel Nesneler dizisine atayın
  • WheelTransform nesnelerini Wheels dizisine atama
  • Son olarak, PUN2_CarSync komut dosyasını Photon Görünümünde Gözlenen Bileşenler dizisine atayın.
  • "CarRoot" nesnesini Prefab'a kaydedin ve Kaynaklar adlı bir klasöre yerleştirin (nesnelerin ağ üzerinden oluşturulabilmesi için bu gereklidir)

3. Oyun Seviyesi Oluşturma

Oyun Seviyesi, tüm aksiyonun gerçekleştiği Odaya girdikten sonra yüklenen bir Sahnedir.

  • Yeni bir Scene oluşturun ve onu "Playground" olarak adlandırın (Veya farklı bir ad tutmak istiyorsanız, adı PUN2_GameLobby.cs'deki PhotonNetwork.LoadLevel("Playground"); satırında değiştirdiğinizden emin olun).

Benim durumumda, bir düzlem ve birkaç küp içeren basit bir sahne kullanacağım:

  • Yeni bir komut dosyası oluşturun ve onu PUN2_RoomController olarak adlandırın (Bu komut dosyası, Oda içindeki mantığı yönetecektir, örneğin oyuncuları oluşturmak, oyuncu listesini göstermek vb.) ve ardından aşağıdaki kodu içine yapıştırın:

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");
    }
}
  • "Playground" sahnesinde yeni bir GameObject oluşturun ve onu çağırın "_RoomController"
  • _RoomController Nesnesine bir PUN2_RoomController komut dosyası ekleyin
  • Bir Araba prefabrikasyonu ve SpawnPoints atayın ve Sahneyi kaydedin

  • Yapı ayarlarına hem GameLobby hem de Oyun Alanı Sahnelerini ekleyin:

4. Test Yapısı Oluşturma

Şimdi bir yapı oluşturup test etme zamanı:

Sharp Coder Video oynatıcı

Her şey beklendiği gibi çalışıyor!

Önerilen Makaleler
PUN 2'yi kullanarak Unity'de Çok Oyunculu Bir Oyun Yapın
Unity, PUN 2 Odalarına Çok Oyunculu Sohbet Ekliyor
Çok Oyunculu Veri Sıkıştırma ve Bit İşleme
PUN 2'yi Kullanarak Sert Cisimleri Ağ Üzerinden Senkronize Etme
Photon Network (Klasik) Başlangıç ​​Kılavuzu
Unity'de Çok Oyunculu Ağ Bağlantılı Oyunlar Oluşturma
PHP ve MySQL ile Unity Giriş Sistemi