(二)PUN 2基本教程
一、介紹
PUN 基礎(chǔ)教程是一個基于 Unity 的教程。我們將使用 Photon Cloud 開發(fā)第一個簡單的 PUN 2 多人游戲。目標是同步每個玩家的動畫角色、健康值和基本光線投射。
1.概述
本教程將從一個空項目開始,逐步指導您完成整個創(chuàng)建過程。在此過程中,將解釋概念,以及網(wǎng)絡(luò)游戲的常見陷阱和設(shè)計注意事項。
我們將實現(xiàn)一個簡單的 UI 來輸入昵稱,我們將向用戶顯示連接進度。
該游戲最多可容納 4 名玩家,并根據(jù)房間內(nèi)的玩家數(shù)量自定義大小的競技場。這主要是為了展示有關(guān)同步場景的幾個概念:加載不同場景時如何處理玩家以及這樣做時可能出現(xiàn)的問題:)
為了不只是讓玩家四處走動而無所事事,我們將實施一個基本的射擊系統(tǒng)以及玩家的健康管理。到那時,我們將學習如何跨網(wǎng)絡(luò)同步變量。
當你的健康為 0 時,游戲結(jié)束,你離開競技場。然后您會再次看到介紹屏幕,如果需要,您可以開始新游戲。
2.你需要知道的
本教程僅假定使用 Unity 編輯器和編程的基礎(chǔ)知識。但是,最好具備良好的游戲創(chuàng)建知識和一些經(jīng)驗,以便專注于 Photon Unity Networking 引入的新概念。
示例代碼是用 C# 編寫的。
3.導入 PUN 和設(shè)置
確保您使用等于或高于 2017.4 的 Unity 版本(不推薦測試版)。創(chuàng)建一個新項目,一般在處理教程時建議這樣做。
打開資產(chǎn)商店并找到 PUN 2 資產(chǎn)并下載/安裝它。導入所有資產(chǎn)后,讓 Unity 重新編譯。
PUN 設(shè)置向?qū)Э梢詭椭M行網(wǎng)絡(luò)設(shè)置,并提供一種方便的方式來開始我們的多人游戲:光子云!
云?是的,云。這是我們可以在游戲中使用的一組 Photon 服務(wù)器。我們稍后會解釋。
通過“免費計劃”使用云是免費的,沒有義務(wù),所以現(xiàn)在我們只需輸入我們的郵件地址,向?qū)Ь蜁l(fā)揮它的魔力。
新帳戶立即獲得“AppId”。如果您的郵件地址已經(jīng)注冊,系統(tǒng)會要求您打開儀表板。登錄并獲取“Ap??pId”以將其粘貼到輸入字段中。
保存 AppId 后,我們就完成了這一步。
?那么,這個“光子云”到底有什么作用呢?!
基本上,它是一堆機器,上面運行著 Photon 服務(wù)器。這個“云”服務(wù)器由 Exit Games 維護,并為您的多人游戲提供無憂服務(wù)。服務(wù)器按需添加,因此可以處理任意數(shù)量的玩家。
盡管 Photon Cloud 并非完全免費,但成本很低,尤其是與常規(guī)托管相比。
Photon Unity Networking 將為您處理 Photon Cloud,但簡而言之,這是內(nèi)部發(fā)生的事情:
每個人都首先連接到“名稱服務(wù)器”。它檢查客戶端想要使用哪個應(yīng)用程序(帶有 AppId)和哪個區(qū)域。然后它將客戶端轉(zhuǎn)發(fā)到主服務(wù)器。
主服務(wù)器是一堆區(qū)域服務(wù)器的樞紐。它知道該區(qū)域的所有房間。任何時候創(chuàng)建或加入房間(比賽/游戲)時,客戶端都會被轉(zhuǎn)發(fā)到另一臺稱為“游戲服務(wù)器”的機器。
PUN 中的設(shè)置非常簡單,您不必關(guān)心托管成本、性能或維護。不止一次。
4.應(yīng)用程序 ID 和游戲版本
由于每個人都連接到相同的服務(wù)器,因此必須有一種方法將您的玩家與其他人的玩家分開。
每個標題(如在游戲、應(yīng)用程序中)在云中都有自己的“AppId”。玩家只會遇到具有相同“AppId”的其他玩家。
還有一個“游戲版”。這是一個您可以編輯的字符串,它將把擁有老客戶的玩家與擁有新客戶的玩家區(qū)分開來。
5.地區(qū)
Photon Cloud 在全球不同的區(qū)域組織,以實現(xiàn)玩家之間的最佳連接。
每個區(qū)域都與所有其他區(qū)域分開,在與分布在不同區(qū)域的遠程團隊合作時記住這一點很重要。確保您最終位于同一地區(qū)。
PUN 2 通過確定一個“開發(fā)區(qū)域”來幫助您,該區(qū)域用于所有開發(fā)構(gòu)建。
6.房間
Photon Cloud 是為“基于房間的游戲”而構(gòu)建的,這意味著每場比賽的玩家數(shù)量有限(比如說:最多 16 人),與其他人分開。在一個房間里,每個人都會收到其他人發(fā)送的任何信息(除非您向特定玩家發(fā)送消息)。在房間外,玩家無法交流,所以我們總是希望他們盡快進入房間。
進入房間的最佳方式是使用隨機匹配??:只需向服務(wù)器詢問任何房間或指定玩家期望的一些屬性。
所有房間都有一個名稱作為標識符。除非房間已滿或關(guān)閉,否則我們可以通過名字加入。方便的是,主服務(wù)器可以為我們的應(yīng)用程序提供房間列表。
7.大廳
您的應(yīng)用程序的大廳存在于主服務(wù)器上,用于列出您的游戲的房間。它不能讓玩家相互交流!
在我們的示例中,我們不會使用大廳,而是簡單地加入一個隨機房間(如果有可用房間),或者如果沒有現(xiàn)有房間可以加入則創(chuàng)建一個新房間(房間可以有最大容量,因此它們可能是全部滿的)。
?8.開發(fā)
本教程的每個部分都涵蓋了項目開發(fā)階段的特定部分。對腳本和 Photon 知識的假設(shè)水平逐漸增加。在最好的情況下,按順序完成它們。
9.教程之外
當然,要創(chuàng)建一個完整的游戲還有很多工作要做,但這只是建立在我們在這里介紹的基礎(chǔ)之上。
請務(wù)必閱讀“入門”部分。
保持好奇,瀏覽文檔和 API 參考只是為了全面了解可用的內(nèi)容。您可能不會立即需要所有內(nèi)容,但是當您需要它或?qū)崿F(xiàn)新功能時,它會重新出現(xiàn)在您的記憶中。您會記得某些方法或?qū)傩允窍嚓P(guān)的,因此是時候正確了解它們了。
利用論壇,不要猶豫,分享您的問題、問題,甚至挫敗感 :) 重要的是您不要被問題困住。通過寫下來讓其他人理解你的問題,你會在你的大腦之外制定它,這有助于解決問題。沒有愚蠢的問題,這完全取決于您的專業(yè)水平以及您對 Unity 和 PUN 的學習/掌握程度。
二、大廳
1.連接到服務(wù)器、房間訪問和創(chuàng)建
讓我們首先解決本教程的核心問題,能夠連接到 Photon Cloud 服務(wù)器并加入一個房間或在必要時創(chuàng)建一個房間。
編碼提示:不要復制/粘貼代碼,您應(yīng)該自己輸入所有內(nèi)容,因為您可能會更好地記住它。編寫注釋非常簡單,在方法或?qū)傩陨戏降男兄墟I入 ///,您將讓腳本編輯器自動創(chuàng)建結(jié)構(gòu)化注釋,例如帶有 <summary> 標記。
讓我們回顧一下到目前為止該腳本中的內(nèi)容,首先是從一般的 Unity 角度,然后是我們進行的 PUN 特定調(diào)用。
(1)命名空間
雖然不是強制性的,但為您的腳本提供適當?shù)拿臻g可以防止與其他資產(chǎn)和開發(fā)人員發(fā)生沖突。如果另一個開發(fā)人員也創(chuàng)建了一個類 Launcher 怎么辦? Unity 會抱怨,您或那個開發(fā)人員將不得不為 Unity 重命名該類以允許執(zhí)行項目。如果沖突來自您從資產(chǎn)商店下載的資產(chǎn),這可能會很棘手?,F(xiàn)在,Launcher 類實際上是 Com.MyCompany.MyGame.Launcher 實際上,其他人不太可能擁有完全相同的命名空間,因為您擁有該域,因此使用反向域約定作為命名空間可以使您的工作安全且井井有條。 Com.MyCompany.MyGame 應(yīng)該替換為您自己的反向域名和游戲名稱,這是一個很好的慣例。
(2)MonoBehaviour 類
請注意,我們從 MonoBehaviour 派生我們的類,這實質(zhì)上將我們的類變成了一個 Unity 組件,然后我們可以將其放到 GameObject 或 Prefab 上。擴展 MonoBehaviour 的類可以訪問許多非常重要的方法和屬性。在您的例子中,我們將使用兩種回調(diào)方法,Awake() 和 Start()。
(3)PhotonNetwork.GameVersion
請注意代表您的游戲版本的 gameVersion 變量。您應(yīng)該將其保留為“1”,直到您需要對已經(jīng)上線的項目進行重大更改。
(4)PhotonNetwork.ConnectUsingSettings()
在 Start() 期間,我們調(diào)用此方法的公共函數(shù) Connect()。這里要記住的重要信息是,此方法是連接到 Photon Cloud 的起點。
(5)PhotonNetwork.AutomaticallySyncScene
我們的游戲?qū)⒂幸粋€可根據(jù)玩家數(shù)量調(diào)整大小的競技場,并確保加載的場景對于每個連接的玩家都是相同的,我們將利用 Photon 提供的非常方便的功能:PhotonNetwork.AutomaticallySyncScene 當這是是的,masterclient 可以調(diào)用 PhotonNetwork.LoadLevel() 并且所有連接的玩家將自動加載相同的級別。
此時,可以保存Launcher Scene,打開PhotonServerSettings(在Unity菜單Window/Photon Unity Networking/Highlight Photon Server Settings中選擇),我們需要將PUN Logging設(shè)置為“Full”:
?編碼時要養(yǎng)成的一個好習慣是始終測試潛在的失敗。這里我們假設(shè)電腦是聯(lián)網(wǎng)的,但是如果電腦沒有聯(lián)網(wǎng)會怎樣呢?讓我們找出來。關(guān)閉計算機上的互聯(lián)網(wǎng)并播放場景。您應(yīng)該會在 Unity 控制臺中看到此錯誤:
Connect() to 'ns.exitgames.com' failed: System.Net.Sockets.SocketException: No such host is known.
?理想情況下,我們的腳本應(yīng)該意識到這個問題,并對這些情況作出優(yōu)雅的反應(yīng),并提出響應(yīng)式體驗,無論出現(xiàn)什么情況或問題。
現(xiàn)在讓我們處理這兩種情況,并在我們的 Launcher 腳本中獲知我們確實連接或未連接到 Photon Cloud。這將是對 PUN 回調(diào)的完美介紹。
2.PUN 回調(diào)
PUN 在回調(diào)方面非常靈活,并提供兩種不同的實現(xiàn)。為了學習,讓我們涵蓋所有方法,我們將根據(jù)情況選擇最適合的方法。
3.實現(xiàn)回調(diào)接口
PUN 提供了您可以在類中實現(xiàn)的 C# 接口:
IConnectionCallbacks:連接相關(guān)的回調(diào)。
IInRoomCallbacks:房間內(nèi)發(fā)生的回調(diào)。
ILobbyCallbacks:大廳相關(guān)回調(diào)。
IMatchmakingCallbacks:匹配相關(guān)回調(diào)。
IOnEventCallback:任何接收到的事件的單一回調(diào)。這等效于 C# 事件 OnEventReceived。
IWebRpcCallback:接收WebRPC操作響應(yīng)的單一回調(diào)。
IPunInstantiateMagicCallback:實例化 PUN 預制件的單個回調(diào)。
IPunObservable:PhotonView 序列化回調(diào)。
IPunOwnershipCallbacks:PUN 所有權(quán)轉(zhuǎn)移回調(diào)。
回調(diào)接口必須注冊和注銷。調(diào)用 PhotonNetwork.AddCallbackTarget(this) 和 PhotonNetwork.RemoveCallbackTarget(this)(可能分別在 OnEnable() 和 OnDisable() 中)
這是確保類符合所有接口但強制開發(fā)人員實現(xiàn)所有接口聲明的一種非常安全的方法。大多數(shù)優(yōu)秀的 IDE 將使這項任務(wù)變得非常容易。然而,腳本最終可能會包含許多可能什么都不做的方法,但必須實現(xiàn)所有方法才能使 Unity 編譯器滿意。因此,這確實是您的腳本將大量使用所有或大部分 PUN 功能的時候。
我們確實要使用 IPunObservable,在本教程的后面進行數(shù)據(jù)序列化。
4.擴展 MonoBehaviourPunCallbacks
我們將經(jīng)常使用的另一種技術(shù)是最方便的。我們將從 MonoBehaviourPunCallbacks 派生類,而不是創(chuàng)建派生自 MonoBehaviour 的類,因為它公開了特定的屬性和虛方法,供我們在方便時使用和覆蓋。這非常實用,因為我們可以確定我們沒有任何錯別字,我們不需要實現(xiàn)所有方法。
注意:覆蓋時,大多數(shù) IDE 默認會執(zhí)行一個堿基調(diào)用并自動為您填充。在我們的例子中,我們不需要,因此作為 MonoBehaviourPunCallbacks 的一般規(guī)則,永遠不要調(diào)用基本方法,除非您覆蓋 OnEnable() 或 OnDisable()。如果您覆蓋 OnEnable() 和 OnDisable(),請始終調(diào)用基類方法。
因此,讓我們通過 OnConnectedToMaster() 和 OnDisconnected() PUN 回調(diào)將其付諸實踐 :
(1)編輯 C# 腳本啟動器
(2)將基類從 MonoBehaviour 修改為 MonoBehaviourPunCallbacks
public class Launcher : MonoBehaviourPunCallbacks {(3)使用 Photon.Realtime 添加;在類定義之前的文件頂部。
(4)為清楚起見,在類的末尾添加以下兩個方法,在 MonoBehaviourPunCallbacks 回調(diào)區(qū)域內(nèi)。
#region MonoBehaviourPunCallbacks Callbackspublic override void OnConnectedToMaster() {Debug.Log("PUN Basics Tutorial/Launcher: OnConnectedToMaster() was called by PUN"); }public override void OnDisconnected(DisconnectCause cause) {Debug.LogWarningFormat("PUN Basics Tutorial/Launcher: OnDisconnected() was called by PUN with reason {0}", cause); }#endregion(5)保存啟動器腳本。
現(xiàn)在,如果我們在有或沒有互聯(lián)網(wǎng)的情況下播放這個場景,我們可以采取適當?shù)牟襟E來通知播放器和/或進一步處理邏輯。當我們開始構(gòu)建 UI 時,我們將在下一節(jié)中處理這個問題?,F(xiàn)在我們將處理成功的連接:
因此,我們將以下調(diào)用附加到 OnConnectedToMaster() 方法:
// #Critical: The first we try to do is to join a potential existing room. If there is, good, else, we'll be called back with OnJoinRandomFailed() PhotonNetwork.JoinRandomRoom();正如評論所說,如果嘗試加入隨機房間失敗,我們需要得到通知,在這種情況下我們需要實際創(chuàng)建一個房間,因此我們在腳本中實現(xiàn) OnJoinRandomFailed() PUN 回調(diào)并使用 PhotonNetwork.CreateRoom 創(chuàng)建一個房間() 并且,您已經(jīng)猜到了,相關(guān)的 PUN 回調(diào) OnJoinedRoom() 將在我們有效加入房間時通知您的腳本:
public override void OnJoinRandomFailed(short returnCode, string message) {Debug.Log("PUN Basics Tutorial/Launcher:OnJoinRandomFailed() was called by PUN. No random room available, so we create one.\nCalling: PhotonNetwork.CreateRoom");// #Critical: we failed to join a random room, maybe none exists or they are all full. No worries, we create a new room.PhotonNetwork.CreateRoom(null, new RoomOptions()); }public override void OnJoinedRoom() {Debug.Log("PUN Basics Tutorial/Launcher: OnJoinedRoom() called by PUN. Now this client is in a room."); }現(xiàn)在,如果您運行該場景,您應(yīng)該按照連接到 PUN 的邏輯順序結(jié)束,嘗試加入現(xiàn)有房間,否則創(chuàng)建一個房間并加入新創(chuàng)建的房間。
在本教程的這一點上,由于我們現(xiàn)在已經(jīng)涵蓋了連接和加入房間的關(guān)鍵方面,所以有一些事情不是很方便,但他們需要盡快解決。這些與學習PUN并無太大關(guān)系,但從整體角度來看卻很重要。
5.在 Unity Inspector 中公開字段
您可能已經(jīng)知道這一點,但如果您不知道,MonoBehaviours 可以自動將字段公開給 Unity Inspector。默認情況下,所有公共字段都是公開的,除非它們被標記為 [HideInInspector]。如果我們想公開非公共字段,我們可以使用屬性 [SerializeField]。這是 Unity 中一個非常重要的概念,在我們的例子中,我們將修改每個房間的最大玩家數(shù)量并將其顯示在檢查器中,以便我們可以在不觸及代碼本身的情況下進行設(shè)置。
我們將對每個房間的最大玩家數(shù)量做同樣的事情。在代碼中對此進行硬編碼并不是最佳做法,相反,讓我們將其作為公共變量,以便我們稍后可以決定并使用該數(shù)字,而無需重新編譯。
在類聲明的開頭,在 Private Serializable Fields 區(qū)域內(nèi)讓我們添加:
/// <summary> /// The maximum number of players per room. When a room is full, it can't be joined by new players, and so new room will be created. /// </summary> [Tooltip("The maximum number of players per room. When a room is full, it can't be joined by new players, and so new room will be created")] [SerializeField] private byte maxPlayersPerRoom = 4;然后我們修改 PhotonNetwork.CreateRoom() 調(diào)用并使用這個新字段而不是我們之前使用的硬編碼數(shù)字。
// #Critical: we failed to join a random room, maybe none exists or they are all full. No worries, we create a new room. PhotonNetwork.CreateRoom(null, new RoomOptions { MaxPlayers = maxPlayersPerRoom });?所以,現(xiàn)在我們不強制腳本使用靜態(tài) MaxPlayers 值,我們只需要在 Unity 檢查器中設(shè)置它,然后點擊運行,不需要打開腳本,編輯它,保存它,等待 Unity 重新編譯最后運行。這種方式的效率和靈活性要高得多。
?三、大廳UI
本部分將重點介紹為大廳創(chuàng)建用戶界面 (UI)。它將保持非?;A(chǔ),因為它與網(wǎng)絡(luò)本身并沒有真正的關(guān)系。
1.Play按鈕
目前我們的大廳自動將我們連接到一個房間,這對早期測試很有幫助,但實際上我們想讓用戶選擇是否以及何時開始游戲。因此,我們只需為此提供一個按鈕。
?如果您現(xiàn)在點擊播放,請注意在您點擊按鈕之前您不會連接。
2.玩家名稱
典型游戲的另一個重要的最低要求是讓用戶輸入他們的名字,這樣其他玩家就知道他們在和誰玩。我們將通過使用 PlayerPrefs 來記住名稱的值,從而為這個簡單的任務(wù)添加一個轉(zhuǎn)折點,以便當用戶再次打開游戲時,我們可以恢復名稱。這是一個非常方便且非常重要的功能,可以在您的游戲的許多區(qū)域?qū)崿F(xiàn),以獲得出色的用戶體驗。
讓我們首先創(chuàng)建將管理和記住玩家名稱的腳本,然后創(chuàng)建相關(guān)的 UI。
3.創(chuàng)建 PlayerNameInputField
(1)創(chuàng)建一個新的 C# 腳本,將其命名為 PlayerNameInputField
(2)這是它的全部內(nèi)容。相應(yīng)地編輯并保存 PlayerNameInputField 腳本
using UnityEngine; using UnityEngine.UI;using Photon.Pun; using Photon.Realtime;using System.Collections;namespace Com.MyCompany.MyGame {/// <summary>/// Player name input field. Let the user input his name, will appear above the player in the game./// </summary>[RequireComponent(typeof(InputField))]public class PlayerNameInputField : MonoBehaviour{#region Private Constants// Store the PlayerPref Key to avoid typosconst string playerNamePrefKey = "PlayerName";#endregion#region MonoBehaviour CallBacks/// <summary>/// MonoBehaviour method called on GameObject by Unity during initialization phase./// </summary>void Start () {string defaultName = string.Empty;InputField _inputField = this.GetComponent<InputField>();if (_inputField!=null){if (PlayerPrefs.HasKey(playerNamePrefKey)){defaultName = PlayerPrefs.GetString(playerNamePrefKey);_inputField.text = defaultName;}}PhotonNetwork.NickName = defaultName;}#endregion#region Public Methods/// <summary>/// Sets the name of the player, and save it in the PlayerPrefs for future sessions./// </summary>/// <param name="value">The name of the Player</param>public void SetPlayerName(string value){// #Importantif (string.IsNullOrEmpty(value)){Debug.LogError("Player Name is null or empty");return;}PhotonNetwork.NickName = value;PlayerPrefs.SetString(playerNamePrefKey,value);}#endregion} }讓我們分析一下這個腳本:
RequireComponent(typeof(InputField)):
我們首先確保此腳本強制執(zhí)行 InputField,因為我們需要它,這是保證此腳本無故障使用的非常方便快捷的方法。
PlayerPrefs.HasKey(),?PlayerPrefs.GetString()?and?PlayerPrefs.SetString():
PlayerPrefs 是一個簡單的配對條目查找列表(就像一個有兩列的 excel 表),一列是鍵,一列是值。 Key 是一個字符串,完全是任意的,你決定如何命名,你需要在整個開發(fā)過程中堅持使用它。因此,始終將 PlayerPrefs Key 存儲在一個地方是有意義的,一種方便的方法是使用 [Static|變量聲明,因為它在游戲過程中不會隨時間改變,每次都是一樣的。可以一直將其聲明為 const,但隨著您使用 C# 獲得越來越多的經(jīng)驗,您將會了解這一點,這只是在此處戲弄 C# 的可能性范圍。
因此,邏輯非常簡單。如果 PlayerPrefs 有一個給定的鍵,我們可以在啟動該功能時獲取它并直接注入該值,在我們的例子中,我們在啟動時用這個填充 InputField,在編輯期間,我們將 PlayerPref Key 設(shè)置為當前InputField 的值,然后我們確定它已本地存儲在用戶設(shè)備上以供以后檢索(用戶下次打開該游戲時)。?
PhotonNetwork.NickName:
這是此腳本的要點,通過網(wǎng)絡(luò)設(shè)置播放器的名稱。該腳本在兩個地方使用它,一次是在檢查名稱是否存儲在 PlayerPrefs 之后的 Start() 期間,一次是在公共方法 SetPlayerName() 中。現(xiàn)在,沒有任何東西調(diào)用這個方法,我們需要綁定 InputField OnValueChange() 來調(diào)用 SetPlayerName() 以便每次用戶編輯 InputField 時,我們都會記錄它。我們只能在用戶按下播放鍵時執(zhí)行此操作,這取決于您,但是這更多涉及腳本方面的知識,因此為了清楚起見,讓我們保持簡單。這也意味著無論用戶將做什么,輸入都會被記住,這通常是期望的行為。
4.為玩家的名字創(chuàng)建 UI
現(xiàn)在你可以點擊播放,輸入你的名字,然后停止播放,再次點擊播放,你輸入的內(nèi)容就會出現(xiàn)。
我們正在取得進展,但就用戶體驗而言,我們?nèi)鄙儆嘘P(guān)連接進度的反饋,以及連接和加入房間時出現(xiàn)問題的反饋。
?5.連接進度
我們將在這里保持簡單,隱藏名稱字段和播放按鈕,并在連接期間將其替換為簡單的文本“正在連接...”,并在需要時將其切換回來。
為此,我們將對播放按鈕進行分組并命名為 Field,這樣我們就可以簡單地激活和停用該組。稍后可以將更多功能添加到組中,這不會影響我們的邏輯。
?此時,為了進行測試,您可以簡單地啟用/禁用控制面板和進度標簽,以查看各個連接階段的情況?,F(xiàn)在讓我們編輯腳本來控制這兩個游戲?qū)ο蟮募せ睢?/p>
(1)編輯腳本Launcher
(2)在 Public Fields 區(qū)域內(nèi)添加以下兩個屬性
[Tooltip("The Ui Panel to let the user enter name, connect and play")] [SerializeField] private GameObject controlPanel; [Tooltip("The UI Label to inform the user that the connection is in progress")] [SerializeField] private GameObject progressLabel;(3)在 Start() 方法中添加以下內(nèi)容
progressLabel.SetActive(false); controlPanel.SetActive(true);(4)在 Connect() 方法的開頭添加以下內(nèi)容
progressLabel.SetActive(true); controlPanel.SetActive(false);(5)將以下內(nèi)容添加到 OnDisconnected() 方法的開頭
progressLabel.SetActive(false); controlPanel.SetActive(true);(6)保存 Script Launcher 并等待 Unity 完成編譯
(7)確保您仍在場景Launcher中。
(8)在層次結(jié)構(gòu)中選擇 GameObject Launcher
(9)從層次結(jié)構(gòu)Control Panel和Progress Label拖放到Launcher組件中的相應(yīng)字段
(10)保存場景
現(xiàn)在,如果您播放場景,您將只看到控制面板,可見并且只要您單擊播放,就會顯示進度標簽。
現(xiàn)在,我們對大廳部分很好。為了進一步向大廳添加功能,我們需要切換到游戲本身,并創(chuàng)建各種場景,以便我們最終可以在加入房間時加載正確的級別。我們將在下一節(jié)中完成,之后,我們將最終完成大廳系統(tǒng)。
四、游戲場景
本節(jié)介紹玩家將要玩的各種場景的創(chuàng)建。
每個場景都將專供特定數(shù)量的玩家使用,場景會越來越大以適應(yīng)所有玩家,并為他們提供足夠的移動空間。
在本教程的后續(xù)部分,我們將實現(xiàn)根據(jù)玩家數(shù)量加載正確關(guān)卡的邏輯,為此我們將使用一個約定,即每個關(guān)卡將使用以下格式命名:“Room for X”,其中 X將代表玩家的數(shù)量。
1.第一個房間創(chuàng)建
這對于一個可玩的關(guān)卡來說已經(jīng)足夠了,但是一些墻會讓玩家保持在地板區(qū)域內(nèi)。只需創(chuàng)建更多立方體并定位、旋轉(zhuǎn)和縮放它們即可充當墻壁。這是所有四面墻的位置和比例,以匹配物體floor
?此時不要忘記保存 Room For 1 Scene。
2.游戲管理器預制件
在所有情況下,用戶界面的最低要求是能夠退出房間。為此,我們需要一個 UI 按鈕,但我們還需要一個腳本來調(diào)用 Photon 讓本地玩家離開房間,所以讓我們從創(chuàng)建我們稱之為游戲管理器預制件開始,第一個它將處理退出本地玩家當前所在房間的任務(wù)。
因此,我們創(chuàng)建了一個公共方法 LeaveRoom()。它所做的是明確讓本地玩家離開 Photon Network 房間。我們將其包裝在我們自己的公共抽象方法周圍。我們可能希望在稍后階段實現(xiàn)更多功能,例如保存數(shù)據(jù),或插入用戶將離開游戲的確認步驟等。
根據(jù)我們的游戲要求,如果我們不在房間里,我們需要顯示 Launcher 場景,所以我們將監(jiān)聽 OnLeftRoom() Photon Callback 并加載大廳場景 Launcher,它在 Build settings 場景的列表中索引為 0 ,我們將在此頁面的“構(gòu)建設(shè)置場景列表”部分中進行設(shè)置。
但是為什么我們要用這個做一個預制件呢?因為我們的游戲需求意味著同一個游戲有多個場景,所以我們需要重用這個游戲管理器。在 Unity 中,重用游戲?qū)ο蟮淖罴逊绞绞菍⑺鼈冏兂深A制件。
接下來,讓我們創(chuàng)建將調(diào)用 GameManager 的 LeaveRoom() 方法的 UI 按鈕。
3.退出房間按鈕預制體
同樣,就像游戲管理器一樣,從我們將有許多不同場景需要此功能的角度來看,提前計劃并將按鈕制作成預制件是有意義的,這樣我們就可以重用它并僅在一個地方修改它我們需要在未來。
4.其他房間創(chuàng)建
現(xiàn)在我們已經(jīng)正確地完成了一個房間,讓我們將其復制 3 次,并適當?shù)孛鼈?#xff08;當您復制它們時,它們應(yīng)該已經(jīng)由 Unity 命名):
- Room for 2
- Room for 3
- Room for 4
在下面找到位置、旋轉(zhuǎn)和比例的變化以加速這個重復過程。(略)
5.構(gòu)建Build Settings列表
對于項目在編輯和發(fā)布時的良好運行至關(guān)重要,我們需要在構(gòu)建設(shè)置中添加所有這些場景,以便 Unity 在構(gòu)建應(yīng)用程序時包含它們。
(1)通過菜單“File/Build Settings”打開構(gòu)建設(shè)置
(2)拖放所有場景,Launcher場景必須保持在第一個,因為默認情況下 Unity 將加載并向玩家顯示該列表中的第一個場景
?現(xiàn)在我們已經(jīng)完成了基本的場景設(shè)置,我們終于可以開始連接所有東西了。讓我們在下一節(jié)中執(zhí)行此操作。
五、Game Manager & Levels
本節(jié)介紹了根據(jù)當前在房間中玩游戲的玩家數(shù)量來處理各種關(guān)卡加載的功能。
1.加載競技場例程
我們創(chuàng)建了 4 個不同的房間,并且按照約定最后一個字符是玩家人數(shù)來命名它們,因此現(xiàn)在可以很容易地綁定房間中當前的玩家人數(shù)和相關(guān)場景。這是一種非常有效的技術(shù),稱為“約定優(yōu)于配置”。例如,基于“配置”的方法會為房間中給定數(shù)量的玩家維護場景名稱的查找表列表。然后我們的腳本會查看該列表并返回一個名稱根本無關(guān)緊要的場景。 “配置”通常需要更多的腳本,這就是為什么我們會在這里選擇“約定”,它可以讓我們更快地工作代碼,而不會用不相關(guān)的功能污染我們的代碼。
(1)打開 GameManager 腳本
(2)讓我們在專用于我們將為該場合創(chuàng)建的私有方法的新區(qū)域中添加一個新方法。不要忘記保存 GameManager 腳本。?
當我們調(diào)用此方法時,我們將根據(jù)我們所在房間的 PlayerCount 屬性加載適當?shù)姆块g。
?這里有兩點需要注意,非常重要:
(1)PhotonNetwork.LoadLevel() 只應(yīng)在我們是 MasterClient 時調(diào)用。所以我們首先使用 PhotonNetwork.IsMasterClient 檢查我們是 MasterClient。調(diào)用者也有責任檢查這一點,我們將在本節(jié)的下一部分中介紹。
(2)我們使用 PhotonNetwork.LoadLevel() 來加載我們想要的級別,我們不直接使用 Unity,因為我們希望依靠 Photon 在房間中所有連接的客戶端上加載這個級別,因為我們已經(jīng)啟用了 PhotonNetwork.AutomaticallySyncScene這個游戲。我們使用 PhotonNetwork.LoadLevel() 來加載我們想要的級別,我們不直接使用 Unity,因為我們希望依靠 Photon 在房間中所有連接的客戶端上加載這個級別,因為我們已經(jīng)啟用了 PhotonNetwork.AutomaticallySyncScene這個游戲。
現(xiàn)在我們有了加載正確關(guān)卡的函數(shù),讓我們將其與連接和斷開連接的玩家綁定。
2.觀看玩家連接
我們已經(jīng)在教程的前一部分研究了獲取 Photon Callbacks 的各種方法,現(xiàn)在 GameManager 需要監(jiān)聽玩家的連接和斷開連接。讓我們來實現(xiàn)它。
(1)打開 GameManager 腳本
(2)添加以下 Photon 回調(diào)并保存 GameManager 腳本
現(xiàn)在,我們有一個完整的設(shè)置。每次玩家加入或離開房間時,我們都會收到通知,我們會調(diào)用之前實現(xiàn)的 LoadArena() 方法。但是,只有當我們是使用 PhotonNetwork.IsMasterClient 的 MasterClient 時,我們才會調(diào)用 LoadArena()。
現(xiàn)在讓我們回到大廳,最終能夠在加入房間時加載正確的場景。
3.從大廳loading Arena
(1)編輯腳本Launcher。
(2)將以下內(nèi)容附加到 OnJoinedRoom() 方法
讓我們測試一下,打開場景Launcher,然后運行它。單擊“播放”,讓系統(tǒng)連接并加入房間。就是這樣,現(xiàn)在我們的大廳開始工作了。但是如果你離開房間,你會注意到當回到大廳時,它會自動重新加入……哎呀,讓我們來解決這個問題。
如果您還不知道為什么會發(fā)生這種情況,請“簡單地”分析日志。我只是簡單地引用一下,因為需要實踐和經(jīng)驗才能獲得自動性來概述問題并知道在哪里查看以及如何調(diào)試它。
現(xiàn)在嘗試一下,如果您仍然無法找到問題的根源,讓我們一起來解決這個問題。
要解決這個問題,我們需要了解上下文。當用戶點擊“播放”按鈕時,我們應(yīng)該舉起一個標志來知道連接過程是由用戶發(fā)起的。然后我們可以檢查此標志以在各種光子回調(diào)中相應(yīng)地采取行動。
(1)編輯腳本啟動器
(2)在 Private Fields 區(qū)域內(nèi)創(chuàng)建新屬性
(3)在 Connect() 方法內(nèi)部將 isConnecting 設(shè)置為 PhotonNetwork.ConnectUsingSettings() 方法的返回值,如下所示:
// keep track of the will to join a room, because when we come back from the game we will get a callback that we are connected, so we need to know what to do then isConnecting = PhotonNetwork.ConnectUsingSettings();結(jié)果:
public void Connect() {progressLabel.SetActive(true);controlPanel.SetActive(false);if (PhotonNetwork.IsConnected){PhotonNetwork.JoinRandomRoom();}else{isConnecting = PhotonNetwork.ConnectUsingSettings();PhotonNetwork.GameVersion = gameVersion;} }(4)在 OnConnectedToMaster() 方法中,用 if 語句包圍 PhotonNetwork.JoinRandomRoom() 如下:
// we don't want to do anything if we are not attempting to join a room. // this case where isConnecting is false is typically when you lost or quit the game, when this level is loaded, OnConnectedToMaster will be called, in that case // we don't want to do anything. if (isConnecting) {// #Critical: The first we try to do is to join a potential existing room. If there is, good, else, we'll be called back with OnJoinRandomFailed()PhotonNetwork.JoinRandomRoom();isConnecting = false; }(5)在 OnDisconnected 方法中,將 isConnecting 設(shè)置為 false
現(xiàn)在,如果我們再次測試并運行 Launcher Scene,并在 Lobby 和 Game 之間來回切換,一切都很好 :) 為了測試場景的自動同步,您需要發(fā)布應(yīng)用程序(為桌面發(fā)布,它是運行測試最快的),并與 Unity 一起運行,因此您實際上有兩個玩家將連接并加入一個房間。如果 Unity 編輯器首先創(chuàng)建房間,它將是 MasterClient,您將能夠在 Unity 控制臺中驗證您在連接時獲得“PhotonNetwork:加載級別:1”和后來的“PhotonNetwork:加載級別:2”與已發(fā)布的實例。
好的!我們已經(jīng)介紹了很多,但這只是工作的一半……:) 我們需要自己解決玩家問題,所以讓我們在下一節(jié)中解決這個問題。不要忘記不時離開計算機休息一下,以便更有效地吸收所解釋的各種概念。
六、構(gòu)建玩家
本節(jié)將指導您從頭開始創(chuàng)建將在本教程中使用的Player預制件,因此我們將涵蓋創(chuàng)建過程的每個步驟。
嘗試創(chuàng)建一個可以在沒有連接 PUN 的情況下工作的Player預制件始終是一個好方法,這樣可以輕松快速測試、調(diào)試并確保一切至少在沒有任何網(wǎng)絡(luò)功能的情況下工作。然后,您可以緩慢而穩(wěn)妥地構(gòu)建和修改每個功能,使其成為網(wǎng)絡(luò)兼容的角色。通常,用戶輸入只能在玩家擁有的實例上激活,而不應(yīng)在其他玩家的計算機上激活。我們將在下面詳細介紹。
1.預制基礎(chǔ)知識
要了解 PUN 的第一個也是重要的規(guī)則是,應(yīng)該通過網(wǎng)絡(luò)實例化的預制件必須位于 Resources 文件夾中。
在 Resources 文件夾中包含預制件的第二個重要副作用是您需要注意它們的名稱。您的資產(chǎn)資源路徑下不應(yīng)有兩個名稱相同的預制件,因為 Unity 會選擇它找到的第一個。因此,請始終確保在您的項目資源中,資源文件夾路徑中沒有兩個同名的預制件。我們很快就會談到這一點。
我們將使用 Unity 作為免費資產(chǎn)提供的 Kyle 機器人。它以 Fbx 文件的形式出現(xiàn),該文件是使用 3ds Max、Maya、cinema 4D 等 3d 軟件創(chuàng)建的。使用這些軟件創(chuàng)建網(wǎng)格和動畫超出了本教程的范圍。這個機器人 Kyle.fbx 位于“\Assets\Photon\PhotonUnityNetworking\Demos\Shared Assets\Models”。
這是開始為您的Player使用“Kyle Robot.fbx”的一種方法:
(1)在你的“項目瀏覽器”中,在某處創(chuàng)建一個名為“Resources”的文件夾,通常建議你組織你的內(nèi)容,所以可以有類似“PunBasics_tutorial\Resources”的東西
(2)創(chuàng)建一個新的空場景,并將其保存為“PunBasics_tutorial\Scenes”中的 Kyle Test。 “Kyle Test”場景的目的僅僅是創(chuàng)建預制件并進行設(shè)置。一旦完成,您就可以擺脫現(xiàn)場。
(3)將 Robot Kyle 拖放到“Scene Hierarchy”上。
(4)將剛剛在層次結(jié)構(gòu)中創(chuàng)建的 GameObject 重命名為 My Robot Kyle
(5)將我的機器人 Kyle 拖放到“PunBasics_tutorial\Resources”
(6)我們現(xiàn)在已經(jīng)創(chuàng)建了一個基于 Kyle Robot Fbx 資產(chǎn)的預制件,并且我們在場景 Kyle Test 的層次結(jié)構(gòu)中有一個它的實例?,F(xiàn)在我們可以開始使用它了。
2.CharacterController
(1)這個Component是Unity提供的一個非常方便的Standard Asset,可以讓我們使用Animator更快地制作出典型的角色,所以讓我們利用這些Unity的強大功能吧。
(2)雙擊 My Kyle Robot 放大場景視圖。注意以腳為中心的“Capsule Collider”;我們實際上需要“Capsule Collider”來正確匹配角色。
(3)在 CharacterController 組件中,將 Center.y 屬性更改為 1(其 Height 屬性的一半)。
?(4)點擊“Apply”以影響我們所做的更改。這是非常重要的一步,因為我們已經(jīng)編輯了預制件 My Kyle Robot 的一個實例,但我們希望這些更改適用于每個實例,而不僅僅是這個實例,所以我們點擊“Apply”。
3.分配Animator Controller
Kyle Robot Fbx 資產(chǎn)需要由 Animator Graph 控制。我們不會在本教程中介紹此圖的創(chuàng)建,因此我們?yōu)榇颂峁┝艘粋€控制器,位于 \Assets\Photon\PhotonUnityNetworking/Demos/PunBasics-Tutorial/Animator/ 下的項目資產(chǎn)中,名為 Kyle Robot
?要將此 Kyle 機器人控制器分配給我們的預制件,只需將 Animator 組件的屬性 Controller 設(shè)置為指向 Kyle Robot
?不要忘記,如果您在 My Kyle Robot 的實例上執(zhí)行此操作,則需要點擊預制件本身的“Apply”以合并這些更改。
4.使用控制器參數(shù)
理解動畫控制器的關(guān)鍵特征是動畫參數(shù)。我們正在使用這些,通過腳本控制我們的動畫。在我們的例子中,我們有 Speed、Direction、Jump、Hi 等參數(shù)。
Animator 組件的一大特色是能夠根據(jù)動畫實際移動角色。此功能稱為 Root Motion,Animator Component 上有一個 Apply Root Motion 屬性,默認情況下為 true,所以我們可以開始了。
因此,實際上,要讓角色行走,我們只需將速度動畫參數(shù)設(shè)置為正值,它就會開始行走并向前移動。我們開工吧!
5.Animator 管理腳本
讓我們創(chuàng)建一個新腳本,我們將在其中根據(jù)用戶的輸入控制角色。
?6.動畫管理:速度控制
我們需要編碼的第一件事是獲取 Animator 組件,以便我們可以控制它。
(1)確保您正在編輯腳本 PlayerAnimatorManager
(2)創(chuàng)建類型為 Animator 的私有變量動畫器
(3)將 Animator 組件存儲在 Start() 方法中的這個變量中
private Animator animator; // Use this for initialization void Start() {animator = GetComponent<Animator>();if (!animator){Debug.LogError("PlayerAnimatorManager is Missing Animator Component", this);} }請注意,由于我們需要一個 Animator 組件,如果我們沒有得到一個,我們會記錄一個錯誤,這樣它就不會被忽視并立即被開發(fā)人員解決。您應(yīng)該始終編寫代碼,就好像它會被其他人使用 :) 這很乏味,但從長遠來看是值得的。
(4)現(xiàn)在讓我們聽取用戶輸入并控制速度動畫參數(shù)。然后保存腳本PlayerAnimatorManager。
// Update is called once per frame void Update() {if (!animator){return;}float h = Input.GetAxis("Horizontal");float v = Input.GetAxis("Vertical");if (v < 0){v = 0;}animator.SetFloat("Speed", h * h + v * v); }讓我們研究一下這個腳本在做什么:
由于我們的游戲不允許后退,因此我們確保 v 小于 0。如果用戶按下“向下箭頭”或“s”鍵(垂直軸的默認設(shè)置),我們不允許這樣做并強制值為 0。
您還會注意到我們對兩個輸入進行了平方。為什么?所以它總是一個正的絕對值以及添加一些緩和。不錯的技巧就在這里。您也可以使用 Mathf.Abs() ,那會很好。
我們還添加了兩個輸入來控制速度,這樣當只按下左或右輸入時,我們?nèi)匀粫谵D(zhuǎn)彎時獲得一些速度。
當然,所有這些都非常針對我們的角色設(shè)計,根據(jù)您的游戲邏輯,您可能希望角色原地轉(zhuǎn)彎,或者能夠向后退。動畫參數(shù)的控制總是非常特定于游戲。
7. 測試,測試,1 2 3 ...
讓我們驗證一下我們到目前為止所做的。確保您已打開 Kyle Test 場景。目前,在這個場景中,我們只有一個攝像頭和 Kyle 機器人實例,場景中缺少機器人站立的地面,如果你現(xiàn)在跑到場景中,Kyle 機器人就會倒下。此外,我們不會關(guān)心場景中的燈光或任何花哨的東西,我們想測試和驗證我們的角色和腳本是否正常工作。
這很好,但我們還有很多工作要做,相機需要跟上,我們還不能轉(zhuǎn)向......
如果您現(xiàn)在想在相機上工作,請轉(zhuǎn)到專用部分,此頁面的其余部分將完成 Animator 控件并實現(xiàn)旋轉(zhuǎn)。
8.Animator Manager 腳本:方向控制
控制旋轉(zhuǎn)會稍微復雜一些;我們不希望我們的角色在按下左右鍵時突然旋轉(zhuǎn)。我們想要柔和平滑的旋轉(zhuǎn)。幸運的是,可以使用一些阻尼來設(shè)置動畫參數(shù)
(1)確保您正在編輯 Script PlayerAnimatorManager
(2)在新區(qū)域“Private Fields”區(qū)域內(nèi)創(chuàng)建一個公共浮動變量 directionDampTime。
#region Private Fields[SerializeField] private float directionDampTime = 0.25f;#endregion?(3)在 Update() 函數(shù)的末尾,添加
animator.SetFloat("Direction", h, directionDampTime, Time.deltaTime);所以我們馬上注意到 animator.SetFloat() 有不同的簽名。我們用來控制 Speed 的參數(shù)很簡單,但是這個參數(shù)需要兩個參數(shù),一個是阻尼時間,一個是 deltaTime。阻尼時間是有道理的:它需要多長時間才能達到所需的值,但是 deltaTime?。它本質(zhì)上允許您編寫與幀速率無關(guān)的代碼,因為 Update() 取決于幀速率,我們需要使用 deltaTime 來解決這個問題。盡可能多地閱讀有關(guān)該主題的內(nèi)容以及在網(wǎng)絡(luò)上搜索此內(nèi)容時您會找到的內(nèi)容。理解這個概念后,您將能夠充分利用 Unity 的許多功能,包括動畫和隨時間推移對值的一致控制。
?(4)播放您的場景,并使用所有箭頭查看您的角色行走和轉(zhuǎn)身的情況
(5)測試directionDampTime的效果:比如1,然后5,看看達到最大轉(zhuǎn)彎能力需要多長時間。您會看到轉(zhuǎn)彎半徑隨著 directionDampTime 的增加而增加。
9.Animator Manager腳本:跳躍
對于跳躍,由于兩個因素,我們需要做更多的工作。第一,我們不希望玩家在不跑的情況下跳躍,第二,我們不希望跳躍是循環(huán)的。
確保您正在編輯 Script PlayerAnimatorManager
在我們在 Update() 方法中捕獲用戶輸入之前插入它
測試。開始奔跑并按下“alt”鍵或鼠標右鍵,Kyle?就會跳起來。
好的,那么首先要了解我們?nèi)绾沃繟nimator是否正在運行。我們使用 stateInfo.IsName("Base Layer.Run") 執(zhí)行此操作。我們只是詢問 Animator 的當前活動狀態(tài)是否為運行。我們必須附加 Base Layer,因為 Run 狀態(tài)在 Base Layer 中。
如果我們處于 Run 狀態(tài),那么我們將監(jiān)聽 Fire2 Input 并在必要時觸發(fā) Jump 觸發(fā)器。
所以,這是到目前為止完整的 PlayerAnimatorManager 腳本:
當您考慮它在場景中實現(xiàn)的目標時,對于幾行代碼來說還不錯。現(xiàn)在讓我們處理相機工作,因為我們能夠在我們的世界中進化,我們需要適當?shù)南鄼C行為來跟進。
?10.相機設(shè)置
在本節(jié)中,我們將使用 CameraWork 腳本。如果您想從頭開始編寫 CameraWork,請轉(zhuǎn)到下一部分,完成后返回此處。
11.PhotonView 組件
我們需要將一個 PhotonView 組件附加到我們的預制件上。 PhotonView 將每臺計算機上的各種實例連接在一起,并定義要觀察的組件以及如何觀察這些組件。
12.Beams設(shè)置
我們的機器人角色仍然缺少他的武器,讓我們創(chuàng)建一些從他的眼睛中射出的激光束。
13.添加Beams模型
為了簡單起見,我們將使用簡單的立方體并將它們縮放到非常細和長。有一些技巧可以快速完成此操作:不要直接將立方體添加為頭部的子項,而是創(chuàng)建它移動它并自行放大,然后將其附加到頭部,這將防止猜測正確的旋轉(zhuǎn)值讓你的光束與眼睛對齊。
另一個重要的技巧是對兩個光束只使用一個對撞機。這是為了讓物理引擎更好地工作,薄對撞機從來都不是一個好主意,它不可靠,所以我們將制作一個大盒子對撞機,以便我們確??煽康負糁心繕?。
(1)打開 Kyle 測試場景
(2)在場景中添加一個立方體,將其命名為 Beam Left
(3)修改它看起來像一根長光束,并正確定位在左眼上
(4)在層次結(jié)構(gòu)中選擇 My Kyle Robot 實例
(5)找到Head child
?(6)添加一個空游戲?qū)ο笞鳛椤癏ead”游戲?qū)ο蟮淖訉ο?#xff0c;將其命名為“Beams” 7. 將“Beam Left”拖放到“Beams”內(nèi) 8.復制“Beams Left”,將其命名為“Beams Right” 9.將其定位使其與右眼對齊 10. 從 `Beams Right` 中移除 Box Collider 組件 11. 調(diào)整 `Beams Left` 的“Box Collider”中心和大小以封裝兩個光束 12. 轉(zhuǎn)動 `Beams Left` 的“Box Collider” `IsTrigger` 屬性設(shè)置為 `True`,我們只想知道光束接觸玩家,而不是碰撞。 13. 創(chuàng)建一個新材料,將其命名為“Red Beam” 14. 將“Red Beam”材料分配給兩個梁 15. 將更改應(yīng)用回預制件
注意:激光束應(yīng)該在角色的碰撞器之外,以免傷到自己。
你現(xiàn)在應(yīng)該有這樣的東西:
?
?14.使用用戶輸入控制光束
好的,現(xiàn)在我們有了光束,讓我們插入 Fire1 輸入來觸發(fā)它們。
讓我們創(chuàng)建一個新的 C# 腳本,稱為 PlayerManager。下面是讓光束工作的第一個版本的完整代碼。
此腳本在此階段的要點是激活或停用光束。激活后,光束將在與其他模型發(fā)生碰撞時有效觸發(fā)。我們稍后會抓住這些觸發(fā)器來影響每個角色的健康。
我們還公開了一個公共屬性 Beams,它可以讓我們在 My Kyle Robot Prefab 的層次結(jié)構(gòu)中引用確切的游戲?qū)ο蟆W屛覀兛纯次覀冃枰绾喂ぷ鞑拍苓B接梁,因為這在預制件中很棘手,因為在資產(chǎn)瀏覽器中,預制件只公開第一個孩子,而不是子孩子,而且我們的梁確實埋在預制件層次結(jié)構(gòu)中,所以我們需要從場景中的一個實例中做到這一點,然后將其應(yīng)用回預制件本身。
如果你點擊播放,然后按下 Fire1 輸入(默認情況下是鼠標左鍵或左 ctrl 鍵),光束將出現(xiàn),并在你松開時立即隱藏。
15.健康值設(shè)置
讓我們實現(xiàn)一個非常簡單的健康系統(tǒng),當光束擊中玩家時,它會減少。由于它不是子彈,而是源源不斷的能量流,我們需要以兩種方式考慮健康損害,當我們被光束擊中時,以及在光束擊中我們的整個過程中。
(1)打開 PlayerManager 腳本
(2)在 Public Fields 區(qū)域內(nèi)添加Public Fields屬性
(3)將以下兩個方法添加到 MonoBehaviour 回調(diào)區(qū)域。然后保存 PlayerManager 腳本。
/// <summary> /// MonoBehaviour method called when the Collider 'other' enters the trigger. /// Affect Health of the Player if the collider is a beam /// Note: when jumping and firing at the same, you'll find that the player's own beam intersects with itself /// One could move the collider further away to prevent this or check if the beam belongs to the player. /// </summary> void OnTriggerEnter(Collider other) {if (!photonView.IsMine){return;}// We are only interested in Beamers// we should be using tags but for the sake of distribution, let's simply check by name.if (!other.name.Contains("Beam")){return;}Health -= 0.1f; } /// <summary> /// MonoBehaviour method called once per frame for every Collider 'other' that is touching the trigger. /// We're going to affect health while the beams are touching the player /// </summary> /// <param name="other">Other.</param> void OnTriggerStay(Collider other) {// we dont' do anything if we are not the local player.if (!photonView.IsMine){return;}// We are only interested in Beamers// we should be using tags but for the sake of distribution, let's simply check by name.if (!other.name.Contains("Beam")){return;}// we slowly affect health when beam is constantly hitting us, so player has to move to prevent death.Health -= 0.1f * Time.deltaTime; }PlayerManager 擴展了 MonoBehaviourPunCallbacks。 MonoBehaviourPunCallbacks 擴展了 MonoBehaviourPun。 MonoBehaviourPun 有一個帶有“延遲初始化”的 photonView 屬性。這就是 photonView 最終出現(xiàn)在 PlayerManager 中的方式。
首先,這兩種方法幾乎相同,唯一的區(qū)別是我們在 TriggerStay 期間使用 Time.deltaTime 減少健康,減少的速度不依賴于幀率。這是一個通常適用于動畫的重要概念,但在這里,我們也需要這個,我們希望所有設(shè)備的健康狀況以可預測的方式下降,如果在更快的計算機上,你的健康狀況下降得更快是不公平的:) Time.deltaTime 是為了保證一致性。如果您有任何問題,請回復我們并通過搜索 Unity 社區(qū)了解 Time.deltaTime,直到您完全理解這個概念,這是必不可少的。
第二個重要方面,現(xiàn)在應(yīng)該明白的是,我們只影響本地玩家的健康,這就是為什么如果 PhotonView 不是我的,我們會提前退出該方法。
最后,如果擊中我們的物體是光束,我們只想影響健康。
為了便于調(diào)試,我們將 Health 浮點數(shù)設(shè)置為公共浮點數(shù),以便在等待構(gòu)建 UI 時輕松檢查其值。
好的,看起來一切都完成了嗎?嗯...如果不考慮玩家的游戲結(jié)束狀態(tài),健康系統(tǒng)是不完整的,當健康達到 0 時發(fā)生,讓我們現(xiàn)在開始吧。
16.健康值檢查游戲結(jié)束
為簡單起見,當玩家的生命值達到 0 時,我們只需離開房間,如果您還記得的話,我們已經(jīng)在 GameManager 腳本中創(chuàng)建了一個離開房間的方法。如果我們可以重用此方法而不是對相同的功能進行兩次編碼,那就太好了。您應(yīng)該不惜一切代價避免針對相同結(jié)果的重復代碼。這也是介紹一個非常方便的編程概念“Singleton”的好時機。雖然這個主題本身可以填滿幾個教程,但我們只會做“單例”的最小實現(xiàn)。了解單例、它們在 Unity 上下文中的變體以及它們?nèi)绾螏椭鷦?chuàng)建強大的功能非常重要,并且會為您省去很多麻煩。因此,請不要猶豫,抽出本教程的時間來了解更多信息。
(1)打開 GameManager 腳本
(2)在 Public Fields 區(qū)域添加這個變量
(3)添加 Start() 方法,如下所示
void Start() {Instance = this; }請注意,我們用 [static] 關(guān)鍵字修飾了 Instance 變量,這意味著該變量無需保存指向 GameManager 實例的指針即可使用,因此您可以在代碼的任何位置簡單地執(zhí)行 GameManager.Instance.xxx() .真的很實用!讓我們看看它如何適合我們的邏輯管理游戲
(1)打開 PlayerManager 腳本
(2)在 Update() 方法中,在我們檢查 photonView.IsMine 的 if 語句中,添加它并保存 PlayerManager 腳本
注意:我們考慮到健康可能是負值,因為激光束造成的損害強度不同。
注意:我們到達 GameManager 實例的 LeaveRoom() 公共方法而無需實際獲取 Component 或任何東西,我們只依賴于假設(shè) GameManager 組件位于當前場景中的 GameObject 上這一事實。
?好的,現(xiàn)在我們進入網(wǎng)絡(luò)!
七、構(gòu)建玩家相機
本節(jié)將指導您創(chuàng)建 CameraWork 腳本,以便在您玩此游戲時跟隨您的玩家。
本節(jié)與網(wǎng)絡(luò)無關(guān),因此將保持簡短。
1.創(chuàng)建 CameraWork 腳本
(1)創(chuàng)建一個名為 CameraWork 的新 C# 腳本
(2)將 CameraWork 的內(nèi)容替換為以下內(nèi)容:
跟隨玩家背后的邏輯很簡單。我們使用距離計算所需的相機位置,并添加偏移量以落后于使用高度。然后它使用 Lerping 來平滑運動以趕上所需的位置,最后,一個簡單的 LookAt 讓相機始終指向玩家。
除了 Camera 工作本身,還設(shè)置了一些重要的東西;控制行為何時應(yīng)該主動跟隨玩家的能力。理解這一點很重要:我們什么時候想讓攝像機跟隨玩家?
通常,讓我們想象一下如果它始終跟隨玩家會發(fā)生什么。當你連接到一個滿是玩家的房間時,其他玩家實例上的每個 CameraWork 腳本都會爭先恐后地控制“主攝像機”,以便它看著它的玩家......好吧,我們不想那樣,我們只想跟隨代表計算機后面的用戶的本地Player。
一旦我們定義了我們只有一個相機但有多個玩家實例的問題,我們就可以輕松找到多種方法來解決這個問題。
(1)僅在本地Player上附加 CameraWork 腳本。
(2)通過關(guān)閉和打開 CameraWork 行為來控制它,具體取決于它必須跟隨的玩家是否是本地玩家。
(3)將 CameraWork 連接到相機并注意場景中何時有本地Player實例并僅關(guān)注該Player實例。
這 3 個選項并不詳盡,可以找到更多方法,但在這 3 個中,我們將任意選擇第二個。以上選項都沒有好壞之分,但這是可能需要最少編碼量并且最靈活的選項......“有趣......”我聽到你說:)
(1)我們暴露了一個字段 followOnStart 如果我們想在非網(wǎng)絡(luò)環(huán)境中使用它,我們可以將其設(shè)置為 true,例如在我們的測試場景中,或者在完全不同的場景中
(2)在基于網(wǎng)絡(luò)的游戲中運行時,當我們檢測到玩家是本地玩家時,我們將調(diào)用公共方法 OnStartFollowing()。這將在播放器預制網(wǎng)絡(luò)一章中創(chuàng)建和解釋的腳本 PlayerManager 中完成
?八、Player Networking
本節(jié)將指導您修改“Player”預制件。我們首先創(chuàng)建了一個按原樣工作的Player,但現(xiàn)在我們將對其進行修改,以便在我們在 PUN 環(huán)境中使用它時它可以工作并符合要求。修改非常輕,但概念很關(guān)鍵。所以這個部分確實非常重要。
1.Transform同步
我們想要同步的明顯特征是角色的位置和旋轉(zhuǎn),這樣當玩家四處移動時,角色在其他玩家的游戲?qū)嵗械男袨榉绞较嗨啤?br /> 您可以在自己的 Script 中手動觀察 Transform 組件,但是由于網(wǎng)絡(luò)延遲和同步數(shù)據(jù)的有效性,您會遇到很多麻煩。幸運的是,為了簡化這項常見任務(wù),我們將使用 PhotonTransformView 組件。基本上,此組件已為您完成所有艱苦的工作。
2.Animator同步
PhotonAnimatorView 還使網(wǎng)絡(luò)設(shè)置變得輕而易舉,將為您節(jié)省大量時間和麻煩。它允許您定義要同步的圖層權(quán)重和參數(shù)。只有當層權(quán)重在游戲過程中發(fā)生變化時才需要同步,并且可能根本不同步它們就可以逃脫。參數(shù)也是如此。有時可以從其他因素中得出動畫值。速度值就是一個很好的例子,您不一定需要完全同步該值,但您可以使用同步位置更新來估計其值。如果可能,請嘗試同步盡可能少的參數(shù)。
每個值都可以被Disabled禁用,或者Discrete離散地或Continuous連續(xù)地同步。在我們的例子中,因為我們沒有使用 Hi 參數(shù),所以我們將禁用它并節(jié)省流量。
Discrete離散同步意味著默認情況下每秒發(fā)送 10 次值(在 OnPhotonSerializeView 中)。接收客戶端將值傳遞給他們本地的 Animator。?
Continuous連續(xù)同步意味著 PhotonAnimatorView 運行每一幀。當調(diào)用 OnPhotonSerializeView 時(默認情況下每秒 10 次),自上次調(diào)用以來記錄的值將一起發(fā)送。接收客戶端然后按順序應(yīng)用這些值以保持平滑過渡。這種模式雖然更流暢,但也會發(fā)送更多的數(shù)據(jù)來達到這種效果。
?3.用戶輸入管理
用戶對網(wǎng)絡(luò)的控制的一個關(guān)鍵方面是相同的預制件將為所有玩家實例化,但其中只有一個代表用戶實際在計算機前玩,所有其他實例代表其他用戶,在其他計算機上玩。因此,考慮到這一點的第一個障礙是"Input Management”。我們?nèi)绾卧谝粋€實例上啟用輸入而不在其他實例上啟用輸入以及如何知道哪個是正確的?輸入 IsMine 概念。
讓我們編輯之前創(chuàng)建的 PlayerAnimatorManager 腳本。在當前形式下,此腳本不知道這種區(qū)別,讓我們來實現(xiàn)它。
好的,如果實例由“客戶端”應(yīng)用程序控制,則 photonView.IsMine 將為真,這意味著該實例代表在此應(yīng)用程序中在此計算機上玩游戲的自然人。因此,如果它為 false,我們不想做任何事情,只依賴 PhotonView 組件來同步我們之前設(shè)置的Transform和Animator組件。
但是,為什么還要在我們的 if 語句中強制執(zhí)行 PhotonNetwork.IsConnected == true 呢?嗯嗯:) 因為在開發(fā)過程中,我們可能想在沒有連接的情況下測試這個預制件。例如,在虛擬場景中,僅創(chuàng)建和驗證與網(wǎng)絡(luò)功能本身無關(guān)的代碼。因此,通過這個附加表達式,我們將允許在未連接時使用輸入。這是一個非常簡單的技巧,將大大改善您在開發(fā)過程中的工作流程。
4.相機控制
它與輸入相同,玩家只有一個游戲視圖,因此我們需要 CameraWork 腳本只跟隨本地玩家,而不是其他玩家。這就是 CameraWork 腳本具有定義何時跟隨的能力的原因。
讓我們修改 PlayerManager 腳本來控制 CameraWork 組件。
在 Awake() 和 Update() 方法之間插入下面的代碼:
/// <summary> /// MonoBehaviour method called on GameObject by Unity during initialization phase. /// </summary> void Start() {CameraWork _cameraWork = this.gameObject.GetComponent<CameraWork>();if (_cameraWork != null){if (photonView.IsMine){_cameraWork.OnStartFollowing();}}else{Debug.LogError("<Color=Red><a>Missing</a></Color> CameraWork Component on playerPrefab.", this);} }首先,它獲取 CameraWork 組件,這是我們所期望的,所以如果我們沒有找到它,我們會記錄一個錯誤。然后,如果 photonView.IsMine 為真,則意味著我們需要跟隨此實例,因此我們調(diào)用 _cameraWork.OnStartFollowing(),這會有效地使相機跟隨場景中的那個實例。
所有其他Player實例都將其 photonView.IsMine 設(shè)置為 false,因此它們各自的 _cameraWork 將不執(zhí)行任何操作。
使這項工作有效的最后一項更改:
在預制件 My Robot Kyle 的 CameraWork 組件上禁用 Follow on Start
現(xiàn)在,這實際上將跟隨玩家的邏輯移交給了將調(diào)用 _cameraWork.OnStartFollowing() 的腳本 PlayerManager,如上所述。
?5.Beams Fire 控制
觸發(fā)也遵循上面暴露的輸入原則,它只需要在 photonView.IsMine 為 true 時工作
打開腳本PlayerManager
用 if 語句包圍輸入處理調(diào)用:
然而,在測試時,我們只看到本地玩家開火。我們還需要查看另一個實例何時觸發(fā)。我們需要一種在網(wǎng)絡(luò)上同步觸發(fā)的機制。為此,我們將手動同步 IsFiring 布爾值,到目前為止,我們使用 PhotonTransformView 和 PhotonAnimatorView 為我們完成變量的所有內(nèi)部同步,我們只需要調(diào)整通過 Unity 方便地暴露給我們的內(nèi)容Inspector,但是這里我們需要的是非常針對您的游戲的,因此我們需要手動執(zhí)行此操作。
打開腳本PlayerManager
實施 IPunObservable:
在 IPunObservable.OnPhotonSerializeView 中添加以下代碼:
if (stream.IsWriting) {// We own this player: send the others our datastream.SendNext(IsFiring); } else {// Network player, receive datathis.IsFiring = (bool)stream.ReceiveNext(); }回到 Unity 編輯器,在你的資源中選擇 My Robot Kyle prefab,在 PhotonView 組件上添加一個 observe entry 并將 PlayerManager 組件拖到它上面
?如果沒有這最后一步,IPunObservable.OnPhotonSerializeView 永遠不會被調(diào)用,因為它不會被 PhotonView 觀察到。
在這個 IPunObservable.OnPhotonSerializeView 方法中,我們得到一個變量流,這是將要通過網(wǎng)絡(luò)發(fā)送的內(nèi)容,這個調(diào)用是我們讀取和寫入數(shù)據(jù)的機會。我們只能在我們是本地玩家時寫入(photonView.IsMine == true),否則我們讀取。
由于 stream 類有 helpers 知道該做什么,我們簡單地依賴 stream.isWriting 來知道在當前實例情況下預期的是什么。
如果我們需要寫入數(shù)據(jù),我們會使用 stream.SendNext() 將 IsFiring 值附加到數(shù)據(jù)流中,這是一種非常方便的方法,可以隱藏數(shù)據(jù)序列化的所有艱苦工作。如果我們需要讀取,我們使用 stream.ReceiveNext()。
6.生命值同步
好的,為了完成網(wǎng)絡(luò)更新Player功能,我們將同步生命值,以便Player的每個實例都具有正確的生命值。這與我們剛才介紹的 IsFiring 值的原理完全相同。
打開腳本播放器管理器
在 SendNext 和 ReceiveNext IsFiring 變量之后,在 IPunObservable.OnPhotonSerializeView 中,對 Health 做同樣的事情:
這就是在這個場景中同步 Health 變量所需要的全部內(nèi)容。
九、Player Instantiation
本節(jié)將介紹網(wǎng)絡(luò)上的“Player”預制實例化,并實現(xiàn)播放時適應(yīng)自動場景切換所需的各種功能。
1.實例化Player
實例化我們的“Player”預制件實際上非常容易。我們需要在剛進入房間時實例化它,我們可以依賴 GameManager Script Start() 消息,這將指示我們已經(jīng)加載了Arena,這意味著我們的設(shè)計表明我們在房間中。
打開 GameManager 腳本
在 Public Fields 區(qū)域添加以下變量:
在 Start() 方法中,添加以下內(nèi)容
if (playerPrefab == null) {Debug.LogError("<Color=Red><a>Missing</a></Color> playerPrefab Reference. Please set it up in GameObject 'Game Manager'",this); } else {Debug.LogFormat("We are Instantiating LocalPlayer from {0}", Application.loadedLevelName);// we're in a room. spawn a character for the local player. it gets synced by using PhotonNetwork.InstantiatePhotonNetwork.Instantiate(this.playerPrefab.name, new Vector3(0f,5f,0f), Quaternion.identity, 0); }這會公開一個公共字段供您引用“Player”預制件。這很方便,因為在這種特殊情況下,我們可以直接拖放到“GameManager”預制件中,而不是在每個場景中,因為“Player”預制件是一種資產(chǎn),因此引用將保持完整(而不是引用層次結(jié)構(gòu)中的游戲?qū)ο?#xff0c;預制件只能在同一場景中實例化時執(zhí)行)。
?警告:始終確保應(yīng)該通過網(wǎng)絡(luò)實例化的預制件位于 Resources 文件夾中,這是 Photon 的要求。
?
然后,在 Start() 中,我們實例化它(在檢查我們是否正確引用了“Player”預制件之后)。
請注意,我們在地板上方實例化(5 個單位以上,而玩家只有 2 個單位高)。這是在新玩家加入房間時防止碰撞的眾多方法之一,玩家可能已經(jīng)在競技場中心移動,因此它避免了突然碰撞。 “掉落”的玩家也是游戲中一個新實體的清晰指示和介紹。
然而,這對我們的情況來說還不夠,我們有一個轉(zhuǎn)折:)當其他玩家加入時,將加載不同的場景,我們希望保持一致性,而不是僅僅因為其中一個玩家離開而破壞現(xiàn)有玩家。所以我們需要告訴 Unity 不要銷毀我們創(chuàng)建的實例,這反過來意味著我們現(xiàn)在需要檢查在加載場景時是否需要實例化。
2.跟蹤玩家實例
打開 PlayerManager 腳本
在“Public Fields”區(qū)域中,添加以下內(nèi)容:
?在 Awake() 方法中,添加以下內(nèi)容:
// #Important // used in GameManager.cs: we keep track of the localPlayer instance to prevent instantiation when levels are synchronized if (photonView.IsMine) {PlayerManager.LocalPlayerInstance = this.gameObject; } // #Critical // we flag as don't destroy on load so that instance survives level synchronization, thus giving a seamless experience when levels load. DontDestroyOnLoad(this.gameObject);通過這些修改,我們可以在 GameManager 腳本中執(zhí)行檢查以僅在必要時實例化。
打開 GameManager 腳本
用 if 條件包圍實例化調(diào)用:
有了這個,我們現(xiàn)在只在 PlayerManager 沒有對 localPlayer 的現(xiàn)有實例的引用時實例化。
3.在競技場外管理Player位置?
我們還有一件事需要注意。競技場的大小根據(jù)玩家的數(shù)量而變化,這意味著如果一個玩家離開而其他玩家在當前競技場大小的邊界附近,他們會發(fā)現(xiàn)自己在較小的競技場之外它會加載,我們需要考慮到這一點,在這種情況下只需將玩家重新定位回競技場的中心。這是您的游戲玩法和關(guān)卡設(shè)計中的一個問題。
目前增加了復雜性,因為 Unity 改進了“場景管理”并且 Unity 5.4 棄用了一些回調(diào),這使得創(chuàng)建適用于所有 Unity 版本(從 Unity 5.3.7 到最新版本)的代碼稍微復雜一些。所以我們需要基于 Unity 版本的不同代碼。它與 Photon Unity Networking 無關(guān),但掌握它對于您的項目在更新中生存很重要。
打開 PlayerManager 腳本
在“私有方法”區(qū)域中添加一個新方法:
在 Start() 方法的末尾,添加以下代碼
#if UNITY_5_4_OR_NEWER // Unity 5.4 has a new scene management. register a method to call CalledOnLevelWasLoaded. UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded; #endif在“MonoBehaviour Callbacks”區(qū)域中添加以下兩個方法
#if !UNITY_5_4_OR_NEWER /// <summary>See CalledOnLevelWasLoaded. Outdated in Unity 5.4.</summary> void OnLevelWasLoaded(int level) {this.CalledOnLevelWasLoaded(level); } #endifvoid CalledOnLevelWasLoaded(int level) {// check if we are outside the Arena and if it's the case, spawn around the center of the arena in a safe zoneif (!Physics.Raycast(transform.position, -Vector3.up, 5f)){transform.position = new Vector3(0f, 5f, 0f);} }重寫 OnDisable 方法如下:
#if UNITY_5_4_OR_NEWER public override void OnDisable() {// Always call the base to remove callbacksbase.OnDisable ();UnityEngine.SceneManagement.SceneManager.sceneLoaded -= OnSceneLoaded; } #endif這段新代碼所做的是觀察正在加載的關(guān)卡,并向下投射當前玩家的位置以查看我們是否擊中了任何東西。如果我們不這樣做,這意味著我們不在競技場的地面之上,我們需要重新定位回到中心,就像我們第一次進入房間時一樣。
如果您使用的 Unity 版本低于 Unity 5.4,我們將使用 Unity 的回調(diào) OnLevelWasLoaded。如果您使用的是 Unity 5.4 或更高版本,則 OnLevelWasLoaded 不再可用,您必須使用新的 SceneManagement 系統(tǒng)。最后,為了避免重復代碼,我們只需要一個 CalledOnLevelWasLoaded 方法,該方法將從 OnLevelWasLoaded 或 SceneManager.sceneLoaded 回調(diào)中調(diào)用。
十、Player UI Prefab
本節(jié)將指導您創(chuàng)建 Player UI 系統(tǒng)。我們需要顯示玩家的姓名及其當前健康狀況。我們還需要管理 UI 位置以跟隨周圍的玩家。
本節(jié)與網(wǎng)絡(luò)本身無關(guān)。但是,它提出了一些非常重要的設(shè)計模式,以提供一些圍繞網(wǎng)絡(luò)及其在開發(fā)中引入的約束的高級功能。
所以,UI 不會聯(lián)網(wǎng),只是因為我們不需要,還有很多其他方法可以解決這個問題并避免占用流量。這總是值得努力的事情,如果你能擺脫一個不聯(lián)網(wǎng)的功能,那就太好了。
現(xiàn)在的合理問題是:我們?nèi)绾螢槊總€聯(lián)網(wǎng)Player提供一個 UI?
我們將擁有一個帶有專用 PlayerUI 腳本的 UI Prefab。我們的 PlayerManager 腳本將保存此 UI Prefab 的引用,并在 PlayerManager 啟動時簡單地實例化此 UI Prefab,并告訴該預制件跟隨那個玩家。
1.創(chuàng)建 UI 預制件
?2.PlayerUI 腳本基礎(chǔ)
創(chuàng)建一個新的 C# 腳本,并將其命名為 PlayerUI
這是基本的腳本結(jié)構(gòu),相應(yīng)地編輯和保存 PlayerUI 腳本:
現(xiàn)在讓我們創(chuàng)建預制件本身。
3.PlayerUI 與 Player 綁定
PlayerUI 腳本需要知道它代表哪個玩家,其中一個原因是:能夠顯示其健康狀況和名稱,讓我們創(chuàng)建一個公共方法來實現(xiàn)此綁定。
打開腳本 PlayerUI
在“私有字段”區(qū)域添加私有屬性:
我們需要提前考慮,我們會定期查看健康狀況,因此緩存 PlayerManager 的引用以提高效率是有意義的。
?在“公共方法”區(qū)域添加這個公共方法:
public void SetTarget(PlayerManager _target) {if (_target == null){Debug.LogError("<Color=Red><a>Missing</a></Color> PlayMakerManager target for PlayerUI.SetTarget.", this);return;}// Cache references for efficiencytarget = _target;if (playerNameText != null){playerNameText.text = target.photonView.Owner.NickName;} }在“MonoBehaviour Callbacks”區(qū)域中添加此方法:
void Update() {// Reflect the Player Healthif (playerHealthSlider != null){playerHealthSlider.value = target.Health;} }有了這個,我們就有了 UI 來顯示目標玩家的名字和生命值。
4.實例化
好的,所以我們已經(jīng)知道我們想要如何實例化這個預制件,每次我們實例化一個玩家預制件。最好的方法是在初始化期間在 PlayerManager 中執(zhí)行此操作。
?打開腳本PlayerManager
添加一個公共字段來保存對 Player UI 預制件的引用,如下所示:
在 Start() 方法中添加此代碼:
if (PlayerUiPrefab != null) {GameObject _uiGo = Instantiate(PlayerUiPrefab);_uiGo.SendMessage ("SetTarget", this, SendMessageOptions.RequireReceiver); } else {Debug.LogWarning("<Color=Red><a>Missing</a></Color> PlayerUiPrefab reference on player Prefab.", this); }所有這些都是標準的 Unity 編碼。但是請注意,我們正在向剛剛創(chuàng)建的實例發(fā)送一條消息。我們需要一個接收器,這意味著如果 SetTarget 沒有找到響應(yīng)它的組件,我們將收到警報。另一種方法是從實例中獲取 PlayerUI 組件,然后直接調(diào)用 SetTarget。通常建議直接使用組件,但知道您可以通過多種方式實現(xiàn)相同的目的也很好。
然而這還遠遠不夠,我們需要處理玩家的刪除,我們當然不希望場景中到處都是孤立的 UI 實例,所以我們需要在發(fā)現(xiàn)目標時銷毀 UI 實例它已經(jīng)被分配了。
打開 PlayerUI 腳本
將此添加到 Update() 函數(shù):
保存 PlayerUI 腳本這段代碼雖然簡單,但實際上非常方便。由于 Photon 刪除聯(lián)網(wǎng)實例的方式,如果目標引用為空,UI 實例更容易銷毀自身。這樣就避免了很多潛在的問題,而且非常安全,不管是什么原因丟失了一個target,相關(guān)的UI也會自動銷毀,非常方便快捷。但是等一下……當一個新關(guān)卡被加載時,UI 被破壞但我們的播放器仍然存在……所以我們也需要在我們知道一個關(guān)卡被加載時實例化它,讓我們這樣做:
打開腳本PlayerManager
在 CalledOnLevelWasLoaded() 方法中添加此代碼:
請注意,有更復雜/更強大的方法來處理這個問題,UI 可以用單例來制作,但它很快就會變得復雜,因為其他加入和離開房間的玩家也需要處理他們的 UI。在我們的實現(xiàn)中,這是直截了當?shù)?#xff0c;代價是重復我們實例化 UI 預制件的位置。作為一個簡單的練習,您可以創(chuàng)建一個私有方法來實例化并發(fā)送“SetTarget”消息,然后從不同的地方調(diào)用該方法而不是復制代碼。
5.Parenting To UI Canvas
?Unity UI 系統(tǒng)的一個非常重要的約束是任何 UI 元素都必須放置在 Canvas GameObject 中,因此我們需要在實例化 PlayerUI Prefab 時處理這個問題,我們將在 PlayerUI 腳本的初始化期間執(zhí)行此操作。
打開腳本 PlayerUI
在“MonoBehaviour Callbacks”區(qū)域內(nèi)添加此方法:
保存 PlayerUI 腳本 為什么要用蠻力以這種方式找到 Canvas?因為當場景要加載和卸載時,我們的 Prefab 也是如此,而 Canvas 每次都會不同。為了避免更復雜的代碼結(jié)構(gòu),我們將采用最快的方法。但是真的不推薦使用“Find”,因為這是一個緩慢的操作。這超出了本教程的范圍,無法實現(xiàn)對此類情況的更復雜處理,但是當您對 Unity 和腳本感到滿意時,這是一個很好的練習,可以找到編碼更好地管理需要加載的 Canvas 元素引用的方法并考慮卸載。
6.跟隨目標玩家?
這是一個有趣的部分,我們需要讓玩家 UI 在屏幕上跟隨玩家目標。這意味著要解決幾個小問題:
UI 是一個 2D 元素,Player是一個 3D 對象。在這種情況下我們?nèi)绾纹ヅ湮恢?#xff1f;
我們不希望 UI 稍微高于Player,我們?nèi)绾卧谄聊簧蠌牟シ牌魑恢闷?#xff1f;
?打開 PlayerUI 腳本
在“公共字段”區(qū)域內(nèi)添加此公共屬性:
將這四個字段添加到“私有字段”區(qū)域:
float characterControllerHeight = 0f; Transform targetTransform; Renderer targetRenderer; CanvasGroup _canvasGroup; Vector3 targetPosition;將其添加到 Awake Method 區(qū)域內(nèi):
_canvasGroup = this.GetComponent<CanvasGroup>();在設(shè)置 _target 后將以下代碼附加到 SetTarget() 方法:
targetTransform = this.target.GetComponent<Transform>(); targetRenderer = this.target.GetComponent<Renderer>(); CharacterController characterController = _target.GetComponent<CharacterController> (); // Get data from the Player that won't change during the lifetime of this Component if (characterController != null) {characterControllerHeight = characterController.height; }我們知道我們的Player基于具有高度屬性的 CharacterController,我們需要它來對Player上方的 UI 元素進行適當?shù)钠啤?/p>
在“MonoBehaviour Callbacks”區(qū)域添加這個公共方法 :
void LateUpdate() {// Do not show the UI if we are not visible to the camera, thus avoid potential bugs with seeing the UI, but not the player itself.if (targetRenderer!=null){this._canvasGroup.alpha = targetRenderer.isVisible ? 1f : 0f;}// #Critical// Follow the Target GameObject on screen.if (targetTransform != null){targetPosition = targetTransform.position;targetPosition.y += characterControllerHeight;this.transform.position = Camera.main.WorldToScreenPoint (targetPosition) + screenOffset;} }?因此,將 2D 位置與 3D 位置匹配的技巧是使用攝像機的 WorldToScreenPoint 函數(shù),由于我們的游戲中只有一個,我們可以依賴于訪問主攝像機,這是 Unity 場景的默認設(shè)置。
請注意我們是如何分幾步設(shè)置偏移量的:首先我們獲取目標的實際位置,然后添加 characterControllerHeight,最后,在推斷出 Player 頂部的屏幕位置后,我們添加屏幕偏移量。
?
Package Demos
1.Asteroids
小行星演示是將 Unity 的 NetworkMeteoroid 演示從 uNet 移植到 PUN 2 的結(jié)果。在這個演示中,1 到 8 個玩家可以競爭摧毀小行星。得分最多的玩家贏得游戲。
如果您想了解更多關(guān)于從 uNet 到 PUN 的移植過程,您可以查看演示的文檔頁面。
演示位置:/Photon/PhotonUnityNetworking/Demos/DemoAsteroids/
?
(1)從 UNet 移植到 PUN
此頁面基于 Unity 的 NetworkMeteoroid 演示描述了從 uNet 到 PUN 的移植過程,您可以通過訪問資產(chǎn)商店的鏈接查看該演示。該頁面顯示了該過程的某些方面,這些方面非常容易處理,而其他一些方面則處理起來稍微復雜一些,以便最終獲得令人信服的結(jié)果。因此,該頁面被細分為不同的部分,以涵蓋所有必要和重要的步驟。這些步驟(或多或少)按難度升序排序。但是,成功移植游戲并不一定要遵循此順序。
還請記住,以下描述不是您可以用來將現(xiàn)有應(yīng)用程序從 uNet 移植到 PUN 的通用方法,但它應(yīng)該讓您了解哪些步驟是必要的以及您可能必須處理哪些問題.
(2)重新導入資產(chǎn),重建預制件和場景
當開始將現(xiàn)有項目從 uNet 移植到 PUN 時,您基本上可以使用現(xiàn)有項目并將所有 uNET 的網(wǎng)絡(luò)邏輯替換為 PUN 的網(wǎng)絡(luò)邏輯。由于我們正在處理一個我們不熟悉的演示,因此我們決定從一個新項目開始并重建演示的大部分內(nèi)容。這樣我們也可以從一開始就展示整個移植過程。實際上沒有必要這樣做,但這樣我們也能夠看到原始演示和我們移植的演示之間的所有差異 - 至少在源代碼中。如果您認為這會導致比僅使用現(xiàn)有的 NetworkMeteoroid 演示更多的工作,那么您可能是對的,但否則我們也必須做很多特別次要的工作。因此,我們可以預計,就此演示而言,兩種移植過程的工作量或多或少是相同的。與項目的復雜性相關(guān),這種體驗肯定會改變。
首先,我們在 Unity 中創(chuàng)建了一個新項目,并開始(或多或少)重新創(chuàng)建原始演示的文件夾結(jié)構(gòu),并重新導入必要的資產(chǎn)(模型和紋理)。完成后,我們開始重建其他所需的資產(chǎn),例如材料和預制件以及場景。在特別重建“大廳”場景時,我們確保沒有重新創(chuàng)建以后不需要的部分,例如專用服務(wù)器選項。我們還確保它的外觀和感覺適合 PUN 的演示中心。
(3)對游戲邏輯進行細微調(diào)整
對應(yīng)用程序的源代碼進行修改時,您需要一個起點。在這種情況下,我們決定從 NetworkMeteoroid 演示的游戲邏輯開始 - 特別是因為沒有那么多事情要做。當然,我們不能一對一地重用整個代碼,但我們可以使用演示的源代碼作為模式,并簡單地對其進行修改。因此,我們可以復制整個代碼并在之后修改它,或者從一開始就應(yīng)用修改重新編寫它。對于這個演示,我們結(jié)合使用了這兩種方式。最后,游戲邏輯本身很可能與原始演示中的相同,除了一些特別適用于網(wǎng)絡(luò)相關(guān)源代碼的小改動。
這里的一個例子是小行星在原始演示中是如何產(chǎn)生的。在我們的修改版本中,它基本上以相同的方式工作(使用一個“游戲管理器”和一個只要游戲運行就運行的協(xié)程),只是對網(wǎng)絡(luò)相關(guān)代碼進行了一些小的調(diào)整。在這個具體的例子中,我們只是用 PUN 的房間對象實例化調(diào)用 PhotonNetwork.InstantiateRoomObject(...) 替換了 uNet 的實例化調(diào)用 NetworkServer.Spawn(...)。使用此調(diào)用,我們可以添加 InstantiationData,例如,我們使用它來共享有關(guān)小行星剛體的其他詳細信息。這還有一個好處,就是我們不必使用單獨的 RPC 或 RaiseEvent 調(diào)用來同步此類信息。
但也有部分源代碼根本沒有修改。例如,處理玩家輸入的方式與原始演示中的方式完全相同,因為它已經(jīng)運行良好,根本不需要任何修改。
(4)對網(wǎng)絡(luò)邏輯進行重大調(diào)整
在這部分,事情(終于)變得有趣了,因為我們必須對演示的源代碼進行一些重大修改,以便用 PUN 的網(wǎng)絡(luò)邏輯替換所有 uNet 的網(wǎng)絡(luò)邏輯。提醒:遺憾的是,在嘗試將現(xiàn)有應(yīng)用程序從 uNet 移植到 PUN 時,沒有可以遵循的通用方法。所以你不能一般地說某個 uNET 屬性(例如 [ClientRpc])可以一直映射到某個 PUN 屬性(在這個例子中是 [PunRPC]),因為在PUN 中根本不存在網(wǎng)絡(luò)邏輯或?qū)傩员旧怼_@意味著您必須考慮對每一行代碼的網(wǎng)絡(luò)相關(guān)源代碼的哪些段應(yīng)用了哪些修改。
由于我們不出于此演示的目的使用服務(wù)器端邏輯,因此我們還必須就如何處理模擬做出另一個重要決定,因為它由原始演示中的服務(wù)器控制。在沒有自定義服務(wù)器端邏輯的情況下使用 PUN 時,我們唯一的可能是使用所有客戶端或僅使用一個客戶端來處理模擬。在我們的例子中,我們選擇了第二個選項,并決定使用 MasterClient 來運行和控制模擬。這意味著它是唯一允許實例化小行星并處理與玩家宇宙飛船發(fā)射的子彈的碰撞檢測的客戶端。此外,這些小行星被實例化為場景對象,其好處是如果 MasterClient 在游戲運行時斷開連接,它們不會被破壞。相反,只要有另一個客戶端可以接管這個角色,模擬的控制就會傳遞給新的 MasterClient。
網(wǎng)絡(luò)邏輯的另一個重要方面是前面提到的小行星和玩家飛船的同步。為了獲得令人信服的結(jié)果,我們決定實現(xiàn)一個自定義的 OnPhotonSerializeView 函數(shù),該函數(shù)處理所有必要數(shù)據(jù)的發(fā)送和接收。這些包括剛體的位置、旋轉(zhuǎn)和速度。隨著對它的進一步修改,這個自定義解決方案后來變成了新的 PhotonRigidbodyView 組件。
(5)通過添加滯后補償解決同步問題
在設(shè)置模擬并在多個客戶端上運行它之后,我們很快發(fā)現(xiàn)我們有明顯的同步問題,當至少有兩個游戲窗口彼此相鄰運行時,這會導致視覺上令人失望的結(jié)果。一個例子是宇宙飛船在兩個不同屏幕上的位置位移。這是由滯后引起的,并導致整個同步的進一步問題:在某些情況下,玩家的宇宙飛船在一個客戶端的視圖中撞上了一顆小行星,但在另一個客戶端的視圖中卻沒有。這進一步迫使 MasterClient(記住他控制模擬和碰撞檢測)有時會引爆另一個玩家的宇宙飛船,因為他的物理系統(tǒng)檢測到碰撞,而這在其他客戶端上根本不可見。這些問題對于任何多人游戲的玩法來說都是致命的。
為了擺脫這些同步問題,我們決定為小行星、宇宙飛船和它們發(fā)射的子彈添加滯后補償。在我們的例子中,滯后補償意味著接收到同步對象信息的客戶端試圖在先前接收到的信息的幫助下計算出更準確和更新的數(shù)據(jù)。一個例子:每當客戶端收到另一艘宇宙飛船的信息時,他使用接收到的位置和速度值以及消息的時間戳和他當前的時間戳來計算另一艘宇宙飛船的最新位置。計算出這個更準確的位置后,我們使用 Unity 的 FixedUpdate 函數(shù)實際將宇宙飛船一步一步地移近它的“真實”位置——至少移動到我們認為這是物體“真實”位置的位置。為清楚起見,您可以查看下面的代碼片段,其中顯示了上述功能的實現(xiàn)。
Owner只發(fā)送飛船的位置、旋轉(zhuǎn)和速度等重要信息。接收者使用此信息更新其本地存儲的值并對位置應(yīng)用滯后補償......?
public void FixedUpdate() {if (!photonView.IsMine){rigidbody.position = Vector3.MoveTowards(rigidbody.position, networkPosition, Time.fixedDeltaTime);rigidbody.rotation = Quaternion.RotateTowards(rigidbody.rotation, networkRotation, Time.fixedDeltaTime * 100.0f);} }?...在使用 FixedUpdate 函數(shù)中本地存儲的數(shù)據(jù)更新遠程對象之前。
請注意:上面顯示的代碼片段并不代表 PUN 包中的實際 PhotonRigidbodyView 組件。
最后,為移植的演示添加滯后補償使其變得更好。這包括令人信服的視覺表現(xiàn)以及具有改進的網(wǎng)絡(luò)同步的整體令人滿意的游戲玩法。
?2.Procedural
程序演示展示了在使用 Photon Cloud 時如何處理程序生成的世界。因此,該演示的重點是生成世界和在多個客戶端上同步應(yīng)用到它的修改。您可以在演示的文檔頁面上相關(guān)信息。
演示位置:/Photon/PhotonUnityNetworking/Demos/DemoProcedural/
?
(1)介紹
在此演示中,我們想演示如何使用 Photon Cloud 處理程序生成的世界和應(yīng)用于它們的修改。因此 PUN 2 包包含一個演示,我們可以在其中創(chuàng)建一個由立方體/塊組成的世界。對于創(chuàng)建過程本身,我們有不同的選項,我們可以選擇這些選項來創(chuàng)建不同的世界。
此文檔頁面描述了演示和世界生成的工作原理以及應(yīng)用的修改如何在所有客戶端之間同步。它還顯示了在使用 Photon Cloud 和一般情況下創(chuàng)建程序生成的世界時所犯的一些最常見的錯誤。
(2)這個演示是如何工作的
當連接到房間時,MasterClient 可以使用控制面板控制確定性世界生成器。下一章將介紹控制面板及其相關(guān)選項。當世界生成器運行時,它會創(chuàng)建多個集群,每個集群包含多個構(gòu)成世界的塊。將世界劃分為不同的集群,有助于稍后在對其應(yīng)用修改時對其進行同步。同步的工作原理也是本文檔頁面描述的一部分。要對世界應(yīng)用修改,任何客戶端都可以左鍵單擊某個塊以降低其高度或右鍵單擊以提高其高度。
您可能想知道現(xiàn)在是否降低或提高塊的高度。我們決定通過在 y 軸上使用塊的比例來描述生成世界的不同高度級別。這有一些優(yōu)點:首先是我們在場景中沒有那么多的游戲?qū)ο?#xff0c;這在性能方面是好的——Unity(以及其他引擎)顯然無法處理幾乎無限數(shù)量的對象。所以為了這個演示的目的,我們對我們的實施沒有問題。另一方面是我們必須以某種方式存儲應(yīng)用的修改,以便所有客戶端都可以使用它們。由于我們在 Photon Cloud 上沒有可用的自定義服務(wù)器端邏輯(除非我們使用企業(yè)云),我們決定使用自定義房間屬性來存儲應(yīng)用的修改。最后一點是,實施的解決方案比更復雜且可能“生產(chǎn)就緒”的解決方案更容易理解。然而,這個演示仍然展示了我們在開發(fā)程序生成游戲時使用 Photon Cloud 的可能性。
(3)生成一個世界
啟動演示時,您可能會注意到游戲窗口左上角的控制面板。該面板只能由 MasterClient 使用,因為它控制著世界生成器。在這個演示中,我們有四個選項可以影響世界生成器。
其中一個選項是種子,在此演示中用數(shù)字表示。種子至少需要一位數(shù)字,最多可以有十位數(shù)字,結(jié)果是從 0 到 9,999,999,999 的區(qū)間。另一個選項描述了世界的整體大小。對于這個演示,我們可以創(chuàng)建尺寸從 16 x 16 塊到 128 x 128 塊的世界。請注意,就此演示而言,生成的世界不是無限的。第三個選項描述了集群的大小。一個簇最多可以包含 64 個塊。創(chuàng)建多少集群主要取決于這個值和前面提到的世界大小。擁有包含大量塊的集群將加速生成世界(由于我們的實施)。另一個選項描述了對生成關(guān)卡的外觀有影響的世界類型。該演示包含三個不同的選項,主要影響生成過程中塊的最大高度。
每當 MasterClient 單擊控制面板上的確認按鈕時,所選選項將存儲在自定義房間屬性中,以便同一房間中的所有客戶端都可以使用它們。每當客戶端收到這些更新時,他將在本地(重新)運行他的世界生成器以創(chuàng)建新世界。由于這些選項是同步的并且沒有使用任何隨機功能,因此每個客戶端將生成相同的世界。
為了生成一個世界,我們使用 Simplex Noise 算法,這是一個噪聲函數(shù)。為此,我們將每個實例化塊的 x 和 z 坐標傳遞給它的 CalcPixel2D 函數(shù)。除了 2D 功能外,Benjamin Ward 使用的 Simplex Noise 實現(xiàn)也提供了 1D 和 3D 功能。這些功能中的每一個都有兩個重要的共同點。第一個是,這些函數(shù)總是返回一個介于 -1.0f 和 1.0f 之間的值。我們使用這個值來計算每個塊的高度(y 尺度)。高度主要取決于這個值和使用的世界類型。第二個方面是,只要輸入保持不變,該函數(shù)的輸出就始終相同。換句話說:更改輸入?yún)?shù)(例如種子)時會得到不同的結(jié)果。這主要是我們必須同步使用的種子和世界生成器的其他選項的原因。
?(4)同步修改
如前所述,應(yīng)用于世界的修改存儲在自定義房間屬性中。這有一個主要好處:每個客戶都將自動收到最新的自定義房間屬性,包括所有修改;我們只需要在客戶端處理它們。這也讓后來加入的客戶更容易,因為我們不必考慮如何與他們分享世界的最新狀態(tài),它只是“自動”發(fā)生。
為了將修改后的世界數(shù)據(jù)存儲在自定義房間屬性中,每個集群都會為其添加一個唯一標識符和一個字典。字典本身包含某些塊的唯一標識符及其相關(guān)高度(y 尺度)。這里的重要方面是,只有修改過的塊才會存儲在自定義房間屬性中。未修改的塊仍然由之前解釋的世界生成器設(shè)置描述,不需要存儲在這里。
注意:我們在此演示中使用字典,因為它比更復雜的解決方案更易于使用且更容易理解。當然還有其他可能的表示來描述對世界所做的修改。
就本演示而言,這非常有效。但是,如果您想要創(chuàng)建一個具有“無限”世界的更大規(guī)模的游戲,您必須考慮將其托管在企業(yè)云或自托管的 Photon 服務(wù)器上。這兩個選項提供了通過使用插件或通過自己實現(xiàn)服務(wù)器應(yīng)用程序來運行自定義服務(wù)器端邏輯的可能性。這對您的游戲來說可能是必不可少的,因為您會繞過一些限制,例如在加入游戲時的最大世界大小或加載時間方面。
(5)最常見的錯誤
本章介紹了在開發(fā)程序生成的網(wǎng)絡(luò)游戲時可能犯的一些最常見的錯誤。
你可能犯的第一個錯誤是試圖“網(wǎng)絡(luò)實例化”一切。假設(shè)您想創(chuàng)建一個由幾堵墻組成的迷宮。就 PUN 而言,一種簡單的方法是為這面墻創(chuàng)建一個預制件,將一個 PhotonView 組件附加到它上面,然后在使用 Unity Editor 時將其直接放置在場景中,或者在運行時使用 PhotonNetwork.Instantiate 或 PhotonNetwork 對其進行實例化。實例化房間對象。這實際上可能適用于一定數(shù)量的對象,但是不推薦這樣做的一個原因是每個客戶端的 ViewID 有限制。此限制適用于用戶以及場景對象。由于此限制,生成的迷宮也可能受到其大小或復雜性的限制。
另一個常見錯誤是在每個客戶端上分別使用來自 Unity 或 System 命名空間的 Random 類。 Random 類的問題是,只要不使用相同的種子,就會在不同的機器上得到不同的結(jié)果。結(jié)果是,不同的客戶端會生成不同的世界,這在多人游戲方面確實很糟糕。如果您現(xiàn)在仍然考慮將 Random 類與同步種子一起使用,還有另一個主要缺點:您很可能不會獲得視覺上令人滿意的結(jié)果。正如所描述的,噪聲算法創(chuàng)建了某種高度圖,它具有 - 取決于生成它的設(shè)置 - 在不同高度級別之間的過渡。使用 Random 類時,很可能在不同高度級別之間不會有任何良好的過渡。取而代之的是,會有很多拼湊而成的結(jié)果在視覺上令人失望。
由于我們已經(jīng)看到了一種通過使用自定義房間屬性來存儲一些數(shù)據(jù)的方法,您可能會考慮使用它們來存儲整個生成的世界。當世界變得太大時,這可能會在一定程度上發(fā)揮作用。然而,問題是,加入房間需要很長時間,因為必須將大量數(shù)據(jù)傳輸?shù)娇蛻舳?。在這種情況下的解決方案是添加服務(wù)器端邏輯,以便服務(wù)器可以決定哪些數(shù)據(jù)需要發(fā)送給客戶端。因此,服務(wù)器不會立即發(fā)送整個世界狀態(tài),而是只發(fā)送世界的那些部分,客戶端當前需要并將在之后按需更新他。
?3.老虎機賽車
?在 Slot Racer 演示中,1 到 4 名玩家可以在賽道上駕駛他們的老虎機。演示沒有使用“經(jīng)典”位置同步,而是使用驅(qū)動距離來同步軌道上玩家的老虎機。
演示位置:/Photon/PhotonUnityNetworking/Demos/DemoSlotRacer/
?4.PUN Cockpit
Cockpit 演示提供了區(qū)域 ping、連接過程、房間創(chuàng)建和房間管理的可視化方法,并完美嵌入到 Slot Racer 演示中。要使用它,您必須將 PunCockpit-Scene 和 SlotCar-Scene 添加到構(gòu)建設(shè)置中并啟動 Slot Racer 演示。
演示位置:/Photon/PhotonUnityNetworking/Demos/PunCockpit/
?
?5.LoadBalancing
?LoadBalancing 演示展示了如何直接使用實時 API。
演示位置:/Photon/PhotonRealtime/Demos/DemoLoadBalancing/
?6.Chat
從 PUN Classic 接管的聊天演示通過使用聊天 API 顯示了一個簡單的聊天室。
演示位置:/Photon/PhotonChat/Demos/DemoChat/
總結(jié)
以上是生活随笔為你收集整理的(二)PUN 2基本教程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2.垃圾收集器与内存分配策略
- 下一篇: 插上U盘显示这个错误ERROR