צור משחק מכוניות מרובה משתתפים עם PUN 2
יצירת משחק מרובה משתתפים ב-Unity היא משימה מורכבת, אבל למרבה המזל כמה פתרונות מפשטים את תהליך הפיתוח.
פתרון אחד כזה הוא Photon Network. באופן ספציפי, המהדורה האחרונה של ה-API שלהם בשם PUN 2 דואגת לאירוח שרתים ומשאירה אותך חופשי ליצור משחק מרובה משתתפים כמו שאתה רוצה.
במדריך זה, אני אראה כיצד ליצור משחק רכב פשוט עם סנכרון פיזיקה באמצעות PUN 2.
Unity גרסה בשימוש במדריך זה: Unity 2018.3.0f2 (64 סיביות)
חלק 1: הגדרת PUN 2
השלב הראשון הוא הורדת חבילת PUN 2 מה-Asset Store. הוא מכיל את כל הסקריפטים והקבצים הדרושים לשילוב מרובה משתתפים.
- פתח את פרוייקט Unity שלך ואז עבור אל Asset Store: (חלון -> כללי -> AssetStore) או הקש Ctrl+9
- חפש את "PUN 2- Free" ואז לחץ על התוצאה הראשונה או לחץ כאן
- ייבא את חבילת PUN 2 לאחר סיום ההורדה
- לאחר ייבוא החבילה עליך ליצור מזהה אפליקציית Photon, זה נעשה באתר שלהם: https://www.photonengine.com/
- צור חשבון חדש (או היכנס לחשבון הקיים שלך)
- עבור לדף היישומים על ידי לחיצה על סמל הפרופיל ולאחר מכן על "Your Applications" או עקוב אחר הקישור הזה: https://dashboard.photonengine.com/en-US/PublicCloud
- בדף יישומים לחץ "Create new app"
- בדף היצירה, עבור Photon Type בחר "Photon Realtime" ועבור שם, הקלד כל שם ולאחר מכן לחץ "Create"
כפי שאתה יכול לראות, היישום כברירת מחדל לתוכנית החינמית. תוכל לקרוא עוד על תוכניות תמחור כאן
- לאחר יצירת האפליקציה, העתק את מזהה האפליקציה שנמצא מתחת לשם האפליקציה
- חזור לפרויקט Unity שלך ואז עבור אל חלון -> Photon Unity Networking -> אשף PUN
- ב-PUN Wizard לחץ על "Setup Project", הדבק את מזהה האפליקציה שלך ואז לחץ "Setup Project"
ה-PUN 2 מוכן כעת!
חלק 2: יצירת משחק מכוניות מרובה משתתפים
1. הקמת לובי
נתחיל ביצירת סצנת לובי שתכיל את היגיון הלובי (גלישה בחדרים קיימים, יצירת חדרים חדשים וכו'):
- צור סצנה חדשה וקרא לה "GameLobby"
- בסצנה "GameLobby" צור GameObject חדש וקרא לו "_GameLobby"
- צור סקריפט C# חדש וקרא לו "PUN2_GameLobby" ואז צרף אותו לאובייקט "_GameLobby"
- הדבק את הקוד למטה בתוך הסקריפט "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. יצירת רכב טרומי
המכונית הטרומית תשתמש בבקר פיזיקה פשוט.
- צור GameObject חדש וקרא לו "CarRoot"
- צור קובייה חדשה והזז אותה בתוך האובייקט "CarRoot" ואז הגדל אותה לאורך ציר Z ו-X
- צור GameObject חדש ושם לו "wfl" (ראשי תיבות של Wheel Front Left)
- הוסף את הרכיב Wheel Collider לאובייקט "wfl" וקבע את הערכים מהתמונה למטה:
- צור GameObject חדש, שנה את שמו ל-"WheelTransform" ואז העבר אותו לתוך האובייקט "wfl"
- צרו צילינדר חדש, הזיזו אותו בתוך האובייקט "WheelTransform" ואז סובבו והקטינו אותו עד שיתאים לממדים של ה-Wheel Collider. במקרה שלי, הסולם הוא (1, 0.17, 1)
- לבסוף, שכפלו את האובייקט "wfl" 3 פעמים עבור שאר הגלגלים ושנו את שמו של כל אובייקט ל-"wfr" (גלגל קדמי ימין), "wrr" (גלגל אחורי ימין), ו-"wrl" (גלגל אחורי שמאלי) בהתאמה
- צור סקריפט חדש, קרא לו "SC_CarController" ואז הדבק בתוכו את הקוד למטה:
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 לאובייקט "CarRoot"
- חברו את רכיב Rigidbody לאובייקט "CarRoot" ושנו את המסה שלו ל-1000
- הקצה את משתני הגלגל ב-SC_CarController (מתנגש גלגלים עבור 4 המשתנים הראשונים ו-WheelTransform עבור שאר ה-4)
- עבור המשתנה Center of Mass צור GameObject חדש, קרא לו "CenterOfMass" והזז אותו בתוך האובייקט "CarRoot"
- מקם את האובייקט "CenterOfMass" באמצע וקצת למטה, כך:
- לבסוף למטרות בדיקה הזיזו את המצלמה הראשית בתוך האובייקט "CarRoot" והפנו אותה לעבר המכונית:
- צור סקריפט חדש, קרא לו "PUN2_CarSync" ואז הדבק בתוכו את הקוד למטה:
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 לאובייקט "CarRoot"
- צרף את רכיב PhotonView לאובייקט "CarRoot"
- ב-PUN2_CarSync הקצה את הסקריפט SC_CarController למערך הסקריפטים המקומיים
- ב-PUN2_CarSync הקצה את המצלמה למערך אובייקטים מקומיים
- הקצה אובייקטים של WheelTransform למערך Wheels
- לבסוף, הקצה את הסקריפט PUN2_CarSync למערך הרכיבים שנצפו בתצוגת Photon
- שמור את האובייקט "CarRoot" ב-Prefab והצב אותו בתיקייה בשם Resources (זה נחוץ כדי להיות מסוגל להריץ אובייקטים ברשת)
3. יצירת רמת משחק
רמת משחק היא סצנה שנטענת לאחר ההצטרפות לחדר, שבה מתרחשת כל האקשן.
- צור סצנה חדשה וקרא לה "Playground" (או אם אתה רוצה לשמור שם אחר, הקפד לשנות את השם בשורה זו PhotonNetwork.LoadLevel("Playground"); ב-PUN2_GameLobby.cs).
במקרה שלי, אני אשתמש בסצנה פשוטה עם מטוס וכמה קוביות:
- צור סקריפט חדש וקרא לו PUN2_RoomController (סקריפט זה יטפל בהיגיון בתוך החדר, כמו השראת השחקנים, הצגת רשימת השחקנים וכו') ואז הדבק בתוכו את הקוד למטה:
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");
}
}
- צור GameObject חדש בסצנה "Playground" וקרא לו "_RoomController"
- צרף סקריפט PUN2_RoomController לאובייקט _RoomController
- הקצה מכונית טרומית ו-SpawnPoints ואז שמור את הסצנה
- הוסף גם סצנות GameLobby וגם Playground להגדרות ה-Build:
4. ביצוע בניית מבחן
עכשיו הגיע הזמן ליצור מבנה ולבדוק אותו:
הכל עובד כמצופה!