【持续更新】Pun多人在线游戏开发教程
一、PUN介紹
1.入門
Photon Unity Networking(首字母縮寫 PUN)是一個 Unity 多人游戲插件包。它提供了身份驗證選項、匹配,以及快速、可靠的通過我們的 Photon 后端實現的游戲內通信。
PUN 輸出幾乎所有 Unity 支持的平臺,且有兩種選項:
注意:對于 Unity 5,兩個 PUN 插件包都含相同的文件。你可以買 PUN+ 來獲得 60 個月的 100 CCU 1 ,但客戶端上仍使用 PUN Free。
CCU,即 concurrent user 的縮寫,意為同時在線用戶,100CCU 就是最多容納 100 個同時在線用戶。
2.連接
PhotonNetwork.ConnectUsingSettings("v4.2");
上面的代碼是你需要連接并開始使用 Photon 功能的所有代碼。
ConnectUsingSettings 設置你的客戶端的游戲版本并使用一個由 PUN 設置向導寫入的配置文件,該配置文件保存在 PhotonServerSettings 里面。
3.匹配
//加入名為"someRoom"的房間
PhotonNetwork.JoinRoom("someRoom");
//如果沒有開放的游戲就會失敗。錯誤回調: OnPhotonJoinRoomFailed
//嘗試加入任何隨機游戲:
PhotonNetwork.JoinRandomRoom();
//如果沒有開放的游戲就會失敗。錯誤回調: OnPhotonRandomJoinFailed
//創建名為"MyMatch"的房間。
PhotonNetwork.CreateRoom("MyMatch");
//如果名為"MyMatch"的房間已存在就會失敗并調用:OnPhotonCreateRoomFailed
好朋友常常想要一起玩游戲。如果他們可以交流(例如 使用 Photon Chat,Facebook), 他們可以瞎編一個房間名并使用 JoinOrCreateRoom 方法。因為他們知道房間的名字,他們可以創建為他人不可見,像這樣:
RoomOptions roomOptions = new RoomOptions() { isVisible = false,maxPlayers = 4 };
PhotonNetwork.JoinOrCreateRoom(nameEveryFriendKnows, roomOptions, 5TypedLobby.Default);
?使用 JoinOrCreateRoom 方法,如果房間不存在就會創建該房間。如果房間滿了,OnPhotonJoinRoomFailed 會被調用 (如果你在某個地方實現了這個回調函數)。
4.游戲
GameObjects 可以被實例化為"networked GameObjects"? 。它們會有一個可以被識別的 PhotonView 組件和一個所有者(或控制者)。所有者會更新其他人。持續更新可以通過拖拽一個腳本到一個 PhotonView 的 Observed 字段被發送。需要更新的腳本必須實現 OnPhotonSerializeView 像這樣:
// 在一個"observed"? 腳本里:
public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
if (stream.isWriting)
{
Vector3 pos = transform.localPosition;
stream.Serialize(ref pos);
}
else
{
Vector3 pos = Vector3.zero;
stream.Serialize(ref pos); // pos 被填充。必須在某個地方使用
}
"networked GameObjects",網絡游戲對象,會在網絡上進行同步。
"observed",被觀察的。
客戶端可以為不見用的操作執行 Remote Procedure Calls :
// 定義一個可以被其他客戶端調用的方法:
[PunRPC]
public void OnAwakeRPC(byte myParameter)
{
//Debug.Log("RPC: 'OnAwakeRPC' Parameter: " + myParameter + " PhotonView: " + this.photonView);
}
// [...]
// 在別的某個地方調用該 RPC
photonView.RPC("OnAwakeRPC", PhotonTargets.All, (byte)1);
獨立于 GameObjects, 你也可以發送你自己的事件:
PhotonNetwork.RaiseEvent((byte)eventCode, (object)eventContent, (bool)sendReliable, (RaiseEventOptions)options)
Remote Procedure Calls,首字母縮寫為 RPCs,意為遠程過程調用。
二、初始設置
Photon Unity Networking (PUN)真的很容易設置。把 PUN 導入到一個新的項目中,然后 PUN 設置向導就會彈出來,如圖 0-1 所示。通過輸入一個郵箱地址來注冊一個新的(免費) Photon Cloud 帳號,或者復制粘貼一個已有的AppId 到該字段里。打完收工。如果你想要自己托管一個 Photon 服務器,點擊"skip",然后像如下描述的
那樣編輯 PhotonServerSettings 。
Photon Cloud 在后面的章節會有詳細解釋,你可以理解為云服務。
要連接,你只需在你的代碼中調用 PhotonNetwork.ConnectUsingSettings() 。如果你需要更多的控制,詳見下面的 Connect Manually 。
1.Photon 服務器設置
設置向導會添加一個 PhotonServerSettings 文件到你的項目,用來保存配置。如圖 0-2 所示,這也是去編輯服務器設置的地方。
你可以設置 AppId、Photon Cloud Region 和更多的。你的客戶端的 GameVersion? 是在代碼里被設置的。要選擇的最重要的選項是托管類型。
托管類型:通過 Hosting Type 你選擇處理你游戲的服務器和其他配置。Photon Cloud 和 Best Region 都涉及到我們管理的云服務。您可以選擇特定區域,
也可以讓客戶選擇最佳 ping 區域。如果你想在別的地方運行 Photon 服務器,選擇 Self Hosted 。安裝程序如下。或者,你的客戶可以在脫機模式。
最佳托管區域:最佳區域模式將在應用首次啟動的時候 ping 所有已知區域。由于這需要一點時間,結果被存儲在 PlayerPrefs。這會加快連接時間。你可以設置哪些區域可以忽略。在更少的區域分發客戶端會導致剩余區域的玩家更多。這在游戲流行之前是有益的。使用 PhotonNetwork.OverrideBestCloudServer() 來定義要使用的另一個區域。
自托管:如果你要自己托管一個 Photon 服務器,你應在 PhotonServerSettings 里面設置好它的地址和端口。當這些都被正確設置了,你可以在你的代碼里調用
PhotonNetwork.ConnectUsingSettings() 。確保您的客戶端可以到達輸入的地址。它可以是一個公共的、靜態的 IP 地址、主機名或在你的客戶端也使用的網絡中的任何地址。端口取決于所選協議,所以請確保這兩個字段匹配。清除該字段會將其重置為默認端口。
如果你為 iOS 開發游戲可以考慮閱讀 PUN and IPv6 和 how to setup Photon Server for IPv6。
協議:這里默認是(可靠的)UDP,但 Photon 還支持使用 TCP 以及將允許一個可靠的 HTTP 協議。我們建議你堅持 UDP。PUN+不支持 TCP。WebGL 導出只能使用WebSockets。
客戶端設置:客戶端設置部分包含了每個項目應設置的幾個選項。當你勾選 Auto-Join Lobby 時,PUN 將在連接(或離開房間)時自動加入默認大廳。Photon 的大廳提供當前房間的列表,這樣玩家可以選擇一個加入。這個默認是關閉的,因為更好的選擇是使用隨機匹配,就像所有的演示案例中使用的那樣。啟用 Enable Lobby Stats 來從服務器獲取大廳統計信息。如果游戲使用多個大廳,并且你想要向玩家展示每一個活動,則這個統計信息會很有用。每個大廳,你都可以獲取這些屬性: name、type、room 和 playercount。詳見PhotonNetworking.LobbyStatistics !這些設置在 PUN v1.60 版本引入。
遠程過程調用列表:Remote Procedure Calls 使你可以在一個房間里調用所有客戶端上的方法。PUN將這些方法的列表保存在 PhotonServerSettings。對于最初的設置,這是不相關的。詳見 Remote Procedure Calls。
2.手動連接
作為替代自動連接的 PhotonNetwork.ConnectUsingSettings() 方法你可以通過PhotonNetwork.ConnectToMaster() 方法來手動連接你自己的 Photon 服務器。當你托
管付費 Photon 服務器時這是有用的。對于 ConnectToMaster() ,你需要提供一個 masterServerAddress 和一個 port 參數。地址可以是你的 On-Premises DNS 名稱或一個 IP。它可以包括冒號后的端口(然后傳遞 0 作為端口)或您可以單獨通過端口。ConnectToMaster() 方法有更多的另外兩個參數 : "appID"和"gameVersion"。
兩者都只與 Photon Cloud 有關,并且當你自己托管 Photon 服務器時,可以設置為任何值。對于 Photon Cloud, 使用 ConnectUsingSettings() 方法。它涉及到我們的 Name Server 自動找到一個區域的主服務器。
DNS,即 Domain Name Server 的首字母縮寫,意為域名服務器,這里指你自己架構的服務器。
三、功能概述
1.PUN
PUN 由相當多的文件組成, 然而只有一個是真正重要的: PhotonNetwork 。這個類包含所有需要的函數和變量.。如果您有自定義要求,可以隨時修改源文件。
要從UnityScript中使用PUN,你需要把 "PhotonNetwork"和"UtilityScripts" 文件夾移動到 Assets\Plugins\文件夾。為了告訴你這個 API 如何工作,這里有幾個例子。
2.連接
PhotonNetwork 始終使用主服務器和一個或多個游戲服務器。主服務器管理當前可用的游戲并進行匹配。一旦房間被發現或創建,實際的游戲是在游戲服務器上完成的。所有的服務器都運行在專用的機器上,沒有所謂的玩家托管的服務器。你不必費心記住該服務器組織,PUN 會為你處理它。
PhotonNetwork.ConnectUsingSettings("v1.0");
上面的代碼是你需要連接并開始使用 Photon 功能的所有代碼。ConnectUsingSettings 設置你的客戶端的游戲版本并使用一個由 PUN 設置向導寫入的配置文件,該配置文件保存在 PhotonServerSettings 里面。你也可以修改文件PhotonServerSettings 屬性來連接到你自己的服務器。或者,使用 Connect() 方法來忽略該 PhotonServerSettings 文件。
3.版本控制
Photon 的負載均衡邏輯使用你的 AppId 來區分你的和他人的游戲。玩家也會被游戲版本分開, ConnectUsingSettings 的參數(見上文)。通過這種方式,您可以發布新功能的客戶端,而不破壞舊版本的游戲。由于我們不能保證不同 PUN 的版本之間相互兼容,PUN 把它自己的版本號添加到你的游戲里。更新 PUN 可能會從舊的版本中分離出新的客戶端,但不會打破老客戶端。
4.創建和加入游戲
接下來,你想加入或創建一個房間。下面的代碼展示了一些必要的函數:
//加入一個房間
PhotonNetwork.JoinRoom(roomName);
//創建這個房間。
PhotonNetwork.CreateRoom(roomName);
// 如果該房間已存在則會失敗并調用: OnPhotonCreateGameFailed
//嘗試加入任何隨機游戲:
PhotonNetwork.JoinRandomRoom();
//如果沒有匹配的游戲則會失敗并調用: OnPhotonRandomJoinFailed
在最好的情況下,您的游戲使用隨機配對。 JoinRandomRoom() 將嘗試加入任何房間。如果該方法失敗了(沒有房間接受另一個玩家),只需創建一個新的房間,并等到其他玩家隨機加入它為止。或者,您的客戶端可以獲得當前可用的房間列表。這是通過加入一個大廳來獲得的。大廳自動發送他們的房間列表到客戶端,并在時間間隔內更新(從而減少流量)。玩家不會看到對方,且無法溝通(以防止當您的游戲繁忙時出問題)。PhotonNetwork 插件可以在其連接時自動加入默認大廳。把 PhotonServerSettings 文件里的"Auto-Join Lobby"屬性開啟即可。當你的客戶端在一個大廳里時,房間列表會得到更新, 這些更新會緩存。如果需要的話,你可以通過 GetRoomList 方法來每一幀訪問房間列表。?
foreach (RoomInfo room in PhotonNetwork.GetRoomList())
{
GUILayout.Label(room.name + " " + room.playerCount + "/" + room.maxPlayers);
}
PhotonNetwork 使用多個回調函數來讓你的游戲知道狀態的變化,如“已連接”或“已加入一個游戲”。像往常對 Unity 一樣,回調可在任何腳本里實現。如果你的腳本擴展 Photon.PunBehaviour , 你可以單獨重寫每個回調。在這種情況下,您不必調用基類實現。
public override void OnJoinedRoom()
{
Debug.Log("OnJoinedRoom() called by PUN: " + PhotonNetwork.room.name);
}
你不需要擴展 PunBehaviour 。如果你在其本身身上實現它所有的回調函數也會起作用。它們也在枚舉 PhotonNetworkingMessage 中被列出和描述。這包括建立游戲房間的基礎知識。接下來是游戲中的實際交流。
5.發消息
在一個房間里,你可以發送網絡信息給其他連接的玩家。此外,您還可以發送緩沖消息,也將被發送到未來連接的玩家(以玩家生成為例)。
發送消息可以使用兩種方法。無論是 RPCs,還是通過在一個由 PhotonView 觀察的腳本里實現 OnSerializePhotonView 。
然而有更多的網絡互動。你可以監聽一些網絡事件的回調函數,如 OnPhotonInstantiate或 OnPhotonPlayerConnected ,并且你可以觸發其中一些事件,如 PhotonNetwork.Instantiate 。如果你被最后一段弄糊涂了,不要擔心,下一步我們會為這些主題逐個做解釋。
6.視覺同步組件
PhotonView 是一個用于發送消息(RPCs 和 OnSerializePhotonView )的腳本組件。你需要將 PhotonView 依附到游戲對象或預設上。請注意,PhotonView 和 Unity 的
NetworkView 非常相似。整個過程,你的游戲中需要至少一個 PhotonView,才能發送消息和可選的實例化/分配其他的 PhotonViews。如圖下圖所示,添加一個 PhotonView 到一個游戲對象,只需選擇一個游戲對象并使用: "Components/Miscellaneous/Photon View"。
7.觀察 Transform?
如果你將一個 Transform 綁定到 PhotonView 的觀察屬性上,你可以選擇同步位置、旋轉和尺度或玩家的這些屬性組合。這可以極大的幫助制作原型或小游戲。注意:任何觀察到的值變化將發送所有觀察到的值-而不只是發生變化的那個單一值。此外,更新的值是不平滑的或插值。
8.觀察 MonoBehaviour
PhotonView 可以被設置來觀察 MonoBehaviour。在這種情況下,腳本的OnPhotonSerializeView 方法會被調用。此方法被調用來寫入對象的狀態并讀取它,這取決于
腳本是否由本地玩家控制。下面簡單的代碼展示了如何用幾行代碼來增加角色狀態同步:
void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
if (stream.isWriting)
{
//我們擁有這個玩家:把我們的數據發送給別的玩家
stream.SendNext((int)controllerScript._characterState);
stream.SendNext(transform.position);
stream.SendNext(transform.rotation);
}
else
{
//網絡玩家,接收數據
controllerScript._characterState = (CharacterState)(int)stream.ReceiveNext();
correctPlayerPos = (Vector3)stream.ReceiveNext();
correctPlayerRot = (Quaternion)stream.ReceiveNext();
}
}
9.觀察選項?
Observe Option 字段讓你選擇更新如何發送以及何時被發送。該字段還會影響到OnPhotonSerializeView 被調用的頻率。
Off 顧名思義,關掉。如果該 PhotonView 被保留為 RPCs 限定時可以很有用。
Unreliable 更新如是被發送,但可能會丟失。這個想法是,下一次更新很快到來,并提供所需的正確的/絕對的值。這對于位置和其他絕對數據來說是有利的,但對于像切換武器這樣觸發器來說是不好的。當用于同步的游戲對象的位置,它會總是發送更新,即使該游戲對象停止運動(這是不好的)。
Unreliable on Change 將檢查每一個更新的更改。如果所有值與之前發送的一樣,該更新將作為可靠的被發送,然后所有者停止發送更新直到事情再次發生變化。這對于那些可能會停止運動的以及暫時不會創建進一步更新的游戲對象來說是有利的。例如那些在找到自己的位置后就不再移動的箱子。
Reliable Delta Compressed 將更新的每個值與它之前的值進行比較。未更改的值將跳過以保持低流量。接收端只需填入先前更新的值。任何你通過 OnPhotonSerializeView 寫入的都會自動進行檢查并以這種方式被壓縮。如果沒有改變, OnPhotonSerializeView 不會再接收客戶端調用。該“可靠的”部分需要一些開銷,所以對于小的更新,應該考慮這些開銷。
現在開始,以另一種方式交流:RPCs。
10.遠程過程調用
Remote Procedure Calls ( RPC )使你可以調用"networked GameObjects"? 上的方法,對由用戶輸入等觸發的不常用動作很有用。
一個 RPC 會被在同房間里的每個玩家在相同的游戲對象上被執行,所以你可以容易地觸發整個場景效果就像你可以修改某些 GameObject 。
作為 RPC 被調用的方法必須在一個帶 PhotonView 組件的游戲對象上。該方法自身必須要被 [PunRPC] 屬性標記。
[PunRPC]
void ChatMessage(string a, string b)
{
Debug.Log("ChatMessage " + a + " " + b);
}
要調用該方法,先訪問到目標對象的 PhotonView 組件。而不是直接調用目標方法,調用 PhotonView.RPC() 并提供想要調用的方法名稱:
PhotonView photonView = PhotonView.Get(this);
photonView.RPC("ChatMessage", PhotonTargets.All, "jup", "and jup!");
你可以發送一系列的參數,但它必須匹配該 RPC 方法的定義。
這些是最基本的。詳情請閱讀 Remote Procedure Calls.?
11.Timing for RPCs and Loading Levels | 時機
RPCs 在指定的 PhotonViews 上被調用,并總是以接收客戶端上的匹配者為目標。如果一個遠程客戶端還沒有加載或創建匹配的 PhotonView,這個 RPC 就會丟失!
因此,丟失 RPCs 一個典型的原因就是當客戶端加載新場景的時候。它只需要一個已經加載有新游戲對象的場景的客戶端,并且其他客戶端不能理解這個 RPC(直到這些客戶端也加載了相同的場景)。
PUN 可以幫你解決此問題。只需在你連接之前設置PhotonNetwork.automaticallySyncScene = true 并在房間的主客戶端上使用PhotonNetwork.LoadLevel() 。這樣,一個客戶端定義了所有客戶端必須在房間/游戲中加載的關卡。
客戶端可以停止執行接收到的消息來防止 RPCs 丟失(這正是 LoadLevel 方法幫你做的)。當你得到一個 RPC 來加載一些場景,立即設置 isMessageQueueRunning = false 直到該內容被初始化。
private IEnumerator MoveToGameScene()
{
// 加載關卡前臨時禁用進一步的網絡信息處理
PhotonNetwork.isMessageQueueRunning = false;
Application.LoadLevel(levelName);
}
?禁用消息隊列將延遲傳入和傳出消息,直到隊列被解鎖。顯然,當你準備好要繼續的時候,打開隊列是非常重要的。
四、簡介
這個 PUN 基本教程是一個基于 Unity3D 的教程。它將教會你如何開發一個你自己的多人在線應用,當然,這是由 Photon Cloud 提供技術支持的。以及
怎樣使用 Animator? 來為角色做動畫。同時我們將一路學習許多重要的功能、提示和技巧,以獲得一個很好的以網絡為基礎的 PUN 開發路線總覽。
1.概述
本教程將從一個空的項目開始,在整個游戲創建過程中一步一步地引導您。一路上,概念將被解釋,以及常見的陷阱和為網絡游戲所做的設計考慮。為了不讓玩家四處走動,什么也不做,我們將實現一個基本的射擊系統,再加上玩家的健康管理,這將有助于我們支持變量通過網絡同步的解釋。
?* Photon Cloud 直譯為光子云,是類似于阿里云、騰訊云那樣的云端服務。本文將保留英文名稱,以方便讀者追本溯源。后面的教程中還有更詳細的解釋。
我們也將使用一個根據在房間里的玩家數量來自定義大小的競技場,競技場的大小正在根據當前游戲的玩家數據被調整。這將展示幾個關于自動同步場景功能、在加載不同場景時如何應對玩家、以及在這種情況下哪些容易出錯的技巧 :)
2.游戲目標
當游戲啟動時,用戶將在 UI 中見證連接協議并了解連接進展情況。當玩家加入或創建一個房間后,玩家進入一個可同時容納多達 4 個玩家的、可調整大小
的競技場。玩家可以跑動、轉彎以及射擊。每一個玩家發射的光束會影響其他被擊中玩家的健康值。當你的健康值是 0 時,那么你就游戲結束了并離開競技場。
然后出現在屏幕上的,是再次讓你開始或加入一個新的游戲,如果想要的話。
3.你需要知道的
本教程假定你只有使用 Unity 編輯器和編程的基礎知識。然而,為了集中介紹 Photon Networking 的新概念,讀者最好有一個良好的知識儲備以及創建
常規的、非網絡游戲的項目經驗。
示例代碼是用 C#寫的,但在 Unity 的腳本中同樣起作用。
* Photon Networking 即 Photon 的網絡架構,包括 Photon 服務器和客戶端。
創建一個新的項目,一般情況這是在使用教程時推薦的操作。然后一旦你同化了概念和設計模式,就能將其應用到自己的項目中。如圖 0-1 所示,該操作是比較簡單的。
4.導入 PUN 與設置
打開 Asset store? 并找到 PUN 插件并下載/安裝它。當你把所有 PUN 插件資源都導入完成時讓 Unity 重新進行編譯。如圖 0-2 所示,按照步驟操作即可。
PUN 設置向導是幫助你完成網絡設置,并提供了一個方便的方式來開始我們的多人游戲開發:The Photon Cloud!Cloud? Yes, Cloud. 這是一組我們可以用來為我們的游戲服務的 Photon服務器。我們將一點點做解釋。使用云端的"Free Plan "是免費且沒有任何義務的,所以現在我們只需輸入我們的郵件地址,向導就會執行它的魔法。如圖 0-3 所示,在 PUN 設置向導里面可以輸入郵件地址,也可以輸入"AppId",然后點擊"Setup Project "即可。
* Free Plan 免費計劃。
* AppId 應用的 Id,可以在 Photon 官網注冊后在儀表盤處獲得。
* Setup Project 設置項目。
新賬戶會立即獲得一個"AppId"。如果你的郵箱地址已經被注冊,你會被要求打開 Dashboard 。登錄即可獲得"AppId",將其復制粘貼到輸入框里面。當"AppId"被保存,我們就完成了這一步的設置。
如圖 0-4 所示,Photon Cloud 就像云團一樣。那么,這個"Photon Cloud" 到底是做什么的呢?!
* Dashboard 儀表盤,即網站的管理后臺,根據權限的不同會有不同的儀表盤界面。這里指用戶儀表盤。
本質上,它是一組運行 Photon 服務器的 PC 機。這個服務器"cloud"是由Exit Games? 維護并為您的多人游戲提供無憂的服務的。服務器會根據需求來進行添加,所以可以勝任處理任何數量的玩家。盡管 Photon Cloud 不是完全免費的,但是成本低,特別是和常規的托管主機相比。
Photon Unity Networking? 將為你處理 Photon Cloud,但這就是內部的情況:首先每個玩家連接到一個"Name Server"? 。服務器會檢查你客戶端使用的是哪個應用(使用 AppId 來識別),以及客戶端想要使用哪個區域的服務器。然后"Name Server"將客戶端轉發到一個主服務器。主服務器是一組區域服務器的集線器。它知道所有現有的游戲。任何時候一個游戲(或房間)被創建或加入,客戶端都會被轉發到其他所謂"Game Server" 的機器。PUN 的設置是非常簡單的,并且你無需擔心托管費用、性能或維護。一次也不需要。
* Exit Games 是國際領先的多平臺網絡游戲引擎供應商,其所開發的高性能 photon 引擎,通過 SDK 的形式,為游戲開發者開發實時多人應用提供了最佳的解決方案。目前該引擎已支持 Unity,iOS,Android,Flash和 HTML5 等多個平臺,它能幫助開發人員輕松實現瀏覽器、PC、Mac 或移動設備,包括 iPad、iPhone 和
Android 的游戲及應用中的多人實時功能。包括 OpenFeint,Bigpoint,Walt Disney,KONAMI 等游戲公司都在使用 Exit Games 的產品。
* Photon Unity Networking 即 PUN,是 Photon 為 Unity 定制的多人游戲網絡解決方案插件。
* "Name Server",即“名稱服務器”。
* "Game Server",即“游戲服務器”。
要記住 Photon Cloud 被構建來做"room-based games"? ,意味著每次匹配有限數量的玩家(譬如說:少于 10),這些玩家是與其他房間的玩家分開的。在一個房間里面(通常情況下)的每一個玩家都會接收到其他玩家發送的任何信息。房間外的玩家則不能溝通,所以我們總是想要他們盡快加入房間。加入一個房間最好的方式就是使用隨機匹配。我們只需請求服務器任何房間或帶特定屬性的房間即可。所有的房間都有一個名稱作為標識符。除非房間已滿或被關閉了,我們可以通過名稱來加入房間。為了方便玩家,主服務器可以為我們的應用程序提供一個房間列表。
* "room-based games" ,即基于房間的游戲,類似于 CF、LOL 之類的。
5.游戲大廳
您的應用的大廳存在于主服務器上,大廳中為您的游戲列出房間。在我們的例子中,我們將不使用大廳,如果有可用的房間就簡單地加入一個隨機房間,如果沒有可用的房間可以加入(房間可以有一個最大容量,所以他們可能是全滿了),則創建一個新的房間。
6.應用 IDs與游戲版本
如果每個玩家都連接到相同的服務器,必須有一個方法來區分你和其他的玩家。每個游戲(同樣適用于應用)在云端有它自己的"AppId"。玩家總是會遇到有同樣"AppId"的其他玩家,僅在客戶端里。也有一個"game version" ,您可以用來將不同客戶端版本的玩家分開。
7.區域
Photon Cloud 被組織在全球不同的區域,以防止玩家因為距離服務器太遠而潛在的不良連接。重要的是要理解這個概念,尤其是當與分散在各地的遠程團隊工作時。與您的隊友測試您的游戲也許不太可能,因為分散在不同地區。所以確保你強制所有想要和彼此互動測試員的區域是相同的。
8.?開發
每個部分涵蓋了項目開發階段的一個非常具體的部分,重要的是要按順序進行工作。腳本和 Photon 知識的假設水平也逐漸增加。
9.小結
所以,我們達成了一個工作系統,用戶可以在互聯網上互相對抗,具備基本的良好體驗要求。我們會學到如何控制 PUN,如何監聽 PUN 狀態和現狀,以及使最直觀的組件來輕易地協同 Animator 工作。我們也了解到 Photon 的一些有趣的功能,如自動場景同步,來創建原創的且強大的游戲。
要創造一個完整的游戲,還有很多事情要做,為上線做準備,但這僅僅是建立在我們所涵蓋的基礎之上。
* API 參考文檔,API,即 Application Programming Interface 的首字母縮寫,意為應用程序編程接口。
五、游戲大廳
1.連接到服務器、房間訪問和創建
讓我們先解決這個教程的核心,能夠連接到 Photon 云服務器,并加入一個房間或如有必要則創建一個。
1. 創建一個新場景,并將其保存為 Launcher.unity ;
2. 創建一個新的 C #腳本 Launcher ;
3. 在 Hierarchy 層次結構中創建一個空的 GameObject 游戲對象,命名為 Launcher;
4. 將 C #腳本 Launcher 添加到 GameObject Launcher 上;
5. 編輯 C #腳本 Launcher 成如下內容的樣子。
6. 保存該 C#腳本 Launcher
讓我們回顧一下目前這個腳本中的內容,首先從一般的 Unity 角度來看,然后看看我們制作的 PUN 的具體調用。
命名空間:? 雖然不是強制性的,給予你的腳本適當的 namespace 可以防止與其他資源和開發者發生沖突。萬一另一個開發者也創建了一個 Launcher 類呢?Unity 將報錯,并且您或該開發人員將不得不為 Unity 重命名該類,以允許執行該項目。如果沖突來自您從資源商店下載的資產,這可能是棘手的。現在, Launcher 類實際上是Com.MyCompany.MyGame.launcher,在引擎下不太可能有其他人會使用和我們一樣的命名空間,因為你擁有這個域名,并且使用倒置域名慣例作為命名空間使你的工作安全而又組織良好。Com.MyCompany.MyGame 應該被你自己的反向域名和游戲名替換,這是一個值得遵循的良好約定。
?MonoBehaviour Class: 請注意我們用 MonoBehaviour 派生出我們的類,這從本質上把我們的類轉化成一個Unity Component,從而使我們可以把這些類添加到GameObject或Prefab上作為組件。一個繼承 MonoBehaviour 的類可以訪問許多非常重要的方法和屬
性。在這個案例中我們使用了兩個回調方法,Awake()和 Start()。
PhotonNetwork.autoJoinLobby: 在 Awake()方法中,我們設置 PhotonNetwork.autoJoinLobby 為 false,因為我們不需要 Lobby 功能,我們只需要獲取當前房間的列表。強制設置通常是一個好主意,因為在同一個項目中你可以有另一個確實想要 autoJoin 該 Lobby 的場景,這樣在轉換不同的方法時就不會有問題了。
PhotonNetwork.ConnectUsingSettings(): 在 Start()方法中我們調用了我們的公共函數 connect() 來使用
PhotonNetwork.ConnectUsingSettings()連接 PUN云。請注意 _gameVersion 變量代表你的 gameversion。你應該保持該參數為 "1" 直到你在你的上線項目中產生突破性改變。這里要記住的重要信息是 PhotonNetwork.ConnectUsingSettings()是你的游戲網絡化以及連接 connect 到 Photon 云的起點。
PhotonNetwork.automaticallySyncScene: 我們的游戲將有一個根據玩家的數量調整大小的競技場,并確保每一個已連接的玩家所
加載的場景都是一樣的,我們將使用 Photon 提供的很方便的功能:PhotonNetwork.automaticallySyncScene當該屬性被設置為 true,MasterClient 可以調用 PhotonNetwork.LoadLevel(),這樣所有已連接的玩家將自動加載相同的關卡。
在這一點上,你可以保存 Launch 場景,并打開 PhotonSettings (從 Unity 的菜單窗口 Window/Photon Unity Networking/Highlight Photon Server Settings 中選擇它),如下圖所示,我們需要像這樣設置調試級別為 Full:
然后我們可以點擊Play。你應該在Unity控制臺看到好幾十條日志。特別是“Connected to masterserver.”這條記錄表明我們現在已連接并準備加入一個房間。
總是測試潛在的錯誤是寫代碼的一個好習慣。在這里我們假設計算機已連接到互聯網,但如果計算機沒有連接到互聯網會發生什么?讓我們探尋一下。關閉您計算機的互聯網并運行該場景。你應該看到在 Unity 控制臺出現一個錯誤"Connect() to 'ns.exitgames.com'
failed: System.Net.Sockets.SocketException: No such host is known"。理想情況下,我們的腳本應該意識到這個問題,并優雅地應對這些情況,并提出一個反應性的經驗,無論出現什么情況或問題。讓我們現在處理這兩種情況,并在我們的 Launcher 腳本內通知我們確實已連接或沒有連接到 PUN 服務器。這將是完美的 PUN 回調介紹。
2. PUN 回調
PUN 的回調是非常靈活的,且提供了三種非常不同的實現。讓我們覆蓋所有三種方法的學習,我們會根據情況來選擇一個最適合的。
"magic" methods | “魔術”方法
使用常規 MonoBehaviour 時,你可以簡單地創建私有方法。
void OnConnectedToMaster()
{
Debug.Log("DemoAnimator/Launcher: OnConnectedToMaster() was called by PUN");
}
這是神奇的因為任何 MonoBehaviour 可實現該方法或來自 PUN 的任何消息。它遵循和 Unity 正在發送到 MonoBehaviours 的諸如 Awake()或 Start()這樣的主方法一樣的原則。但是我們不會用這個,因為如果你拼錯了這些“神奇”的方法,也不會告知你的錯誤,所以這是一個非常實用的快速實現,但只有在知道每個方法的確切名稱,并且你很熟悉和擅長調試技術來快速找到這些問題時。
Using IPunCallbacks and IPunObservable Interfaces | 使用接口
PUN 提供了兩種 C#接口 Interfaces ,你可以在你的類里面實現它們。 IPunObservable和 IPunCallbacks 。
這是一個非常安全的方法,以確保一個類符合接口的所有,但會強制開發人員實現接口的所有聲明。大多數好的腳本編輯器會使這個任務很容易,如上圖使用 MonoDevelop 所演示那樣。但是,該腳本可能會有很多什么都不做的方法,但必須全部實現來讓 Unity 編譯器高興。所以這是當你的腳本是要大量使用所有或大部分的 PUN 功能時適用。我們確實要在接下來的數據序列化的教程中使用 IPunObservable 接口。
Using Photon.PunBehaviour | 使用 Photon.PunBehaviour
最后的技術,這是一個我們將經常使用,且是最方便的。與其從 MonoBehaviour 中創建一個派生類,我們將從Photon.PunBehaviour 派生類,如下圖所示,因為它暴露了特定的屬性和虛函數 virtual methods 供我們使用以及方便我們重寫 override。這是很實用的,因為我們可以肯定的是,我們沒有任何的錯別字,并且我們不需要實現所有的方法。
注:在重寫時,大多數腳本編輯器將默認實現基本調用和自動為你填充,但在我們的案例中我們并不需要,所以就Photon.PunBehaviour 一般規則,不要調用基本方法。注意:重寫的另一大好處是,只需懸停在方法的名稱上你就可以得到上下文的幫助。所以讓我們來實踐一下 OnConnectedToMaster()和 OnDisconnectedFromPhoton()PUN 回調。
1. 編輯 C #腳本 Launcher
2. 把基類從 MonoBehaviour 修改成 Photon.PunBehaviour
public class Launcher : Photon.PunBehaviour {
3. 在類的結尾添加以下兩種方法, 在域 region Photon.PunBehaviour CallBacks 內更加明了。
#region Photon.PunBehaviour CallBacks //域可以使代碼結構更加清晰
public override void OnConnectedToMaster()
{
Debug.Log("DemoAnimator/Launcher: OnConnectedToMaster() was called by
PUN");
}
public override void OnDisconnectedFromPhoton()
{
Debug.LogWarning("DemoAnimator/Launcher:
OnDisconnectedFromPhoton() was called by PUN");
}
#endregion
4. 保存 Launcher 腳本。
現在,如果我們玩這個場景,無論有或沒有互聯網,我們都可以采取適當的步驟來通知玩家,并進一步進入邏輯。我們將在下一節開始構建 UI 時處理這個問題。現在我們將處理成功的連接:
所以,我們在 OnConnectedToMaster()方法中追加如下的調用:
// #Critical | 極重要: 我們首先嘗試要做的就是加入一個潛在現有房間。如果有,很好,
否則,我們將調用回調 OnPhotonRandomJoinFailed()
PhotonNetwork.JoinRandomRoom();
并且如注釋所說那樣,如果加入一個隨機房間的企圖失敗了我們需要被通知,在該情況下我們需要創建一個房間,所以我們在腳本里實現 OnPhotonRandomJoinFailed()PUN 回調,并使用 PhotonNetwork.CreateRoom() 來創建一個房間,以及,你已經猜到了,相關
的 Pun 回調 OnJoinedRoom()將在我們實際加入一個房間時通知你的腳本:
public override void OnPhotonRandomJoinFailed (object[] codeAndMsg)
{
Debug.Log("DemoAnimator/Launcher:OnPhotonRandomJoinFailed() was called by PUN. No random room available, so we create one.\nCalling: PhotonNetwork.CreateRoom(null, new RoomOptions() {maxPlayers = 4}, null);");
// #Critical | 極重要: 我們加入一個隨機房間失敗,也許沒有房間存在或房間已滿。別擔心,我們創建一個新的房間即可。
PhotonNetwork.CreateRoom(null, new RoomOptions() { maxPlayers = 4 }, null);
}
public override void OnJoinedRoom()
{
?Debug.Log("DemoAnimator/Launcher: OnJoinedRoom() called by PUN. Now this client is in a room.");
}
現在如果你運行該場景,你應該遵循連接到 PUN 的邏輯序列,試圖加入一個現有的房間,否則創建一個新的房間并加入那個新創建的房間。
在教程的這個點上,因為我們現在已經覆蓋了連接和加入一個房間的關鍵部分,有幾件事情不是很方便,并且這些問題需要盡早解決。這些問題都不是真正關系到學習 PUN 的,但重要的是從整體的角度來看。
Expose variables in Unity Inspector | 暴露變量
你可能已經知道了這一點,但假如你不知道,MonoBehaviours 自動暴露自己的公共屬性到 Unity 的 Inspector 窗口。這在 Unity 里是一個非常重要的概念,在我們的情況下,我們要修改定義 LogLevel 的方式,并制作一個公共變量,這樣我們不接觸代碼本身就可以
完成設置。
/// <summary>
/// PUN 的日志等級。
/// </summary>
public PhotonLogLevel Loglevel = PhotonLogLevel.Informational;
并且在 Awake()方法里面我們將進行接下來的修改:
// #NotImportant | 不重要
//強制為 LogLevel 賦值
PhotonNetwork.logLevel = Loglevel;
所以,現在我們不強制腳本是某種類型的 LogLevel,我們只需要在 Unity 的 Inspector里進行設置,然后運行,不需要打開腳本,編輯,保存,等待 Unity 重新編譯,最后再運行。這是一個更富有成效和靈活的方式。
我們會為每個房間的最大玩家數量做同樣的事情。在代碼里硬編碼并不是最佳實踐,相反,讓它作為一個公共變量,這樣我們就可以決定和調試那個數值而無需重新編譯。在類聲明的開頭,在公共變量 Public Variables 區域我們添加:
/// <summary>
/// 每個房間的最大玩家數。當一個房間滿了,它不能加入新的玩家,所以新的房間將被創建。如圖 11 所示,該公共變量將暴露在 Unity 的 Inspector 窗口面板中。
/// </summary>
[Tooltip("每個房間的最大玩家數。當一個房間滿了,它不能加入新的玩家,所以新的房間將被創建。")]
public byte MaxPlayersPerRoom = 4;
然后我們修改 PhotonNetwork.CreateRoom()調用,并使用這個新的公共變量來取代我們之前使用的硬編碼的數字。
// #Critical | 重要:我們沒能進入一個隨機的房間,也許沒有這樣的房間存在,或者都滿了。不用擔心,我們創造一個新的房間。
PhotonNetwork.CreateRoom(null, new RoomOptions() { maxPlayers =MaxPlayersPerRoom }, null);
六、游戲大廳UI
這部分將著重于為大廳創建用戶界面(UI)。它將保持非常基本的,因為它不是真正關系到網絡。
1. The Play Button | 開始按鈕
目前我們的游戲大廳是自動把我們連接到一個房間,這是很好的早期測試,但我們真的希望讓用戶選擇是否以及何時開始游戲。所以我們將簡單地為此提供一個按鈕即可。
1. 打開上一節創建的 Launcher 場景;
2. 使用 Unity 菜單'GameObject/UI/Button'創建一個 UI 按鈕, 并命名為: Play Button注意該操作在創建按鈕的同時在場景的Hierarchy 層級中創建了一個 Canvas 幕布和一個 EventSystem 事件系統游戲對象,所以我們不必手動創建;
3. 編輯 Play Button 按鈕的子類 Text 的變量為"Play";
4. 選擇 Play Button 并找到 Button 組件里的 On Click () 部分;
5. 點擊'+'小按鈕來添加一個條目;
6. 把 Launcher 游戲對象從 Hierarchy 層級中拖進該字段;
7. 在下拉菜單中選擇 Launcher.connect() ,這樣我們就把該按鈕和 Launcher 腳本連接到了一起,當玩家按下該按鈕的時候就會從 Launcher 腳本中調用"Connect()"方法;
8. 打開 Launcher 腳本;
9. 移除我們在 Start()方法里調用 Connect() 的那一行代碼;
10. 保存 Launcher 腳本和場景;
如果你現在點擊運行,你會發現不會再連接直到你點擊該按鈕。
2. The Player Name | 玩家名稱
典型游戲的另一個重要的最低要求就是讓用戶輸入他們的名字,這樣其他玩家就知道和他們一起玩游戲的是誰了。我們將為這個簡單的任務添加一點曲折,通過使用 PLayerPrefs記住名字的值,這樣當用戶打開游戲,我們可以恢復該名字是什么。這是一個非常方便和非常重要的功能,實現在您的游戲的許多領域可以為用戶帶來非常好的體驗。讓我們首先創建該腳本,將用來管理和記住玩家的名稱,然后創建相關的用戶界面。
創建 PlayerNameInputField:
1. 創建一個新的 C #腳本,命名為 PlayerNameInputField;
2. 這里是它的全部內容。編輯并保存相應的 PlayerNameInputField 腳本;
讓我們來一起分析這個腳本:
RequireComponent(typeof(InputField)):我們首先要確保這個腳本強制執行 InputField,因為我們需要它,這是一個非常
方便快捷的方法,以保證該腳本的使用沒有問題。
PlayerPrefs.HasKey(), PlayerPrefs.GetString()和 PlayerPrefs.SetString():PlayerPrefs 是一個簡單的配對條目查找列表(就像兩列的 Excel 表),一列是 key ,一列是 Value 。鍵 key 是一個字符串,并且是完全任意的,你決定如何命名且您將需要在整個開發中堅持該命名。正因為如此,總是把你的 PlayerPrefs 鍵只保存在一個地方是很有道理的,一個方便的方法是使用一個[Static|變量名,因為在游戲中它不會隨著時間的推移而改變,而且每次都是一樣的。所以,邏輯是非常直接的。如果 PlayerPrefs 有已給定的鍵,我們可以獲取它并在啟動該功能的時候直接為其注入值,在我們的案例中,在我們啟動和編輯過程中用這個來為InputField 填入值,我們用 InputField 的當前值來設置 PlayerPref,然后我們確定它是在用戶設備本地存儲供以后檢索(下一次用戶將打開這個游戲)。
PhotonNetwork.playerName:這是這個腳本的要點,在網絡上設置玩家的名稱。該腳本在兩個地方使用玩家名稱,一次是在 Start()過程中,在檢查完該名稱是否被存儲在 PlayerPrefs 里面之后,以及在公共方法 SetPlayerName() 里面。現在,并沒有調用這個方法,我們需要綁定 InputField 的OnValueChange()來調用 SetPlayerName() ,這樣每次用戶編輯該 InputField 時我們都把它記錄下來。我們只有在用戶按下開始按鈕后才能執行該操作,這取決于你,但是這是一個更明智的腳本,所以讓我們為了清楚起見保持其簡單。這也意味著,無論用戶會做什么,輸入將被記住,這往往是所需的行為。
3. Creating the UI for the Player's Name | 創建 UI
1. 確保你仍然在 Launcher 場景里面,如果不在,打開該場景;
2. 使用 Unity 的菜單'GameObject/UI/InputField'創建一個 UI InputField , 將該游戲對象 GameObject 命名為 Name InputField;
3. 設置 RectTransform 里面的 PosY 值到 35 ,這樣它會在 Play Button 按鈕上面;
4. 找到 Name InputField 的子類 PlaceHolder ,并將它的 Text 值設置為"Enter your Name...";
5. 選擇 Name InputField GameObject;
6. 把我們剛剛創建好的 PlayerNamerInputField 腳本添加到該對象上;
7. 找到 InputField 組件內的 On Value Change (String) 部分;
8. 添加那個'+'號來添加一條綁定事件;
9. 拖拽 Launch 的 PlayerNamerInputField 組件到該字段進行綁定;
10. 在 Dynamic String 部分的下拉菜單中選擇 PlayerNameInputField.SetPlayerName();
11. 保存該場景(Ctrl+S);
現在你可以點擊運行了,輸入你的名字,停止運行,再次運行,你之前輸入的名字就會出現。
我們越來越接近目標了,但是在用戶體驗 User Experience 方面我們缺少對連接進度的反饋,以及在連接和加入房間出錯時的反饋。
4. The Connection Progress | 連接進程
我們將在這里保持簡單,并隱藏名稱字段和開始按鈕,并用一個簡單的文本“Connecting...”在連接期間代替,并在需要時切換回來。
要實現這樣的效果,我們將開始按鈕和名稱字段歸為一組,以便我們簡單地激活和停用該組。后面更多的功能可以添加到該組,且它不會影響到我們的邏輯。
1. 老規矩,確保你仍然在 Launcher 場景;
2. 使 用 Unity 菜 單'GameObject/UI/Panel' 創 建一 個 UI Panel, 將 該游 戲對 象GameObject 命名為 Control Panel;
3. 從 Control Panel 中刪除其 Image 和 Canvas Renderer 組件,我們不需要這個面板的任何視覺效果,我們只關心它的內容;
4. 把 Play Button 和 Name InputField 拖拽到 Control Panel 上;
5. 使用 Unity 菜單'GameObject/UI/Text'創建一個 UI Text, 將其命名為 Progress;Label 不要擔心它會干擾視覺,我們將在運行時激活/關閉他們;
6. 選擇 Progress Label 的 Text 組件;
7. 把 Alignment 設置成 center align 和 middle align;
8. 把 Text 的值設置成"Connecting...";
9. 把 Color 設置成白色或任何可以背景中凸顯出來的顏色;
10. 保存該場景。
在這一點上,想要測試,您可以簡單地啟用/禁用控制面板 Control Panel 和進度標簽Progress Label ,看看事情在不同的連接階段會如何顯示。現在讓我們編輯腳本來控制這些兩個游戲對象的激活。
1. 編輯腳本 Launcher
2. 在公共屬性區域 Public Properties Region 中添加以下兩個屬性
[Tooltip("讓用戶輸入名稱、連接和開始游戲的 UI 面板")]
public GameObject controlPanel;
[Tooltip("通知用戶連接正在進行中的 UI 標簽")]
public GameObject progressLabel;
3. 把以下的代碼添加到 Start()方法
progressLabel.SetActive(false);
controlPanel.SetActive(true);
4. 在 Connect()方法的開始處添加下面的代碼
progressLabel.SetActive(true);
controlPanel.SetActive(false);
5.在 OnDisconnectedFromPhoton()方法開始處添加如下代碼
progressLabel.SetActive(false);
controlPanel.SetActive(true);
6. 保存 Launcher 腳本并等待 Unity 完成編譯
7. 確保你仍然在 Launcher 場景
8. 在 Hierarchy 層級中選擇游戲對象 GameObject Launcher
9. 從 Hierarchy 層級中把 Control Panel 和 Progress Label 拖拽到 Launcher 組件里的對應字段。
10. 保存場景
現在,如果你運行該場景。你只會看到控制面板 Control Panel,只要你點擊開始游戲,進程標簽 Progres Label 就會出現。
現在,我們的游戲大廳部分暫時做好了。為了進一步添加功能到大廳,我們需要切換到游戲本身,并創建各種場景,以便我們終于可以在加入一個房間時加載正確的關卡。我們將在下一節和之后小節中完成,我們將最終完成大廳系統。
七、游戲場景
本節介紹了玩家將要進行游戲的各種場景的創建。每個場景將致力于滿足特定數量的玩家,越來越大,以適應所有玩家,給他們足夠的空間走動。在本教程中,我們將實現基于玩家數量加載正確關卡的邏輯,這將使用一個達到關卡的約定,關卡名稱以這種格式“Room for X”命名,X 代表玩家的數量。
1.First Room Creation | 第一個房間創建
1. 創建一個新場景, 保存并命名為 "Room for 1" ;
2. 創建一個立方體 Cube 并命名為 " floor";
3. 將其放置在 0,0,0。這是重要的,因為我們的邏輯系統將生成的玩家放置在中心之上(0,x,0);
4. 把 floor 放大到 20,1,20。
這就足夠一個可玩的水平了,但一些墻將使玩家在該范圍內。只需創建更多的立方體,
調整其位置、旋轉和縮放來使它們作為墻壁。以下是四墻壁與地板的游戲對象的位置和規模。
做到這里別忘了保存 Room For 1 場景。
2.Game Manager Prefab | 游戲總管預制體
在所有情況下,用戶界面的最低要求是能夠退出房間。為此,我們需要一個 UI Button按鈕,但我們也需要一個調用 Photon 來使本地玩家離開房間的腳本,所以讓我們開始創建我們將調用的 Game Manager 預設,和第一個任務將處理退出本地玩家當前所在的房間。
1. 創建一個新的 C #腳本 GameManager;
2. 在場景中創建一個空游戲對象,命名為 Game Manager;
3. 把 GameManager 腳本拖拽到 Game Manager 游戲對象上;
4. 通過把場景層級 Hierarchy 中的 Game Manager 拖拽到資源瀏覽器 Assets 中將其轉化成預設,而層級中的原型就會變成藍色的;
5. 編輯 GameManager 腳本;
6. 用以下的代碼進行替換:
7. 保存 GameManager 腳本。
因此,我們創建一個公共方法 LeaveRoom() 。它的作用是顯式地讓本地玩家離開 Photon網絡房間,我們用我們自己的公共方法來將其包裝使其抽象化。我們可能會想在稍后的階段實現更多的功能,如保存數據,或插入一個用戶將離開游戲等的確認步驟。基于我們的游戲要求,如果我們不在一個房間里我們需要顯示 Launcher 場景,所以我們要監聽 OnLeftRoom() Photon 回調并加載游戲大廳場景 Launcher ,該場景在 Build settings 的場景列表中的索引為 0,而我們將在這一節的 Build Settings Scene List 部分里進行設置。
但為什么要制作成預制體呢?因為我們的游戲需求意味著同一個游戲有幾個不同的場景,所以我們需要重用這個游戲管理器 Game Manager 。在 Unity 中重用對象的最佳方式是把他們變成預設。
接下來,讓我們創建 UI Button 按鈕,用來調用我們的 GameManager 的 LeaveRoom()方法。
3.Quit Room Button Prefab | 退出房間按鈕預設
再次,就像 Game Manager 一樣,長遠來看我們會有許多不同的場景需要這個功能,所以睿智的做法是未雨綢繆并將該按鈕做成一個預設,這樣我們就可以重用它了,并且在將來需要改變它的時候在一個地方做修改即可。
1. 確保你在場景 Room for 1 里面;
2. 使用 Unity 菜單創建'GameObject/UI/Panel'一個 UI Panel,并命名為 Top Panel ;
3. 移除 Image 和 Canvas Renderer 組件清空這個面板。如果你覺得不刪除好看點的話也可以保留,這只是為了審美而已;
4. 同時按住 Shift 和 Alt 設置垂直錨點預置到 top ,設置水平錨點預置到 stretch 。RectTransform 錨點需要一些經驗去適應它,但它是值得的;
5. 把 RectTransform 的高度設置成 50;
6. 右鍵點擊該面板的游戲對象 Top Panel 并添加一個 UI/Button,命名為 Leave button;
7. 選擇 Leave button 的子類 Text, 并將它的文本設置為 Leave Game;
8. 如下圖所示,把 OnClick 按鈕事件連接到 Hierarchy 層級中的 Game Manager 實例來調用 LeaveRoom() 。
1. 通過把 Leave button 從場景 Hierarchy 層級中拖拽到 Assets 資源瀏覽器將其轉變成一個預設,它將在 Hierarchy 層級中變成藍色;
2. 保存該場景及項目。
4.Other Rooms Creation | 其他房間創建
現在,我們已經有一個房間做好了,讓我們復制 3 次,并適當地為其命名(他們應該在被復制的時候被 Unity 命名過了):
? Room for 2
? Room for 3
? Room for 4
查找以下位置、旋轉和規模的變化,以加快這一重復過程。
Room for 2:
Room for 3:
地面大小縮放: 50,1,50
Room for 4:
地面大小縮放: 60,1,60
5.Build Settings Scenes List | 構建設置場景列表
在編輯和發布時,項目的良好運行至關重要,我們需要在 Build Settings 中添加所有這些場景,以便 Unity 在構建應用程序時包含它們。
1. 通過 Unity 菜單"File/Build Settings"打開 Build Settings;
2. 如下圖所示,拖拽所有的場景到列表,Launcher 場景必須保持在第一,因為 Unity默認將為玩家加載和展示列表中的第一個場景。
現在我們有了基本的場景設置,我們終于可以開始把一切串聯起來。讓我們在下一節做這個。
八、Game Manager&Levels
本節介紹了添加功能來處理基于目前在房間里玩的玩家數量來加載不同的關卡。
1.Loading Arena Routine | 加載競技場例行程序
我們已經創建了 4 個不同的房間,我們以一定的規律來它們命名,場景名稱的最后一個字符是玩家的數量,所以現在很容易把房間里的玩家數量和相關場景綁定起來。這是一個非常有效的技術被稱為“convention over configuration”,就是方法匹配玩家數量到對應的房間。然后,我們的腳本將在該列表中查找,并返回一個場景,其中的名稱并不重要。"Configuration"需要更多編寫,這就是為什么我們會在這里使用"Convention",這讓我們得到更快的編寫代碼,而無需在我們的代碼中寫無關的功能。
1. 打開 GameManager 腳本;
2. 讓我們添加一個新的方法到一個新的專用于私有方法的區域。不要忘記保存GameManager 腳本;
#region Private Methods //私有方法區域 void LoadArena() { if ( ! PhotonNetwork.isMasterClient ) { Debug.LogError( "PhotonNetwork : Trying to Load a level but we are not the master Client" ); } Debug.Log( "PhotonNetwork : Loading Level : " + PhotonNetwork.room.PlayerCount ); PhotonNetwork.LoadLevel("Room for "+PhotonNetwork.room.playerCount); } #endregion3. 保存 GameManager 腳本。
當我們調用這個方法時,我們將根據我們所在房間的 PlayerCount 屬性加載合適的房間。
這里有兩件事值得注意,這是非常重要的。
? 只有在我們是主客戶端的時候才能調用 PhotonNetwork.LoadLevel()。所以我首先使用 PhotonNetwork.isMasterClient 來檢查我們是否是主客戶端。檢查這個是調用者的責任,我們將在這一節的下一部分詳細講。
? 我們使用 PhotonNetwork.LoadLevel()來加載我們想要加載的關卡,我們不會支持使用 Unity,因為我們想要依賴 Photon 來在所有已連接的該房間內的客戶端上加載這個關卡 ,因為我們為這個游戲啟用了
PhotonNetwork.automaticallySyncScene。
現在我們已經有了加載正確關卡的方法,讓我們把這個和玩家的連接和斷開連接綁定起來。
2.Watching Players Connection | 觀察玩家連接
目前,我們的 GameManager 腳本是一個常規的 MonoBehaviour,我們已經在本教程的前面部分研究了得到 Photon 回調的各種方法,現在 GameManager 需要監聽玩家連接與
斷開。讓我們來實現這個。
1. 打開 GameManager 腳本;
2. 把基類從 MonoBehaviour 修改成 Photon.PunBehaviour:
3. 讓我們來添加下面的 Photon 回調信息并保存 GameManager 腳本:
#region Photon Messages //Photon 消息區域 public override void OnPhotonPlayerConnected( PhotonPlayer other ) { Debug.Log( "OnPhotonPlayerConnected() " + other.NickName ); //如果你是正在連接的玩家則看不到 if ( PhotonNetwork.isMasterClient ) { Debug.Log( "OnPhotonPlayerConnected isMasterClient " + PhotonNetwork.isMasterClient ); //在 OnPhotonPlayerDisconnected 之前調用LoadArena(); //加載競技場 } } public override void OnPhotonPlayerDisconnected( PhotonPlayer other ) { Debug.Log( "OnPhotonPlayerDisconnected() " + other.NickName ); //當其他客戶端斷開連接時可見 if ( PhotonNetwork.isMasterClient ) { Debug.Log( "OnPhotonPlayerConnected isMasterClient " + PhotonNetwork.isMasterClient ); //在 OnPhotonPlayerDisconnected 之前調用 LoadArena(); } } #endregion4. 保存 GameManager 腳本
現在,我們有一個完整的設置。每當一個玩家加入或離開房間,我們就被告知,我們會調用我們剛剛在上面創建的 LoadArena() 方法。然而,只有使用PhotonNetwork.isMasterClient 確定我們是主客戶端時才能調用 LoadArena() 。現在我們回到大廳,終于可以在加入房間時加載正確的場景。
3.Loading Arena from the Lobby | 加載競技場
1. 編輯腳本 Launcher;
2. 追加下面的代碼到 OnJoinedRoom() 方法中:
3. 保存腳本 Launcher .
讓我們來測試下這個腳本,打開場景 Launcher ,運行它。點擊"Play",讓系統連接并加入一個房間。就是這樣,游戲大廳正在工作。但如果你離開房間,當回到大廳時你將注意到,它會自動重新加入…哎呀,讓我們解決這個問題。如果你還不知道為什么,"simply"分析日志。我把簡單地引用起來,因為分析日志需要實踐和經驗來獲得概述問題的能力,并且知道到哪里去找,以及如何調試它。現在你自己嘗試一下,如果你仍然無法找到問題的根源,讓我們一起做這件事。
1. 運行 Launcher 場景;
2. 點擊"Play"按鈕,等待直到你加入了房間,并且"Room for 1"已被加載;
3. 清除 Unity 控制臺;
4. 點擊"Leave Room"按鈕;
5. 研究 Unity 的控制臺 , 注意到 "DemoAnimator/Launcher: OnConnectedToMaster() was called by PUN"被記錄了;
6. 停止 Launcher 場景;
7. 雙擊該日志條目 "DemoAnimator/Launcher: OnConnectedToMaster() was called by PUN",對應的腳本將被加載并指向對應調試調用的代碼;
8. 嗯 … 所以 ,每次我們收到我們已連接的通知 ,我們會自動加入一個JoinRandomRoom方法。但那不是我們想要的;
要解決這個問題,我們需要了解上下文。當用戶點擊"Play"按鈕時,我們應該發起一個標志來知道連接過程源于用戶。然后我們可以檢查這個標志在各種 Photon 回調中相應行為。
1. 編輯腳本 Launcher;
2. 在私有變量區域 Private Variables regions 內創建一個新屬性;
3. 在 connect() 方法前面添加下列代碼:
// 跟蹤玩家加入一個房間的意愿,因為當我們從游戲中回來時,我們會得到一個我們已連接的回調,所以我們需要知道那個時候該怎么做 isConnecting = true;4. 在 OnConnectedToMaster() 方法中, 用一個如下所示的 if 語句來包裹PhotonNetwork.JoinRandomRoom()
//如果我們不想加入一個房間,我們不想做任何事情。 //這種情況下 isConnecting 是 false,通常是當你掉線或退出游戲時,當這一關卡被加載, OnConnectedToMaster 將被調用,在這種情況下,我們不想做任何事情。 if (isConnecting) { // #Critical | 關鍵: 我們首先要做的是加入一個潛在的現有房間。如果有,很好,否 則,我們將回調 OnPhotonRandomJoinFailed() PhotonNetwork.JoinRandomRoom(); }5. 保存腳本 Launcher
現在如果我們再次測試并運行 Launcher 場景,大廳和游戲之間來回切換,一切都很好 :)? 為了測試場景的自動同步,你將需要發布該應用(發布為桌面版,這是最快的運行測試的平臺),并在 Unity 中同時運行,所以你實際上有兩個玩家將連接和加入房間。如果 Unity編輯器首先創建房間,它將是主客戶端,并且你可以在 Unity 控制臺得到證明:你將得到"PhotonNetwork : Loading Level : 1"以及接下來在你連接已發布實例應用時的
"PhotonNetwork : Loading Level : 2"。
很好!我們已經談了很多,但這只完成了一半… :) 我們需要處理玩家本身了,讓我們在下一節做吧。別忘了按時離開電腦休息一下,這樣可以更有效地吸收我們解釋的各種概念。
如果你對某個特定的功能有疑問,或者如果你在教程中遇到錯誤,或者遇到了在這里沒有涉及到錯誤或問題,請不要猶豫在論壇上提問,我們將樂意幫助 :)
九、創建玩家Player
本節將引導你從零開始創建將在本教程中使用的玩家預設 Prefab,所以我們涵蓋了創作過程的每一步。嘗試并創建一個無需連接PUN就能工作的玩家預設Prefab常常是一個很好的途徑,這樣很容易快速測試、調試以及確保一切在沒有網絡功能的情況下也能工作。然后,你可以建立和慢慢修改,加入網絡兼容的特性:通常情況下,用戶輸入只能在玩家擁有的實例上才能被激活,不在別人的電腦上。我們將在下面詳細介紹。
1.The Prefab Basics | 預制體基礎
你需要了解的第一且最重要的約定就是預設Prefab需要在網絡上被實例化,它需要被放在 Resources 資源文件夾里,否則就不能被找到。第二重要的,在 Resources 資源文件夾里有預設 Prefabs 的副作用是,你
需要注意這些預設的名字。你不應該在你的資產下的 Resources 資源路徑里有兩個命名相同的預制件 Prefab,因為 Unity 你會選擇找到的第一個,所以一定要確保在你的項目資產里,Resources 資源文件夾路徑下沒有兩個預設 Prefabs的命名是相同的。我們很快就會講到。我們將使用 Unity 提供的免費資源 Kyle 機器人來作為玩家預設。它是一個Fbx 文件,可以通過諸如 3ds Max、 Maya、 cinema4d 之流的 3d 軟件來制作 generated。在這些軟件上制作網格和動畫已經超出了本教程的范圍,但創造自己的人物和動畫是必不可少。如下圖所示,這個機器人 Kyle.fbx 位于/Assets/Photon Unity Networking/Demos/Shared Assets/ 路徑下。
下面是開始使用 Kyle Robot.fbx 來制作玩家預設的一種方法:
1. 在你的項目瀏覽器里,在某個地方創建一個名為"Resources"的文件夾,通常建議你組織你的內容,所以可以像這樣 'DemoAnimator_tutorial/Resources/'的路徑;
2. 創建一個新的空場景,并將其保存在 /PunBasics_tutorial/Scenes/ 路徑下,命名為Kyle Test ;
3. 把 Robot Kyle 資源拖拽到場景 Hierarchy 層級上;
4. 把你剛剛在 Hierarchy 層級中創建的游戲對象重命名為 My Robot Kyle;
5. 再把 My Robot Kyle 拖拽進 /PunBasics_tutorial/Resources/ 文件夾下。現在我們已經創建了一個基于 Kyle Robot Fbx 資產的預設 Prefab,并且我們在你的 Kyle Test 場景 Hierarchy 層級中有它的一個實例。現在我們可以開始完善這個預設了。
2.CharacterController | 角色控制器
一、如下圖右側所示,我們來一起為 Hierarchy 層級中的 My Kyle Robot 實例添加一個 CharacterController Component 組件。你也可以直接在預設 Prefab本身上做這個操作,但我們需要對其進行調整,所以這種方式更快。
這些組件是一個由Unity為我們更快的創建使用Animator的典型人物角色而提供的非常方便的標準資產,所以讓我們來一起利用這些偉大的Unity 功能。
二、雙擊 My Kyle Robot 來使場景視圖 Scene View 聚焦到該對象上。如上圖左側所示,注意到 Capsule Collider 在腳的中心,我們實際上需要 Capsule Collider 適當地匹配角色。
三、如上圖 31 右側所示,在 CharacterController Component 組件里把Center.y 屬性改成 1(是其 Height 屬性的一半)。
四、如下圖所示,點擊 Apply 來應用我們所做的改變到預設上。這是非常重要的一步,因為我們完成 My Kyle Robot 預設實例的編輯,但是我們想要這些改變被應用到每一個實例上,不僅僅是這一個,所以我們點擊 Apply 來應用。
3.Animator Setup | 動畫設置
Assigning an Animator Controller | 指派動畫控制器
Kyle Robot Fbx 資源需要被 Animator Graph 控制,如下圖所示。我們在本教程中將不包括創建此圖表,所以我們提供了一個控制器,位于您的項目資Photon Unity Networking/Demos/PunBasics/Animator/ 路徑下,叫做 Kyle Robot
如下圖所示,要指派這個 Kyle Robot 控制器到我們預設 Prefab,簡單地設置預設上的 Animator 組件的 Controller 屬性指向 Kyle Robot 即可。
別忘了,如果你在 My Kyle Robot 的實例上執行該操作的話,你需要點擊 Apply來讓預設 Prefab 本身合并這些改變。
處理控制器參數 | Controller Parameters
要搞清楚 Animator Controller 的關鍵特性就是 Animation Parameters,通過該參數我們可以在腳本里控制我們的動畫。在我們的例子中,我們有參數如Speed , Direction , Jump , Hi 。動畫組件 Animator Component 的其中一個偉大的特點就是能夠真正基于它的動畫四處移動角色,這個功能被稱為 Root Motion,并且在 Animator Component 上有一個屬性 Apply Root Motion 默認為 true,所以我們保持默認即可。
因此,實際上,要有角色行走,我們只需要將 Animation Parameter 動畫參數 Speed 設置為正數值,它就可以開始行走和向前移動。讓我們就這樣做!
Animator Manager Script | 動畫管理腳本
讓我們創建一個新的腳本,在該腳本中我們將根據用戶的輸入來控制角色。
1. 在 DemoAnimator/tutorial/Scripts/ 路徑下創建一個名為 PlayerAnimatorManager 的 C#新腳本;
2. 將該腳本添加到 Prefab 預設 My Robot Kyle 上;
3. 如下面的代碼所示,為你的腳本添加命名空間 Com.MyCompany.MyGame ;
4. 為了結構清晰起見,用 MONOBEHAVIOUR MESSAGES 域把 Start()和 Update()
包裹起來;
5. 保存腳本 PlayerAnimatorManager。
Animator Manager: Speed Control | 速度控制
我們需要編碼的第一件事就是獲取 Animator Component 動畫組件,這樣我們才可以控制它。
1. 確保您正在編輯腳本 PlayerAnimatorManager;
2. 創建一個 Animator 類型的私有變量 animator;
3. 在 Start()方法內把 Animator Component 存儲進這個變量。
4. 注意,因為我們必需一個 Animator Component 動畫組件,如果我們沒有獲取到,我們記錄一個錯誤,這樣可以及時發現問題并由開發者直接解決。你應該總是以別人會使用的方式來寫代碼 :) 這是乏味的,但長期來
看是值得的。
5. 現在讓我們來監聽用戶輸入 User Inputs ,并且控制 Animation Parameter 動畫參數 Speed 。然后保存 PlayerAnimatorManager 腳本。
void Update () { if (!animator) //如果沒有獲取到動畫組件,則用 return 中斷 { return; } float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); if( v < 0 ) //數值校正 { v = 0; } animator.SetFloat( "Speed", h*h+v*v ); //設置速度參數值 }6. 保存腳本 PlayerAnimatorManager
讓我們來研究這個腳本在做什么:
由于我們的游戲不允許玩家后退,我們確保 v 不小于 0。用戶正在按下‘↓’鍵或“s”鍵,我們不允許使用此值,并強制將值改成 0。你也會注意到,我們把兩個輸入都平方化處理,為什么?這樣的話它總是一個正絕對值,以及增加一些 easing。很好的微妙技巧就在這里。你也可以使用Mathf.Abs() ,那也能正常工作。我們還把兩個輸入相加來控制速度 Speed ,以便當只按下左的右輸入,我們仍然獲得一些速度,因為我們轉向。當然所有這一切對于我們的角色設計都很具體,取決于你的游戲邏輯,你可能想角色轉變,或有往回走的能力,所以游戲的動畫控制參數 Animation Parameters 總是非常具體。
Test, test, 1 2 3... | 測試
讓我們來驗證一下我們迄今所做的。確保你已經打開了 Kyle Test 場景。目前,在這個場景中,我們只有一個相機和 Kyle Robot 實例,我們不在乎場景的照明或任何花哨的效果,我們要驗證我們的角色和腳本是否能正常工作。
1. 選擇相機 Camera,并移動它來得到一個很好的總覽視角。一個好的技巧是在場景視圖 Scene View 中獲取你喜歡的視角,選擇相機,然后到菜單"GameObject/Align With View",這樣像機將匹配場景視圖的視角。
2. 最后一步,把 My Robot Kyle 在 y 軸上向上移 0.1,否則碰撞在開始時就丟失了,角色會直接穿過地面,所以總是在碰撞器之間預留一些物理空間,來模擬接觸。
3. 運行場景,按下‘↑’或“A”鍵,角色正在行走!你可以用所有的鍵來驗證測試。這很好,但仍然有大量的工作在我們面前,相機需要跟隨,而且我們還不能轉彎…如果你現在就像完成攝像機,可以直接跳到像機部分的內容,接下來的頁面將完成動畫控制和實現轉彎。
Animator Manager Script: Direction Control | 方向控制
控制旋轉會稍微復雜一點,我們不希望我們的角色在我們按左鍵和右鍵時突然旋轉,我們希望平緩和平滑的旋轉。幸運的是,一個動畫參數 Animation Parameter 可以設置一些阻尼。
1. 確保您正在編輯腳本 PlayerAnimatorManager;
2. 在腳本的新的域"PUBLIC PROPERTIES"內創建公共浮點數變量DirectionDampTime。
3. 在 Update()函數末尾加上:
animator.SetFloat( "Direction", h, DirectionDampTime, Time.deltaTime );4. 所以我們馬上注意到 animator.SetFloat()有不同的用法。我們用來控制速度 Speed 的是簡單直接的,但是對于這一個需要多兩個參數,一個阻尼時間,一個是 deltaTime。阻尼時間是有意義的:它是需要多久才能達到期望值,但 deltaTime?它本質上讓你寫的代碼獨立于幀速率,因為Update()是依賴于幀速率的,我們需要通過使用 deltaTime 來計數。盡可能多地閱讀關于這個主題知識,以及那些當你在網上搜索時你會發現的內容。只有在你之后理解這個概念后,當涉及到動畫和隨著時間推移值的一致性控制,你就可以充分利用到 Unity 的許多功能;
5. 保存腳本 PlayerAnimatorManager;
6. 運行您的場景,并使用所有的箭頭鍵來看看你的角色是如何走動和轉身;
7. 測試 DirectionDampTime 的效果:例如將其值改為 1,然后 5,并看看角色需要多長時間來達到最大彎度。你將發現轉彎半徑隨著 DirectionDampTime 增加。
Animator Manager Script: Jumping | 跳躍
對于跳躍,我們需要更多的工作,因為兩個因素。一、我們不希望玩家在沒有奔跑的情況下跳躍;二,我們不想玩家循環跳躍。
1. 確保您正在編輯腳本 PlayerAnimatorManager;
2. 在方法里,在我們捕捉用戶輸入之前插入這些代碼。
3. 保存腳本 PlayerAnimatorManager;
4. 測試。開始運行,按“Alt”鍵,凱爾應該會跳躍。好的,首先要了解我們如何知道動畫是否在運行,我們使用stateInfo.IsName("Base Layer.Run") 來判斷。我們只是想問動畫當前活動狀態是否是Run 。我們必須追加 Base Layer ,因為 Run 狀態位于 Base Layer 中。如果我們在奔跑的狀態中,那么就可以監聽 Fire2 Input 輸入,并且啟用 Jump觸發器是必要的。所以,這是目前的完整 PlayerAnimatorManager 腳本:
當你考慮到它在場景中實現的功能時,對幾行代碼來說不是太壞。現在,讓我們處理相機的工作,因為我們能夠在我們的世界發展,我們需要一個適當的相機行為來跟隨。
4.Camera Setup | 攝像機設置
在本章中,我們將使用 CameraWork 腳本來完成攝像機對玩家的跟隨。如果你想從零開始寫 CameraWork ,請看下一部分,在完成后再回來看這一部分。
1. 添加 CameraWork 組件到 My Kyle Robot 預設;
2. 開啟 Follow on Start 屬性,這樣可以使攝像機立即跟隨角色。當我們將開始網絡實現的時候我們將把它關掉;
3. 把 Center Offset 屬性設置成 0,4,0 ,這可以使攝像機看得更高,這樣可以給出更好的環境視角,而不是直接盯著玩家,我們將看到太多的地面;
4. 運行 Kyle Test 場景,讓角色四處走走來測試攝像機是否恰當地跟隨角色。
5.Beams Setup | 射線設置
我們的機器人角色仍然缺少它的武器,讓我們創造一些激光束從其眼睛內射出來。
Adding the Beams models | 添加射線模型
為了簡單起見,我們將使用簡單的立方體和將其縮放成非常薄和長。有一些技巧可以將其很快完成:不要直接將一個立方體添加為頭部的子物體,而是單獨創建它移動它,并單獨進行縮放,然后附加到頭部,它將防止猜測適當的旋轉值,該值使您的光束與眼睛對齊。另一個重要的技巧是在兩個光束中只使用一個碰撞器。這是為了讓物理引擎的工作得更好,薄的碰撞器絕不是一個好主意,因為碰撞器太薄是不可靠的,所以我們要做一個大的碰撞器來使我們確定能夠可靠地打擊到目標。
1. 打開 Kyle test 場景;
2. 添加一個立方體到場景中,并命名為 Beam Left;
3. 修改它,使其看起來像一個長的光束,并妥善定位于左眼;
4. 在 Hierarchy 層級內選擇 My Kyle Robot 實例;
5. 如下圖所示,定位到 Head 子類;
6. 添加一個空對象作為 Head 游戲對象的子類,并命名為 Beams;
7. 把 Beam Left 拖拽進 Beams;
8. 如下圖所示,復制 Beams Left , 并命名為 Beams Right;
9. 定位它,以便它與右眼對齊;
10. 從 Beams Right 中移除 Box Collider 組件;
11. 調整 Beams Left 的 Box Collider 中心和大小來封裝兩個光束;
12. 把 Beams Left 的 Box Collider 組件的 IsTrigger 屬性改成 True ,我們只想在射線觸碰到玩家時被通知,并且沒有碰撞;
13. 創建一個新的材質, 命名為 Red Beam , 將其保存在'DemoAnimator_tutorial/Scenes/'路徑下;
14. 把 Red Beam 材質指派給兩個射線;
15. 把更改應用到預設上(點擊實例上的 Apply 即可)。
如下圖所示,你現在應該有這樣的東西了:
Controlling the Beams with User Input | 用戶控制射線
好吧,現在我們有射線了,讓我們插入發射輸入來觸發它們。讓我們創建一個新的 C #腳本,叫做 PlayerManager 。下面是第一個版本的完整代碼,來使射線工作。
using UnityEngine; using UnityEngine.EventSystems; using System.Collections; namespace Com.MyCompany.MyGame { /// <summary> /// 玩家總管,處理發射輸入和射線。 /// </summary> public class PlayerManager : MonoBehaviour { #region Public Variables //公共變量區域 [Tooltip("The Beams GameObject to control")] public GameObject Beams; //要控制的射線 #endregion #region Private Variables //私有變量區域 //當玩家發射的時候為 true bool IsFiring; #endregion #region MonoBehaviour CallBacks //Mono 行為回調 void Awake() { if (Beams==null) //如果射線為空,則報錯 { Debug.LogError("<Color=Red><a>Missing</a></Color> Beams Reference.",this); }else{ Beams.SetActive(false); } } void Update() { ProcessInputs (); //觸發射線的激活狀態 if (Beams!=null && IsFiring != Beams.GetActive ()) { Beams.SetActive(IsFiring); } } #endregion #region Custom //自定義區域 /// <summary> /// 處理輸入。當用戶按下發射時,維護一個標志。 /// </summary> void ProcessInputs() { if (Input.GetButtonDown ("Fire1") ) { if (!IsFiring) { IsFiring = true; } } if (Input.GetButtonUp ("Fire1") ) { if (IsFiring) { IsFiring = false; } } } #endregion } }在這個階段,這個腳本主要是激活或停用射線。當被激活時,射線將在與其他模型發生碰撞時有效地觸發,所以我們將在觸發后捕捉這些觸發器來影響每個角色的健康值或體力。我們也暴露出公共屬性 Beams ,將讓我們引用在 My Kyle Robot 預設 Prefab 的層級內部的具體對象。讓我們看看要如何辦才能連接 Beams ,因為這在預設內部是棘手的,由于在資產瀏覽器里面,預設只暴露出第一個子類,并非其他子類,并且我們的射線確實是埋在預制體的層次中,所以我們需要做的是從場景中的一個實例上獲取,然后將它應用到預制體本身。
1. 打開 Kyle test 場景;
2. 在場景層級中選擇 My Kyle Robot;
3. 添加 PlayerManager 組件到 My Kyle Robot;
4. 把 My Kyle Robot/Root/Ribs/Neck/Head/Beams 拖拽進 PlayerManager 在 Inspector窗口里的 Beams 屬性里;
5. 把實例上的改變應用到預設上。
如果你點擊運行,并且按下 Fire1 Input 輸入(默認是鼠標左鍵或左 Ctrl 鍵),射線將出現,并在你松開時立即隱藏。
6.Health Setup | 體力設置
讓我們實現一個非常簡單的健康系統,體力值會在射線擊中玩家的時候減少。因為它不是子彈,而是源源不斷的能量,我們需要用兩種方法來計算傷害,當我們被射線擊中,并且射線一直擊中我們。
1. 打開 PlayerManager 腳本;
2. 把 PlayerManager 轉化成Photon.PunBehaviour來暴露PhotonView組件:
3. 在 Public Variables 區域添加一個 Health 公共屬性:
[Tooltip("The current Health of our player")] //玩家的當前體力值 public float Health = 1f;4. 在 MonoBehaviour CallBacks 區域添加下面這兩個方法,然后保存 Player Manager 腳本。
/// <summary> /// 當碰撞器'other'進入觸發器時調用的 MonoBehaviour 方法。 /// 如果碰撞器是射線就會影響到玩家的體力值 /// 注:當跳躍的同時射擊,你會發現自己的射線和自身發生交互 /// 你可以把碰撞器移動稍遠一些,以防止這樣的 Bug 或檢查光束是否屬于玩家。 /// </summary> void OnTriggerEnter(Collider other) { if (! photonView.isMine) { return; } //我們只對敵人感興趣,我們可以通過標簽來區分,也可以簡單地檢查名稱 if (!other.name.Contains("Beam")) { return; } Health -= 0.1f; } /// <summary> /// 每一個'other'碰撞器觸摸這個觸發器的時候每幀調用的 MonoBehaviour 方法 /// 當射線持續觸碰玩家時我們將繼續影響體力值。 /// </summary> /// <param name="other">其他碰撞器</param> void OnTriggerStay(Collider other) { //如果不是本地玩家,什么都不做 if (! photonView.isMine) { return; } if (!other.name.Contains("Beam")) { return; } //當射線持續擊中我們的時候我們緩慢地影響體力值,這樣玩家不得不移動來防止被擊 殺。 Health -= 0.1f*Time.deltaTime; }5. 保存 PlayerManager 腳本。
首先,兩方法幾乎完全相同,唯一不同的是,我們在 TriggerStay 中使用Deltatime 來減少體力值,遞減速度不應與幀率相關。這是一個重要的概念,通常適用于動畫,但在這里,我們也需要這樣,我們希望體力值在所有的設備上以可預見的方式減少,在一個更快的計算機上,你的體力值減少得越快 :) ,這是不公平的,Deltatime 來保證該值減少的一致性。如果你有問題的話可以給我們發郵件,通過搜索 Unity 社區來學習 DeltaTime,直到你完全了解 DeltaTime概念,它是必要的。第二重要的方面,那現在應該明白的是,我們只影響本地玩家的體力值,這就是為什么我們在 PhotonView 不是 Mine 時提前退出方法。
最后,我們只想如果擊中我們的對象是射線時才影響體力值,所以我們使用標簽"beam"來檢查,這是我們如何標簽我們的射線對象的。為了便于調試,我們使體力值作為一個公共浮點數,以便在我們等待 UI 被構建時檢查它的值。
好吧,這看起來全都做對了嗎?呃...健康系統沒有考慮到玩家的游戲結束狀態是不完整的,當體力值達到 0 時觸發該狀態,讓我們現在來完成這個。
Health checking for game over | 檢查體力來結束游戲
為了使事情簡單,當玩家的體力值達到 0,我們就離開房間,如果你記得,我們已經在 GameManager 腳本里創造了離開房間的方法。如果我們可以重復使用這種方法,而不是為相同的功能編碼兩次,這才是最好的做法。為同樣的結果重復代碼是你應該不惜一切代價避免的。這也將是一個很好的介紹時機來引入一個非常方便的編程概念,"Singleton"。雖然這個主題本身可以填補一些教程,我們只會做"Singleton"最小化實現。要了解 Singleton,它們在 Unity 上下文中的變體,以及它們如何幫助創建強大的功能是非常重要的,將為您節省很多麻煩。所以不要猶豫在這個教程之外花更多時間來了解更多關于 Singleton 的知識。
1. 打開 GameManager 腳本
2. 在 Public Properties 區域添加這個變量
3. 在 Start()方法中添加這個代碼
Instance = this;4. 保存 GameManager 腳本
注意我們用[static]關鍵字來修飾實例變量,這意味著這個變量可無需持有一個指向 GameManager 實例的指針就可用,所以你可以簡單地在你的代碼的任何地方使用 GameManager.instance.xxx() 。這確實很實際!讓我們看看單例是如何在邏輯管理上適合我們的游戲。
1. 打開 PlayerManager 腳本
2. 在 Update()方法中,在我們 ProcessInputs 之后,添加下面這個判斷,并保存 PlayerManager 腳本
3. 保存 PlayerManager 腳本
注意,我們考慮到體力值可能是負的,因為激光束造成的損害強度不同。注意我們沒有得到任何組件就使用了GameManager實例的LeaveRoom()公共方法,我們只是依賴于這樣的事實:我們認為在當前場景中的一個游戲對象上有一個 GameManager 組件。
好吧,現在我們進入網絡!
十、創建玩家攝像機腳本
本部分將引導您創建 CameraWork 腳本來讓攝像機在你玩游戲時跟隨你。
這一部分與網絡無關,所以它會保持簡短。
創建 CameraWork 腳本
1. 創建一個新的 C #腳本,叫做 CameraWork
2. 用下面的代碼來替換 CameraWork 腳本里面的內容 :
3. 保存腳本 CameraWork。
如果你剛剛開始實時 3D、向量和四元數為基礎的數學,跟隨玩家背后的數學是棘手的。所以我不會在這個教程內不遺余力地試圖解釋。但是如果你好奇并想學習,不要猶豫與我們聯系,我們會盡全力解釋。然而,這個腳本不只是關于瘋狂的數學,重要的也是設置;當攝像機的行為應該積極地跟隨玩家時有能力去控制。并且理解這一點很重要:為什么我們在攝像機跟隨玩家時想要去控制。通常情況下,如果它總是跟隨玩家,讓我們想象會發生什么。當你連接到一個滿是玩家的房間,在其他玩家上的每一個 CameraWork 腳本都會努力控制主攝像來讓其看到自己的玩家…好吧,我們不想這樣,我們只想跟隨本地玩家。一旦我們定義了這個問題,我們只有一個攝像機,但有幾個玩家的實例,我們可以很容易地找到幾種方法去解決這個問題。
1. 只在本地玩家上依附 CameraWork 腳本。
2. 通過關閉 CameraWork 來控制其行為,并根據是否是本地玩家來進行跟隨。
3. 把 CameraWork 依附到攝像機上并時刻注意場景中的本地玩家實例,然后只跟隨本地玩家。
這 3 個選項并不詳盡,可以找到更多的方法,但從這 3 個,我們將任意選擇第二個。上述選項沒有一個更好或更壞,但這可能是一個需要編碼最少和最靈活… "有趣..." 我聽到你說 :)
? 我們已經暴露了公共屬性 followOnStart ,如果我們想用在非網絡環境上就將其設置為 true,例如在我們的測試場景里,或完全不同的場景里。在我們的網絡游戲運行的時候,當檢測到本地玩家時我們會調用OnStartFollowing() 公共方法。這將在 Player Prefab Networking 章里創建并解釋的 PlayerManager 腳本里完成。
十一、把玩家修改成網絡預設
本部分將引導您修改玩家預制體。我們首先創建了一個玩家,但現在我們要修改它使其在我們在 PUN 環境里使用的時候能夠工作和兼容。非常輕量的修改,但概念是至關重要的。所以這部分確實很重要。
1.PhotonView Component | PhotonView 組件
首先,我們需要在我們的預設上添加一個 PhotonView 組件。一個 PhotonView 就是將不同的實例在每臺電腦上連接起來的組件,并定義觀察什么組件以及如何觀察這些組件。
1. 添加一個 PhotonView 組件到 My Robot Kyle
2. 設置 Observe Option 為 Unreliable On Change
3. 注意 PhotonView 提醒你,你需要觀察的東西對其有任何影響
讓我們一起來設置我們將要觀察的,然后我們將會回到這個 PhotonView 組件并完成它的設置。
2.Transform Synchronization | 同步 Transform
我們想要同步的一個明顯的特征是角色的位置和旋轉,這樣當一個玩家移動時,其他計算機上的其他玩家代表以同樣的方式移動和旋轉。您可以直接觀察到自己腳本中的 Transform 組件,但是由于網絡延遲和數據同步的有效性,您會遇到很多麻煩。幸運的是為了讓這個共同的任務更容易,我們將使用一個[Photon Transform View]組件,作為 Transform 組件和 PhotonView 之間的中間人。基本上,用這個組件把你要做的所有難點都解決了。
1. 添加一個 PhotonTransformView 組件到'My Robot Kyle'預設
2. 如下圖所示,拖拽 PhotonTransformView 組件的標題將其放進 PhotonView 組件的第一個觀察組件條目里(文中的 Gif 圖請參考官網)
3. 現在檢查 PhotonTransformView 里的 Synchronize Position
4. 在 Synchronize Position 里,選擇"Lerp Value for Interpolation Option"
5. 設置 Lerp Speed 為 10 (這個值越大就能越快追上)
6. 如下圖所示,檢查 SynchronizeRotation
提示: 注意如下圖所示的藍皮書幫助鏈接。點擊它揭示信息,了解所有的各種設置和它們的影響。
3.Animator Synchronization | 動畫同步
PhotonAnimatorView 組件也使得網絡設置輕而易舉,并且幫你省去了大把的時間和麻煩。它允許您定義哪些層權重和哪些參數要同步。層權重只需要在游戲中發生改變時同步,有可能逃脫而完全沒有同步。參數也一樣。有時它可能從其他因素中獲得動畫值。一個速度值是一個很好的例子,你不必需要精確地同步這個值,你可以使用同步位置更新來估計它的值。如果可能的話,盡量少同步參數。
1. 添加一個 PhotonAnimatorView 組件到 My Robot Kyle 預設
2. 把 PhotonAnimatorView 拖拽到 PhotonView 組件的新的觀察組件條目上
3. 現在,在同步參數中,設置 Speed 為 Discrete
4. 設置 Direction 為 Discrete
5. 設置 Jump 為 Discrete
6. 設置 Hi 為 Disabled
每個值都可以被禁用,或同步(無論是離散或連續)。在我們的情況下,因為我們不使用 Hi 參數,我們將禁用它,并節省帶寬。Discrete synchronization 離散同步意味著一個值每秒被發送 10 次(在OnPhotonSerializeView 里面)。接收客戶端把值傳遞給本地的動畫。
Continuous 連續同步意味著 PhotonAnimatorView 每幀運行。當 OnPhotonSerializeView被調用(每秒 10 次),該值自從上次被調用就開始記錄,再一起發送。接收客戶端然后按序列應用這些值,以保持平穩過渡。雖然這個模式是平滑的,它也發送更多的數據來實現這種效果。
4.User Input Management | 用戶輸入管理
在網絡用戶控制的一個關鍵方面是相同的預制件將在所有的玩家客戶端上被實例化,但其中只有一個角色是代表在當前電腦前玩游戲的用戶,所有其他實例代表在其他電腦上玩游戲的其他用戶。因此,首先要考慮的就是輸入管理。我們怎樣才能使在一個實例上啟用輸入而不是其他,如何知道哪個是正確的?引入 isMine 概念。
讓我們來一起編輯我們之前創建的 PlayerAnimatorManager 腳本。在當前形態下,該腳本并不知道其中的區別,讓我們來一起實現這個功能。
1. 打開腳本 PlayerAnimatorManager
2. 把 PlayerAnimatorManager 類 的 基 類 從 MonoBehaviour 改 成Photon.MonoBehaviour,從而很方便地暴露 photonView 組件。
3. 在 Update() 調用里,在最開始插入下面的代碼
4. 保存腳本 PlayerAnimatorManager
5. 好的,如果實例由'client'應用控制 PhotonView.isMine 將是 true,意味著這個實例代表的就是在這臺電腦上玩這個游戲的本地玩家。所以如果它是 false,我們不想做任何事,并且像我們早先設置好的那樣僅僅依靠 PhotonView 組件來同步Transform 和動畫組件。
6. 但是,為什么又要在if語句里面判斷 PhotonNetwork.connected == true ?呃 嗯 :) 因為在開發過程中,我們也許想要在沒有連接的情況下測試這個預設。例如在虛擬場景中,只是創建和驗證與網絡功能無關的代碼。所以有這個語句我們就可以允許在不聯網的情況下輸入。這是一個非常簡單的技巧,將在開發過程中大大提高您的工作流程。
5.Camera Control | 攝像機控制
和上一部分講的輸入一樣,玩家只有一個游戲視圖,所以我們需要 CameraWork 腳本,來控制攝像機只能跟著本地玩家,而不是其他玩家。這就是為什么 CameraWork 腳本有定義何時跟隨的能力。讓我們一起修改 PlayerManager 腳本來控制 CameraWork 組件。
1. 打開 PlayerManager 腳本.
2. 把下面的代碼插入到 Awake() 和 Update() 方法之間。
3. 保存腳本 PlayerManager
首先,獲取到 CameraWork 組件,我們想到,如果我們找不到它,我們記錄一個錯誤。然后,如果 photonView.isMine 是 true ,這意味著我們需要跟隨這個實例,所以我們調用_cameraWork.OnStartFollowing() ,該方法有效地讓相機跟隨場景中的對應實例。所有其他玩家實例將把它們的 photonView.isMine 設置為 false ,這樣它們的對應_cameraWork 將什么也不做。使其運行起來的最后一個改動:
1. 如下圖所示,在 My Robot Kyle 預設上的 CameraWork 組件上禁用 Follow on Start
這樣就把跟隨玩家的邏輯都交給了 PlayerManager 腳本,該腳本將按照上面描述的那樣調用 _cameraWork.OnStartFollowing() 。
6.Beams Fire Control | 射線發射控制
發射同樣遵循上述的輸入原則,它只在 photonView.isMine 是 true 的時候工作:
1. 打開腳本 PlayerManager
2. 用 if 語句來包裹輸入調用。
3. 保存腳本 PlayerManager
然而,在測試時,我們只看到了本地玩家射擊。我們需要查看其他實例什么時候射擊!我們需要一個同步整個網絡發射的機制。要做到這一點,我們要去手動同步 IsFiring 布爾值,直到現在,我們得到了 PhotonTransformView 和 PhotonAnimatorView 來我們做所有變量的內部同步,我們只有調整那些通過 Unity 的 Inspector 方便地暴露出來的值,但在這里我們所需要的對于你的游戲來說是非常具體的,所以我們需要手動來做。
1. 打開腳本 PlayerManager
2. 實現 IPunObservable
3. 在 IPunObservable.OnPhotonSerializeView 里面加入下列代碼
if (stream.isWriting) { // 我們擁有該玩家:把我們的數據發送給其他玩家 stream.SendNext(IsFiring); }else{ //網絡玩家,接收數據 this.IsFiring = (bool)stream.ReceiveNext(); }4. 保存腳本 PlayerManager
5. 回到 Unity 編輯器,在資源里面選擇 My Robot Kyle 預設,并在 PhotonView 組件上添加一個觀察條目,把 PlayerManager 組件拖拽到該條目上。
沒有這最后一步,IPunObservable.OnPhotonSerializeView 絕不會被調用,因為沒有被 PhotonView 觀察。
在這個 IPunObservable.OnPhotonSerializeView 方法里面,我們傳遞一個 stream 變量,這是將被通過網絡發送的,并且如果我們進行讀寫數據也會調用。我們只有是本地玩家( PhotonView.isMine == true )時才寫入,否則我們就讀取。
由于 stream 類有助手,知道要做什么,我們僅僅依靠 stream.isWriting 來知道在當前實例想要什么。如果我們想要寫入數據,我們把 IsFiring 值追加到數據流,使用 stream.SendNext() ,該方法非常方便地隱藏了所有數據序列化的艱苦工作。如果我們想讀取,使用stream.ReceiveNext()。
Health Synchronization | 同步體力值
好了,要完成為網絡更新玩家的功能,我們將同步的健康值,使每個玩家的實例將有正確的健康值。這和上述講的 IsFiring 值是完全相同的原則。
1. 打開腳本 PlayerManager
2. 在 IPunObservable.OnPhotonSerializeView 里面,在你 SendNext 和 ReceiveNext 變量IsFiring 之后,為 Health 執行一樣的操作
十二、玩家網絡實例
十三、玩家UI預設Prefab
十四、匹配指南
十五、實例化
十六、同步和狀態
十七、RPCs和RaiseEvent
十八、所有權轉移
十九、優化技巧
二十、剔除演示
二十一、PUN案例
二十二、動作演示
二十三、Mecanim案例
二十四、Photon Animator View(觸發器)
二十五、2D跳和跑案例
二十六、回合制游戲案例
總結
以上是生活随笔為你收集整理的【持续更新】Pun多人在线游戏开发教程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 项目实训(十四)pun的建立,使用以及注
- 下一篇: PUN☀️八、拓展网络同步:RPCs 和