# Unet 連線 ## 聲明 此書籍文章內容從翻譯 https://docs.unity3d.com/Manual/UNetOverview.html 而來。 並以CC-BY-SA 4.0 授權釋出。 ## 概觀 這裡有兩種連線功能提供給使用者: * 使用者使用 Network Manager 或是高層級API,建立多人連線遊戲。 * 使用者如果要建立連線的基礎或是進階的連線遊戲。需使用 NetworkTransport API (網路傳輸API)。 ### 高層級API Unity 的連線功能提供了高層級的API(下稱HLAPI)。使用這個代表你可以使用這涵蓋所有所需的連線機制,而且不用管一些低層級的機制是如何運作的。HLAPI 提供了: * 使用 Network Manager 控制連線遊戲的狀態。 * 操作當莊家的遊戲,也就是莊家也是遊戲玩家。 * 使用一般用途的序列化器來序列化資料。 * 送出和接收網路訊息。 * 從客戶端送出網路的 Command (命令)到伺服器端。 * 從伺服器端送出 RPC (遠端方法呼叫) 到客戶端。 * 從伺服器端送出網路事件到客戶端。 ### 引擎和編輯器整合 Unity 的連線整合至引擎和編輯器,允許你在 Component 和視覺的支援下建立起你的多人連線遊戲。它提供: * NetworkIdentity Component 給要連線的物件。 * NetworkBehaviour 給要連線的腳本。 * 可自訂物件的 Transform 的同步。 * 支援在場景中放置需連線的物件。 * 網路相關的 Component 。 ### 網路服務 先不急著翻。 ### NetworkTransport(底層傳輸層級) 底層傳輸層級提供: * 以 UDP 協定為基礎並優化。 * 多通道設計來預防[隊列頭的阻塞](https://zh.wikipedia.org/wiki/%E9%98%9F%E5%A4%B4%E9%98%BB%E5%A1%9E)。 * 提供多種不同等級的服務品質(QoS)給每個通道。 * 靈活的網路拓樸,且支援P2P或是主從式架構。 ### 範例 [此網址](https://forum.unity.com/threads/unet-sample-projects.331978/)堤供幾個連線範例。 ## 高層級API 高層級API(下稱 HLAPI )是給用 Unity 製作的遊戲,有能力連線的系統。它建置在低層級的即時傳輸溝通層上,且控制許多連線所需要的任務。傳輸層支援各種網路拓樸,但 HLAPI 是伺服器主導為設計的系統。然而它允許一位參與者同時可以是客戶端和伺服器端,所以不需要專職的伺服器。 HLAPI 是一組內建在 Unity 連線的命令,並使用 UnityEngine.Networking 命名空間。它著重於方便使用和互動式開發,並提供好用的機能給多人連線遊戲,像是: * 訊息控制器。 * 一般用途且高效能的序列化。 * 分派的物件管理。 * 狀態同步。 * 網路類別:伺服器、客戶端等等。 HLAPI 是從一系列的層級中包裝上來: ![](https://docs.unity3d.com/uploads/Main/NetworkLayers.png) --- ### 網路遊戲物件 網路遊戲物件是被Unity的多人連線系統所控制和同步。使用同步的遊戲物件,你可以產生共享的經驗給所有正在遊玩你的遊戲實體的玩家。他們看到並聽到相同的事件和動作,儘管是從他們遊戲中的視角中。 在Unity的多人連線遊戲是典型的包含網路遊戲物件、非網路的遊戲物件混合而成的場景所組成。網路遊戲物件為那些在遊戲中,需要同步移動或改變給所有在遊玩的使用者。非網路的遊戲物件為那些不須在遊戲中改變或移動(舉例來說,像是石頭和圍籬等固定的障礙物)或是會移動的遊戲物件但不須同步給每位玩家(舉例來說,像是優雅擺動的樹和在遊戲中飄過的雲)。 網路遊戲物件是掛載 **Network Identity** 的組件。然而,只賦予 Network Identity 組件給你的遊戲物件是不足以在你的遊戲中產生效果。 Network Identity 組件是用來同步的第一步,且其允許 **Network Manager** 來同步遊戲物件的產生和刪除,但除了這些,它並沒有釐清你遊戲物件的屬性哪些要被同步。 準確來說必須被同步的每個遊戲物件取決於你製作的遊戲類型和每個遊戲物件的用途。以下是一些你也許要同步的範例: * 移動遊戲物件的**位置**和**旋轉**,像是玩家物件和非玩家物件。 * 具有動畫的遊戲物件的**動畫狀態**。 * 變數的**值**,舉例來說像是每場遊戲所經過的時間、玩家目前有的能量。 這些事情之中有些可以被Unity自動地同步。網路遊戲物件自動同步的產生和刪除是被 NetworkManager 所管理,並有所熟知的產生(**Spawning**)。你可以使用 Network Transform 組件來同步遊戲物件的位置和旋轉角度,而且你可以使用 Network Animator 組件來同步遊戲物件的動畫。 要同步網路遊戲物件的其他屬性,你需要使用腳本。詳見[狀態同步](#狀態同步)來了解。 --- #### 玩家遊戲物件 UNet 的高層API是以不同於非玩家物件的方式控制玩家物件。當新玩家加入了遊戲 (當新客戶端連接到伺服器端),該玩家的遊戲物件會在客戶端中成為「本地端玩家」遊戲物件,並且 Unity 會關聯該玩家的連線至該玩家的遊戲物件。Unity 會關聯玩家遊戲物件給每個正在遊玩遊戲的每個人,並導引[網路事件](#遠端動作)給這獨立的遊戲物件。玩家無法發送[網路事件](#遠端動作)給其他玩家的遊戲物件,除了自己的以外。 **NetworkBehaviour** 類別(你從自己的網路腳本中繼承而來的)中有個屬性稱為 **isLocalPlayer**。在每個客戶端的玩家遊戲物件, Unity 在 NetworkBehaviour 腳本中設定此屬性為真,並觸發 **OnStartLocalPlayer()** 回呼。這代表每個每個客戶端有不同的遊戲物件都像這樣被設定,因為在每個客戶端中不同的遊戲物件會有一個代表本地端玩家。下方的圖表表示兩個客戶端和他們的本地端玩家。 ![](https://docs.unity3d.com/uploads/Main/NetworkLocalPlayers.png) 只有是屬於「你的」玩家遊戲物件(從你作為玩家的觀點來看)會已經設定 **isLocalPlayer**。經常你會在操控輸入端、使攝影機跟隨遊戲物件或是做其他只會在屬於此客戶端的玩家身上才發生的事情的時候,會使用到此旗標。 玩家遊戲物件代表在伺服器端上的玩家(就是遊玩遊戲的人),並且有能力從客戶端執行網路事件。這些網路事件是固定客戶端到伺服器端的[遠端函式呼叫](#遠端動作)。在這伺服器權威的系統中,其他在伺服器端的遊戲物件無法直接收到從客戶端遊戲物件來的命令。這皆為了安全並且減少製作遊戲的複雜度。藉著導引從玩家藉由遊戲玩家物件所送來的命令,你可以確定這些訊息從對的地方、對的客戶端而且可以從集中的地方去控制。 Network Manager 在每次客戶端連接到伺服器的時候新增玩家。在一些情況考量下,你也許想要先不產生玩家,直到輸入事件發生—像是使用者在控制器按下了「開始」按鈕。要關閉自動玩家產生,瀏覽 Network Manager 組件的面板,取消核取 **Auto Create Player**。 --- #### 場景物件 動態被產生的物件都是由 **NetworkServer.Spawn()** 方法加到網路中,但是已存在場景上的物件是以不同的方式去控制。 這些物件會在伺服器端和客戶端中隨著場景被載入,並在任何產生 (Spawn) 訊息被送出之前存在於執行期中。 在伺服器端和客戶端中,場景已經載入的時候,其中掛有 **NetworkIdentity** 組件的物件會被關閉。 之後,當場景完全被載入之後,**NetworkServer.SpawnObjects()** 會被呼叫來啟動這些有網路機制的場景物件。當伺服器場景完成載入—或是被使用者的程式碼呼叫時,這些事情會自動地被 NetworkManager 完成。 這造成帶有網路機制的場景物件,會以特殊的方式被產生 — 取代新實體被產生的方式,存在中的實體會被掛勾於網路上。 這裡有一些好的理由來使用場景物件,而不是動態產生物件。這些物件是: * 隨著場景被載入,所以不須在執行期時被中斷。 * 可以有一些和預製物件 (Prefab) 不相同的特殊改動。 * 可以被其他在同場景下的物件實體當作參考,可以避免在執行期中去尋找並掛勾他們。 一但場景物件藉由 NetworkServer.SpawnObjects() 被產生之後,它們會像其他被產生的物件一樣。更新會被送出,而且 ClientRPC 訊息也可被產生。 假如場景中的物件,在客戶端加入遊戲前被刪除的話; 它將不會在新加入的客戶端中被產生。 帶有網路機制的場景物件會在啟動網路的時候被關閉。任何掛有NetworkIdentity 的場景物件都是網路場景物件。 在伺服器端上,當啟動或是場景完成載入時,這些物件會在**NetworkServer.SpawnObjects()** 被呼叫時啟動。 當客戶端連接時,客戶端會接收到在伺服器端上存在的每個場景物件,而且也在客戶端中可見,所傳來的 **ObjectSpawnScene** 產生訊息。 這些訊息造成在客戶端中的物件會被啟用,並擁有從伺服器端得來的最新狀態。所以只有客戶端物件是可見,而且在伺服器端沒被移除的話,將會在客戶端中被啟用。 而且像一般的非場景物件,這些場景物件在客戶端加入遊戲的時候,將會以最新狀態啟動。 --- ### 動作和溝通 #### 狀態同步 狀態同步是從伺服器端到遠端客戶端中來完成。自從和伺服器端共用場景時,本地客戶端不會有資料序列化。任何資料序列化給本地客戶端是多餘的。然而 SyncVar 會在本地客戶端被呼叫時掛勾上去。 資料不會從遠端客戶端同步到伺服器端,那是 命令(Command) 的工作。 ##### 同步變數 同步變數(SyncVar) 是從伺服器同步到客戶端, NetworkBehaviour 腳本中的成員變數。當物件被產生(Spawn)或是新玩家在半途中加入遊戲時,它們身上可見的的網路物件會收到所有同步變數的最新狀態。藉由使用 [SyncVar] 自訂標籤,可讓成員變數要轉變為同步變數: ``` class Player : NetworkBehaviour { [SyncVar] int health; public void TakeDamage(int amount) { if (!isServer) return; health -= amount; } } ``` 同步變數的狀態會在 OnStartClient() 被呼叫之前套用到客戶端上的物件,所以物件的狀態被保證會在 OnStartClient() 之中是最新的狀態。 同步變數可以是整數、字串或浮點數等基本型別。它也可以像是 Vector3 等等的 Unity 內建型別或是使用者定義結構,但是結構類型的同步變數更新時,是以單體方式來送出,而並非結構內的欄位更新時,遞增去做更新。一個 NetworkBehaviour 腳本最多可以擁有 32 個同步變數,這包含了同步陣列。 當同步變數在伺服器端被改變時,更新會自動的被送出。並不需要為同步變數去執行任何手動的更變標記(Dirtying)。 ##### 同步陣列 同步陣列(SyncList) 類似同步變數,但是是以值得集合取代獨立的值。同步陣列的內容包含了同步變數的初始狀態更新。同步陣列不需要 SyncVar 標籤,它們是特製的類別。這些是內建設定給基本型別的同步陣列類型: * SyncListString * SyncListFloat * SyncListInt * SyncListUInt * SyncListBool 也有可被用在使用者定義結構的 SyncListStruct 類別。使用自訂結構且繼承 SyncListStruct 類別的成員可以包含基本型別、陣列和常見的 Unity 型別。它們不可以包含複雜的類別或是泛型容器。 同步陣列有個以 SyncListChanged 委派實作的回傳,允許客戶端在陣列改變時收到通知。 此委派被呼叫時會攜帶所發生的運算狀況、和所改變的元素索引值。 ``` public class MyScript : NetworkBehaviour { public struct Buf { public int id; public string name; public float timer; }; public class TestBufs : SyncListStruct<Buf> {} TestBufs m_bufs = new TestBufs(); void BufChanged(SyncListStruct<Buf>.Operation op, int itemIndex) { Debug.Log("buf changed:" + op); } void Start() { m_bufs.Callback = BufChanged; } } ``` ##### 自訂序列化函式 ##### 序列化過程 --- #### 遠端動作 此網路系統中有很多方式,經由網路中來執行動作。這些類型的動作有時候被稱為遠端方法呼叫( RPC )。在此系統中有兩種RPC,分別為,客戶端呼叫、在伺服器中執行的命令( Command ) 和伺服器端呼叫、在客戶端中執行的客戶端RPC( ClientRpc )。 下面的這張流程圖表示了遠端動作執行的方向: ![](https://docs.unity3d.com/uploads/Main/UNetDirections.jpg) ##### 命令 命令是從客戶端中的玩家物件中送到伺服器端的玩家物件。為了安全,命令只能從你的玩家物件中送出,所以你不可控制其他玩家的物件。要讓函式轉換成命令,就增加 [Command] 屬性,並且函式名稱增加"Cmd"前綴。當此函式在客戶端中被呼叫時,將只會在伺服器端中執行。任何的參數將會自動地被傳遞到伺服器。 命令函式必須要有"Cmd"前綴。當程式碼被解析時這是個提示,表示此函式是特別的且不會像一般的函式那樣在本地執行。 ``` class Player : NetworkBehaviour { public GameObject bulletPrefab; [Command] void CmdDoFire(float lifeTime) { GameObject bullet = (GameObject)Instantiate( bulletPrefab, transform.position + transform.right, Quaternion.identity); var bullet2D = bullet.GetComponent<Rigidbody2D>(); bullet2D.velocity = transform.right * bulletSpeed; Destroy(bullet, lifeTime); NetworkServer.Spawn(bullet); } void Update() { if (!isLocalPlayer) return; if (Input.GetKeyDown(KeyCode.Space)) { CmdDoFire(3.0f); } } } ``` 在客戶端中,以每個幀傳送命令時要特別注意!這會造成大量的網路流量。 預設情況下,命令會在第0通道,也就是預設通道中被送出。所以預設的命令會被可靠地送至伺服器。這可以藉由自訂 [Command] 屬性的Channel參數來改變。參數必須為整數,代表通道的號碼。 第一通道預設設定為非可靠的通道,所以要使用它的話,設定命令屬性的參數為1,就像這樣: ``` [Command(channel=1)] ``` 在 Unity 5.2 開始,在具有客戶端權力的非玩家物件上可送出命令。這些物件必須藉由 NetworkServer.SpawnWithClientAuthority 產生,或是使用 NetworkIdentity.AssignClientAuthority 來設定權力。 從這些物件送出的命令會在伺服器端上的物件實體中被執行,而非在和客戶端關聯的角色物件。 ##### 客戶端RPC呼叫 客戶端RPC呼叫,是從伺服器端中的物件中傳送到所有客戶端的物件。它可以從任何掛有 NetworkIdentity組件、且被產生出來的伺服器物件中被傳送。自從伺服器擁有權力後,之後這些可傳送這些呼叫的伺服器物件就沒有任何安全議題。要讓函式轉換成客戶端RPC呼叫,就增加 [ClientRPC] 屬性,並且函式名稱增加"Rpc"前綴。當此函式在伺服器端中被呼叫時,將只會在客戶端中執行。任何的參數將會自動地被傳遞到客戶端。 客戶端RPC呼叫函式必須要有"Rpc"前綴。當程式碼被解析時這是個提示,表示此函式是特別的且不會像一般的函式那樣在本地執行。 ``` class Player : NetworkBehaviour { [SyncVar] int health; [ClientRpc] void RpcDamage(int amount) { Debug.Log("Took damage:" + amount); } public void TakeDamage(int amount) { if (!isServer) return; health -= amount; RpcDamage(amount); } } ``` 當此遊戲在是莊家、且有一個本地客戶物件的狀況下執行時,客戶端RPC呼叫將會在本地客戶物件被觸發,即使是和伺服器端相同的執行過程。所以對客戶端RPC呼叫來說,本地客戶和遠端客戶的行為是一樣的。 ##### 遠端動作的參數 這些被命令和客戶端RPC所呼叫的參數會被序列化並經由網路傳送。這些參數可以是: * 基本型別 (byte, int, float, string, UInt64, 等等) * 基本型別的陣列 * 包含允許型別的結構 * Unity 內建的數學型別 (Vector3, Quaternion, 等等) * NetworkIdentity * NetworkInstanceId * NetworkHash128 * 掛有 NetworkIdentity 組件的遊戲物件 遠端動作的參數不可以是遊戲物件底下的組件,像是腳本實體和型變 ( Transform )。它們也不可以是經由網路、無法被序列化的其他型別。 --- ### 莊家轉移 在沒有專職伺服器的多人連線遊戲中,有一個連線的連線者(Peer)會同時扮演控制中心,此連線者稱為 莊家。它同時執行伺服器和本地客戶端,其他的連線者們分別執行遠端客戶端。 假如莊家斷線,而遊戲無法繼續。莊家有可能是因為自行跳出、執行體被終止or當掉、莊家的機器壞了或是莊家的網路連線不穩。 莊家轉移 功能允許其餘的其中一個連線者變成莊家,讓遊戲可以持續。 如何運作 當已啟用莊家轉移的多人連線遊戲時,連線者的地址會被分派到遊戲中的連線者。當失去莊家時,一連線者可以變成莊家。其他的連線者可以連到新的莊家繼續遊戲。 下圖是 NetworkMigrationManager Component 在編輯器中顯示的畫面。 ![](https://docs.unity3d.com/uploads/Main/NetworkMigrationManagerInspector.png) 這裡也提供簡單、類似 NetworkManagerHUD 的UI。此UI只適合拿來測試或製作原型,正式的遊戲建議使用自己的UI,甚至是自訂的邏輯(像是自動加入到新莊家)來控制莊家轉移機制。 ![](https://docs.unity3d.com/uploads/Main/NetworkMigrationManagerHUD.png) 即使轉移也許會因為舊莊家斷線或離開而發生,舊莊家再次以客戶端的身分連線到新莊家是有可能的。 在場景上所有連線物件的 SyncVars 和 SyncLists 都會在莊家轉移時保持住。此行為也會在有自訂的序列化資料的物件上套用。 所有遊戲玩家的物件會在失去莊家時停用。所以當其他客戶端重新加入新莊家開的遊戲,所有代表客戶端的玩家皆會在莊家身上重新啟用,並在每個客戶端生成。所以在轉移時不會有遺失玩家資料的問題。 注意:只有在客戶端上啟用的資料才會在轉移時保留。假如資料只有在伺服器上,它將不會在客戶端變成新莊家時被保留。所以任何沒有儲存在 SyncVars 或 SyncLists 的資料都不會保留。 回呼函式 OnStartServer 會在客戶端變成新莊家時被觸發。 在新莊家,NetworkMigrationManager 使用 BecomeNewHost 方法來從既有的客戶端場景建構伺服器場景。 在啟用莊家轉移機制的遊戲中,伺服器是藉由遊戲中的連線者身上的 連線ID 來分辨。當客戶端重新連線到新莊家的遊戲中,這個連線ID會被轉移到新莊家,所以可以辨別此玩家曾經連接到舊莊家。此ID會被設定在 客戶端場景,稱為 重新連線ID。 #### 非角色物件 具有客戶端權力的非角色物件也會被莊家轉移機制控制。每個客戶端擁有的角色物件都會被停用並重新啟用同樣的方法。 #### 判別連線者 在莊家失聯前,所有連線者都連上莊家了。它們都有一個獨特的 連線ID ,以下稱為 舊的連線ID。 當決定了新莊家,其他連線者重新連線上時,它們會提供舊的連線ID給新莊家。讓新的莊家來配對相應的玩家物件給重新連線的客戶端。 至從沒有連線到舊莊家的時候,它曾是舊莊家。會使用特別、值為零的舊連線ID來重新連線。在常數 ClientScene.ReconnectIdHost 可以看到。 當使用內建的UI時,舊的連線ID會自動設定。它可以使用 NetworkMigrationManager.Reset 或 ClientScene.SetReconnectId 來手動設定。 #### 運作流程 1. 機器A 當莊家,且莊家轉移功能打開。 2. 機器B 啟動客戶端並加入莊家轉移功能的遊戲。 * 機器B 被告知連線代號(機器A-0, 自己,即機器B-1)。 3. 機器C 啟動客戶端並加入莊家轉移功能的遊戲。 * 機器C 被告知連線代號(機器A-0, 機器B-1, 自己,即機器B-2) 4. 機器A 遺失,所以也代表莊家遺失。 5. 機器B 從莊家斷線。 1. 機器B的 MigrationManager 觸發回呼函式。 2. 機器B所有的玩家物件都停用。 3. 機器B保持在連線的場景。 6. 機器B 使用工具函式來變成新莊家,並把自己加進去。 1. 機器B 呼叫 BecomeNewHost() 2. 機器B 開始監聽連線。 3. 機器B 把自己的玩家物件重新啟用。 4. 機器B 的玩家回到遊戲並保持原有的狀態。 7. 機器C 從莊家斷線。 1. 機器C的 MigrationManager 觸發回呼函式。 2. 機器C所有的玩家物件都停用。 3. 機器C保持在連線的場景。 8. 機器C 使用工具函式來找到新莊家,並把自己加進去。 1. 機器C 重新連線到新莊家。 9. 機器B 從 機器C 收到連線。 1. 機器C 送重新連線的訊息,包含 oldConnectionId (而不是 AddPlayer 訊息)。 2. 機器B的 MigrationManager 觸發回呼函式。 3. 機器B 使用 oldConnectionId 來找到已停用的角色物件,並使用 ReconnectPlayerForConnection() 重新新增。 4. 把機器C的玩家物件重新啟用。 5. 機器C的玩家回到遊戲並保持原有的狀態。 10. 機器A復原。 1. 機器A 使用工具函式來找到新莊家,並把自己加進去。 2. 機器A 重新連線到機器B。 11. 機器B 從 機器A 收到連線。 12. 機器A 發送重新連接的訊息,其 oldConnectionId 值為零。 1. 機器B的 MigrationManager 觸發回呼函式。 2. 機器B 使用 oldConnectionId 來找到已停用的角色物件,並使用 ReconnectPlayerForConnection() 重新新增。 3. 把機器A的玩家物件重新啟用。 4. 機器A的玩家回到遊戲並保持原有的狀態。 #### 回呼方法 NetworkHostMigrationManager 所包含的回呼方法: ``` // 從連線到莊家的狀態斷線時,在客戶端觸發。控制要切換成連線或是離線場景。 protected virtual void OnClientDisconnectedFromHost( NetworkConnection conn, out SceneChangeOption sceneChange) // 當莊家遺失之後,在莊家觸發。莊家必須切換場景。 protected virtual void OnServerHostShutdown() // 當從舊莊家來的客戶端重新連線玩家物件時,在新莊家(伺服器)中觸發。 protected virtual void OnServerReconnectPlayer( NetworkConnection newConnection, GameObject oldPlayer, int oldConnectionId, short playerControllerId) // 當從舊莊家來的客戶端重新連線玩家物件時,在新莊家(伺服器)中觸發。 protected virtual void OnServerReconnectPlayer( NetworkConnection newConnection, GameObject oldPlayer, int oldConnectionId, short playerControllerId, NetworkReader extraMessageReader) // 當從舊莊家來的客戶端重新連線非玩家物件時,在新莊家(伺服器)中觸發。 protected virtual void OnServerReconnectObject( NetworkConnection newConnection, GameObject oldObject, int oldConnectionId) // 當連線者更新時,在莊家和客戶端都會觸發。 protected virtual void OnPeersUpdated( PeerListMessage peers) // 工具方法,會在失去莊家之間的連線後,要選擇新的莊家時觸發,被預設的UI呼叫 public virtual bool FindNewHost( out NetworkSystem.PeerInfoMessage newHostInfo, out bool youAreNewHost) // 當非玩家物件的權力改變時 protected virtual void OnAuthorityUpdated( GameObject go, int connectionId, bool authorityState) ``` #### 限制 只有在伺服器上使用的資料,會在莊家斷線時遺失。要遊戲正常的轉移莊家,載入的資料必須被分派到客戶端,而不是藏在伺服器端內。 此功能只能在直接連線的遊戲上使用。額外的功能需要使用 Unity 官方的配對機制和伺服器。