Buat Game Mobil Multi Pemain dengan PUN 2

Membuat game multipemain di Unity adalah tugas yang rumit, namun untungnya ada beberapa solusi yang menyederhanakan proses pengembangan.

Salah satu solusinya adalah Jaringan Foton. Secara khusus, rilis terbaru API mereka yang disebut PUN 2 menangani hosting server dan membuat Anda bebas membuat game multipemain sesuai keinginan Anda.

Dalam tutorial ini saya akan menunjukkan cara membuat game mobil sederhana dengan sinkronisasi fisika menggunakan PUN 2.

Unity versi yang digunakan dalam tutorial ini: Unity 2018.3.0f2 (64-bit)

Bagian 1: Menyiapkan PUN 2

Langkah pertama adalah mendownload paket PUN 2 dari Asset Store. Ini berisi semua skrip dan file yang diperlukan untuk integrasi multipemain.

  • Buka proyek Unity Anda lalu buka Asset Store: (Window -> General -> AssetStore) atau tekan Ctrl+9
  • Cari "PUN 2- Free" lalu klik hasil pertama atau klik di sini
  • Impor paket PUN 2 setelah Download selesai

  • Pada halaman pembuatan, untuk Photon Type pilih "Photon Realtime" dan untuk Name, ketikkan nama apa saja lalu klik "Create"

Seperti yang Anda lihat, Aplikasi defaultnya adalah paket Gratis. Anda dapat membaca lebih lanjut tentang Paket Harga di sini

  • Setelah Aplikasi dibuat, salin ID Aplikasi yang terletak di bawah nama Aplikasi

  • Kembali ke proyek Unity Anda lalu buka Window -> Photon Unity Networking -> PUN Wizard
  • Di PUN Wizard klik "Setup Project", paste App ID Anda lalu klik "Setup Project"

PUN 2 sekarang siap!

Bagian 2: Membuat Game Mobil Multi Pemain

1. Menyiapkan Lobi

Mari kita mulai dengan membuat adegan Lobi yang berisi logika Lobi (Menelusuri ruangan yang ada, membuat ruangan baru, dll.):

  • Buat Adegan baru dan beri nama "GameLobby"
  • Dalam adegan "GameLobby" buat GameObject baru dan beri nama "_GameLobby"
  • Buat skrip C# baru dan beri nama "PUN2_GameLobby" lalu tempelkan ke objek "_GameLobby"
  • Tempelkan kode di bawah ini ke dalam skrip "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. Membuat Prefab Mobil

Pabrikan Mobil akan menggunakan pengontrol fisika sederhana.

  • Buat GameObject baru dan beri nama "CarRoot"
  • Buat Kubus baru dan pindahkan ke dalam objek "CarRoot" lalu perbesar skalanya sepanjang sumbu Z dan X

  • Buat GameObject baru dan beri nama "wfl" (singkatan dari Wheel Front Left)
  • Tambahkan komponen Wheel Collider ke objek "wfl" dan atur nilainya dari gambar di bawah:

  • Buat GameObject baru, ganti namanya menjadi "WheelTransform" lalu pindahkan ke dalam objek "wfl"
  • Buat Silinder baru, pindahkan ke dalam objek "WheelTransform" lalu putar dan turunkan skalanya hingga sesuai dengan dimensi Wheel Collider. Dalam kasus saya, skalanya adalah (1, 0,17, 1)

  • Terakhir, duplikat objek "wfl" sebanyak 3 kali untuk sisa roda dan ganti nama masing-masing objek menjadi "wfr" (Roda Depan Kanan), "wrr" (Roda Belakang Kanan), dan "wrl" (Roda Belakang Kiri) masing-masing

  • Buat script baru, beri nama "SC_CarController" lalu pastekan kode di bawah ini ke dalamnya:

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;
        }
    }
}
  • Lampirkan skrip SC_CarController ke objek "CarRoot"
  • Pasang komponen Rigidbody ke objek "CarRoot" dan ubah massanya menjadi 1000
  • Tetapkan variabel roda di SC_CarController (Wheel collider untuk 4 variabel pertama dan WheelTransform untuk 4 variabel lainnya)

  • Untuk variabel Center of Mass buat GameObject baru, beri nama "CenterOfMass" dan pindahkan ke dalam objek "CarRoot"
  • Tempatkan objek "CenterOfMass" di tengah dan sedikit ke bawah, seperti ini:

  • Terakhir, untuk tujuan pengujian, pindahkan Kamera Utama ke dalam objek "CarRoot" dan arahkan ke mobil:

  • Buat script baru, beri nama "PUN2_CarSync" lalu paste kode di bawah ini ke dalamnya:

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);
                }
            }
        }
    }
}
  • Lampirkan skrip PUN2_CarSync ke objek "CarRoot"
  • Lampirkan komponen PhotonView ke objek "CarRoot"
  • Di PUN2_CarSync tetapkan skrip SC_CarController ke array Skrip Lokal
  • Di PUN2_CarSync tetapkan Kamera ke array Objek Lokal
  • Tetapkan objek WheelTransform ke array Wheels
  • Terakhir, Tetapkan skrip PUN2_CarSync ke array Komponen yang Diamati dalam Tampilan Foton
  • Simpan objek "CarRoot" ke Prefab dan letakkan di folder bernama Resources (ini diperlukan agar dapat menelurkan objek melalui jaringan)

3. Membuat Level Game

Level Game adalah Adegan yang dimuat setelah bergabung dengan Ruangan, tempat semua aksi terjadi.

  • Buat Scene baru dan beri nama "Playground" (Atau jika Anda ingin tetap menggunakan nama lain, pastikan untuk mengganti nama di baris ini PhotonNetwork.LoadLevel("Playground"); di PUN2_GameLobby.cs).

Dalam kasus saya, saya akan menggunakan adegan sederhana dengan sebuah bidang dan beberapa kubus:

  • Buat skrip baru dan beri nama PUN2_RoomController (Skrip ini akan menangani logika di dalam Ruangan, seperti memunculkan pemain, menampilkan daftar pemain, dll.) lalu tempelkan kode di bawah ini ke dalamnya:

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");
    }
}
  • Buat GameObject baru di adegan "Playground" dan beri nama "_RoomController"
  • Lampirkan skrip PUN2_RoomController ke Objek _RoomController
  • Tetapkan cetakan Mobil dan SpawnPoints lalu simpan Adegan

  • Tambahkan GameLobby dan Playground Scenes ke pengaturan Build:

4. Membuat Uji Bangun

Sekarang saatnya membuat build dan mengujinya:

Sharp Coder Pemutar video

Semuanya berfungsi seperti yang diharapkan!

Artikel yang Disarankan
Buat Game Multiplayer di Unity menggunakan PUN 2
Sinkronkan Rigidbodies Melalui Jaringan Menggunakan PUN 2
Membangun Game Jaringan Multipemain dalam Unity
Kompresi Data Multi Pemain dan Manipulasi Bit
Unity Menambahkan Obrolan Multipemain ke Ruang PUN 2
Panduan Pemula Jaringan Foton (Klasik).
Tutorial Papan Peringkat Online Unity