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
- Setelah paket diimpor Anda perlu membuat ID Aplikasi Foton, ini dilakukan di situs web mereka: https://www.photonengine.com/
- Buat akun baru (atau masuk ke akun Anda yang sudah ada)
- Buka halaman Aplikasi dengan mengklik ikon profil lalu "Your Applications" atau ikuti tautan ini: https://dashboard.photonengine.com/en-US/PublicCloud
- Pada halaman Aplikasi klik "Create new app"
- 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:
Semuanya berfungsi seperti yang diharapkan!