前段時間,研究了一下UNet,經過項目實踐,大致整理了下遇到的問題。
源碼Bitbucket:需要說明的是,這個項目只包含上層的包裝,一些低層的網絡實現在Unity內部,如NetworkTransport類等並不包含。
UNet常見概念簡介
Spawn:簡單來說,把服務器上的GameObject,根據上面的NetworkIdentity組件找到對應監視連接,在監視連接里生成相應的GameObject.
Command:客戶端調用,服務器執行,這樣客戶端調用的參數必需要UNet可以序列化,這樣服務器在執行時才能把參數反序列化。需要注意,在客戶端需要有權限的NetworkIdentity組件才能調用Command命令。
ClientRpc:服務端調用,客戶端執行,同上,服務端的參數序列化到客戶端執行,一般來說,服務端會找到上面的NetworkIdentity組件,確定那些客戶端在監視這個NetworkIdentity,Rpc命令會發送給所有的監視客戶端。
Server/ServerCallback:只在服務器端運行,Callback是Unity內部函數。
Client/ClientCallback:同上,只在客戶端運行,Callback是Unity內部函數。
SyncVar:服務器的值能自動同步到客戶端,保持客戶端的值與服務器一樣。客戶端值改變並不會影響服務器的值。
上面的大部分特性都會轉化成相應的MsgType,其中服務器調用,客戶端執行對應MsgType有如Spawn,ClientRpc,SyncVar對應的MsgType分別為ObjectSpawn,Rpc,UpdateVars,這些都是NetworkServer調用,客戶端得到相應消息,執行相應方法。客戶端調用,服務器執行的MsgType有如Command,客戶端發送,服務器檢測到相應消息后執行。
UNet主要類介紹
NetworkIdentity組件介紹:網絡物體最基本的組件,客戶端與服務器確認是否是一個物體(netID),也用來表示各個狀態,如是否是服務器,是否是客戶端,是否有權限,是否是本地玩家等。
一個簡單例子,A是Host(又是服務器,又是客戶端),B是一個Client,A與B分別有一個玩家PlayA與PlayB.在機器A上,playA與playB isServer為true,isClent為true,其中playA有權限,是本地玩家,B沒權限,也不是本地玩家。在機器B上,playA與playB isServer為false,isClent為true,其中playB有權限,是本地玩家,A沒權限,也不是本地玩家。A與B上的PlayA的netID相同,A與B上的PlayB的netID也相同,其中netID用來表示他們是同一網絡物體在不同的機器上。
在下面用網絡物體來表示帶有NetworkIdentity組件的GameObject.
NetworkConnection:定義一個客戶端與服務器的連接,包含當前客戶端監視那些服務器上的網絡物體,以及封裝發送和接收到服務器的消息。
NetworkClient:主要持有當前NetworkConnection對象與所有NetworkClient列表的靜態對象,處理一些默認客戶端的消息。
網絡物體上的監視者就是一個或多個NetworkConnection,用來表示一個或多個客戶端對這個網絡物體保持監視,那么當這個網絡物體在服務器上更新后,會自動更新對所有監視者的對應的網絡物體。
NetworkScene:簡單來說,1Server與Client需要維護一個網絡物體列表,Server可以遍歷所有網絡物體發送消息等,並且維持Server與Client上的網絡物體保持同步,並且客戶端記錄需要注冊的prefab列表.其中NetworkServer與ClientScene都包含一個NetworkScene對象,引用網絡物體列表。
NetworkServer:主要持有一個NetworkScene並且做一些只有在服務器上才能對網絡服務做的事,如spawn, destory等。以及維護所有客戶端連接。
ClientScene:主要持有一個靜態NetworkScene對象,用於注冊網絡物體的prefab列表,以及客戶端場景上已經有的網絡物體列表,處理SyncVar,Rpc,SyncEvent特性等,還有以及ObjectSpawn,objectDestroy,objectHide消息等。
UNet用時想到的問題與源碼的答案
問題1 spawn發生了什么,客戶端為什么要注冊相應的prefab.
1 當服務器spawn一個網絡物體時,網絡物體調用OnStartServer,分配netID.並注冊到相應服務器上的的NetworkScene的網絡物體列表中,更新如isServer為true等信息。
2 查找所有客戶端連接,查看每個客戶端連接是否需要監視這個網絡物體,如果為true,那么給這個客戶端上一個消息MsgType.ObjectSpawn或是MsgType.ObjectSpawnScene(這種一般是服務場景變換后自動調用),並傳遞上面的netID.
3 當客戶端接受到ObjectSpawn消息,會在注冊的prefab里查找,查找到后Instantiate個網絡物體,當接受到ObjectSpawnScene時,會在場景里查找這個網絡物體,然后都注冊到ClientScene里的NetworkScene的網絡物體列表中,並更新netID與服務器的一樣。更新如isClent為true等信息。
我們手動spawn一個物體時,調用的是ObjectSpawn消息,客戶端接到這個消息處理得到一個assetID,我們要根據prefabe實例一個新對象,只有客戶端注冊了相應的prefabe信息才能根據對應的assetID找到prefabe.
問題2 NetworkIdentity的netID表示什么,那個時候分配。
當服務器與客戶端的netID相同,表示他們是同一物體,相應標示如SyncVar,服務器變了,對應客戶端上相同的netID的網絡物體,更新成服務器上的數據,Rpc,Commandg 一般也是相同的netID之間調用。
分配一般發生在服務器spawn一個網絡物體時,網絡物體調用OnStartServer時發生產生netID。
在客戶端接受相應的ObjectSpawn消息,會把服務器上的對應物體的netID傳遞過來,產生新的網絡物體並賦這個netID。
問題3 NetworkIdentity的sceneID是什么,在場景里已經有NetworkIdentity組件的物體是如何在客戶端與服務器聯系的。
當網絡物體並不是spawn產生在服務器與客戶端,而是在服務器與客戶端場景本身就有時,我們也需要在服務器與客戶端之間建立聯系,這種物體會有一個sceneID來標示,這種模型一般是服務器場景變換完成后,NetworkServer調用spawnObjects會把這種網絡物體與所有客戶端同步,當spawn完成后過后,相應客戶端會產生一個和服務端相同的netID。
問題4 服務器場景切換后,各個NetworkIdentity組件的物體如何與客戶端聯系。
如下順序因為有異步操作,並不能確定,如下順序只是一般可能的順序。
1。服務器異步調用場景,發送給所有客戶端開始切換場景。MsgType.Scene
2。客戶端接受MsgType.Scene,開始切換場景。
3。服務器場景完成,會查找所有的網絡物體,然后spawn這些網絡物體,這樣各個網絡物體通過相同的netID聯系起來。
4。客戶端場景完成后,再次調用OnClientConnect,一般來說,不執行任何操作。
問題5 客戶端為什么要網絡物體的權限,它有了權限能做什么。
一般來說,當spawn某個服務器上的網絡物體后,服務器有它的權限,客戶端並不能更改這個網絡物體,或是說更改這個網絡物體相應的屬性后並不能同步到服務器和別的客戶端上,只是本機上能看到改變。
那么我如果需要能改變這個網絡物體上的狀態,並能同步到所有別的客戶端上,我們需要擁有這個網絡物體的權限,因為這樣才能在本機上發送Command命令,才能告訴服務器我改變了狀態,服務器也才能告訴所有客戶端這個網絡物體改變了狀態。
其中本地player在創建時,當前客戶端對本地player有權限。客戶端上有權限的網絡物體上的SyncVar改變后,也並不會能同步到服務器,服務器根本沒有注冊UpdateVars消息,這種還是需要客戶端自己調用Command命令。
問題6 UNet常見的封裝狀態同步處理有那些,其中NetworkTransform與NetworkAnimator分別怎樣通信,如果是客戶端權限的網絡物體又是怎么通信的了。
UNet常見的封裝狀態同步狀態方法有二種。
一是通過ClientRpc與Command是封裝發送消息。客戶端與服務端一方調用,然后序列化相應的參數,然后到服務器與客戶端反序列化參數執行。
二是網絡內置的序列化與反序列化,序列化服務器的狀態,然后客戶端反序列化相應的值,如SyncVar通過相應的OnSerialize,OnDeserialize.這種只能同步服務器到客戶端。
這二種本質都是客戶端與服務器互相發送MsgType消息,對應的服務器與客戶端注冊相應消息處理。NetworkAnimator 服務器上的動畫改變,會發消息通知所有客戶端相應狀態改變了,如Rpc。NetworkTransform 服務器通過OnSerialize序列化相應的值,然后客戶端反序列化相應的值。
如果客戶端有對應NetworkTransform與NetworkAnimator網絡物體的權限。NetworkAnimator 相應客戶端提交狀態到服務器上,然后分發到所有客戶端,相當於調用了Command,並在Command里調用了Rpc方法。NetworkTransform 相應客戶端發送消息到服務器上,服務器更新相應位置,方向。然后通過反序列化到所有客戶端。
所以如果客戶端有授權,那么NetworkAnimator與NetworkTransform在服務器或是有授權的客戶端的狀態改變都能更新到所有客戶端,注意這二個組件對localPlayerAuthority的處理不同,在NetworkTransform中,localPlayerAuthority為false時,客戶端不能更新到所有客戶端,在NetworkAnimator中,localPlayerAuthority為true時,服務器不能更新到客戶端上。
其中注意SyncVar特性,就算客戶端授權,客戶端改變后,也不會同步到別的機器上。
所以如果我們自己設計類似的網絡組件,需要考慮客戶端授權的相應處理,就是差不多添加一個Command命令。
問題7 客戶端授權與本地player授權有什么區別。
一般物體的權限都在服務器上,如果要對網絡物體授權給客戶端,一般通過SpawnWithClientAuthority實現,這樣在相應客戶端上的hasAuthority為true,其中相應的playerControllerID為-1。
而本地player授權localPlayerAuthority,在相應的網絡物體上的Local Player Authority勾選上,在對這個網絡物體的所有監視客戶端上,本地player授權都是true,這種一般用於玩家,或是玩家控制位移的物體,playerControllerID大於等於0。
所以客戶端授權針對是某個客戶端,在這個客戶端上的這個網絡物體的hasAuthority為true,而本地player針對是某個網絡物體,在所有客戶端上的這個網絡物體的localPlayerAuthority都為true.
問題8 UNet怎么實現迷霧地圖
通過NetworkProximityChecker,這樣每楨檢測當前網絡物體的監視連接,確定那些客戶端需要這個網絡物體。同樣,想實現更復雜的可以自己實現類似。
問題9 NetworkServer.Destroy做了啥
必須是網絡物體,且最好能在服務器調用,調用時,發給所有的監視Connect,銷毀對應網絡物體,然后服務器銷毀。請看MsgType.ObjectDestroy消息流程.
需要注意的是在服務器中,Destroy某網絡物體,會自動調用NetworkServer.Destroy。代碼在NetworkIdentity.OnDestroy.
問題10 服務器添加角色時做了那些事。
當客戶端連接服務器時,設置自動創建角色后,會自動創建角色。
1 服務器添加一個player,設定playercontrollerID
2 設置當前conn的ready為true.然后檢測當前的conn是否需要監視服務器上NetworkScene的網絡物體列表的各個網絡物體,其中客戶端上的isspawnFinished表示NetworkScene的網絡物體列表是否檢測完成。
3 把服務器的player的spawn下去,設定對應網絡物體記錄的本地權限客戶端為當前客戶端,相應的playercontrollerid發送到客戶端。
問題11 NetworkClient與networkServer的active表示什么,那些時候用
networkServer開始監聽后,設定active為true。
networkClient連接上服務器后,設定為true。
當有些消息發送,或是Rpc與Command等的調用時,時機可能會在active之前,引發錯誤。
問題12 網絡的Update做些啥。
1 服務器更新,處理一些如客戶端鏈接與丟失鏈接,還有接收消息並找到對應事件處理,以及序列化服務器網絡物體要更新的數據。
2 客戶端更新,如上服務器的處理,主要也是相應消息處理。
3 檢查服務器與客戶端的場景是否加載完成。
最后,想象一下,在網絡環境下,我們拉開弓箭,生成箭,箭在客戶端上緩緩拉開,我們應該如何做?
首先弓箭要讓所有客戶端看的到,我們要在服務器上生成,然后spawn分發到相應多個客戶端,然后當前客戶端還需要當前箭的權限,這樣當前用戶才能控制這把箭,並把當前用戶控制箭產生的新位置同步給所有的客戶端。
其次如果采用Valve的LabRender渲染器,需要在開始服務器時關閉,等到對應的角色加載后,再通過localplayer打開各自對應的valveCamer,不然服務器上的valveCamer可能得不到正確的陰影圖。
如果有分析不對的地方,歡迎大家指出。