頂置文

========================= [頂置文開始] =========================
獨立遊戲「蠟筆大冒險」專頁

JJKid 常出沒地點:




========================= [頂置文結束] =========================

2025年7月3日 星期四

Unity Netcode for GameObjects 開發心得筆記

 實作成品,遊戲下載

最近研究了 Unity 提供的多人連線套件 Netcode for GameObjects,簡稱 NGO。用來做一些比較輕量的多人連線應用。我用這個套件做了個五子棋對戰遊戲,可以和遠端的朋友一起玩(你應該有朋友吧?)。只有連線對戰功能,不提供與 AI 對戰。另外遊戲裡沒有擋禁手,就當作休閒遊戲玩玩吧。


這次採用的架構是某一台電腦自己當主機開房間,讓其它玩家連線進來,也就是 Host 的模式。


快樂五子棋遊戲下載連結


使用套件介紹

使用的套件為 Netcode for GameObjects,簡稱 NGO。是 Unity 官方針對小型至中型多人遊戲所設計的網路解決方案,它不是為大規模 MMO 或 FPS 對戰遊戲設計的。適合的遊戲類型:下棋、麻將、回合制對戰、派對遊戲、休閒對戰遊戲。不適合的遊戲類型:大型 FPS、MMORPG、大規模即時戰略遊戲。理想的連線人數大概是 2 至 10 人左右,如果需求是更多人的連線遊戲,就要考慮其它的解法了,例如 Netcode for Entites、Mirror、Photon。


安裝 NGO 很簡單,啟動 Package Manager,搜尋 Netcode 就能找到套件,直接安裝就好了。



IP 連線方式

無論用哪種連線方式,都需要在專案裡建一個 NetworkManager 物件,掛上二個腳本:Network Manager 與 Unity Transport,並把 Unity Transport 的 Protocol Type 擇擇 Unity Transport。


使用 IP 的方式開房間。


使用 IP 的方式加入房間。



Relay 連線方式

前面提到使用 IP 連線的方式,這種方式適合所有的主機都在同一個網域內,例如大家都在同一個區域網路。但現實中的應用情境每台電腦通常都透過 NAT 連上網路,以 IP 的方式加入房間是找不到主機的 (除非在路由器裡設定了 Port Forwarding,這種進階操作一般人不會)。為了解決這個問題,Unity 提供了 Relay 的服務。簡單地說可以想像成網路上有一台 Relay 的主機,Host 與 Client 都連到這台 Relay 主機,在那裡做資料交換。


使用 Relay 建立房間,Host 端會得到一組 Join Code。要連線的 Client 端只要輸入這組 Join Code 就能連線到房間。當然這樣還是得透過別方式讓對方知道 Join Code 是什麼,還是不夠方便。能不能直接有個列表,列出現在線上有哪些房間,我選擇我想加入的房間就可以加入了?答案是可以。如果會寫後端程式的話這個功能可以自己做,不然 Unity 也提供了這個服務,就是 Lobby。不過我這次沒用到 Lobby 所以就不細說了。


要使用 Relay 功能,首先得設定專案連結。在 Edit --> Project Settings --> Services 這樣把專案連結好。


要呼叫使用 Relay 的功能前,得先初始化一些設定,執行一次即可。


使用 Relay 的方式開房間。


使用 Relay 的方式加入房間。



NetworkVariable

NetworkVariable 是 NGO 提供的一個功能,以它宣告出來的變數,資料會自動同步;並且在資料有異動的時候發出通知,因此可以做為一種資料驅動的方式做出回應,例如偵聽某個變數的值改變了,因而去修改場景物件的顏色。NetworkVariable 看起來很方便,但有一些限制。

  • NetworkVariable 不可以宣告成 static。
  • NetworkVariable 支援的資料型別有限制,只能是 int、float、enum、Vector3 這一類的基本型別,如果要用自訂型別,則該型別必須實作 INetworkSerializable。並且留意不能用 string。


有一個小地方要注意,在宣告 NetworkVariable 的同時就要把它 new 出來。不要想著先宣告,然後在 OnNetworkSpawn( ) 的時候再 new 出實體,這樣會破壞它的同步功能,這個有時會不小心踩坑。



RPC

全名是 Remote Procedure Call,簡稱 RPC。讓你從一台裝置去呼叫另一台裝置的某個 function。例如由 Client 去呼叫 Host 端執行某個 function;或是 Host 呼叫某個 (或全部) Client 去執行某個 function。可以把 RPC 視為一個「事件」發生。尤其是 Client 常常需要透過 RPC 去通知 Server 做一些事,例如修改 NetworkVariable 的值,因為只有 Server 端有修改權限。RPC 同樣有一些限制。

  • RPC 只能在 NetworkBehaviour 中宣告。NetworkBehaviour 就像是 MonoBehaviour 一樣,是我們寫自訂腳本的地方,只是它又多了網路連線功能。
  • RPC 沒有回傳值,而且它傳遞的資料型別有限制,一樣只能是 int、float、enum、Vector3 這一類的基本型別,或是實作了 INetworkSerializable 的自訂型別。



NetworkTransform

NetworkTransform 是一個用來自動同步物件的 Transform(位置、旋轉、縮放)的元件。例如玩家角色的移動、場景上一些物件的移動。為了節省頻寬,只同步有需要的屬性,不需要同步的屬性不要打勾。


如果有個物件需要輪流給不同的 Client 擁有 Transform 的控制權,則可以把 Network Transform 的 Authority Mode 改為 Owner。這樣只要把 Owner 指定給特定的 Client,它就能直接控制該物件的 Transform,然後自動同步給所有其它 Client。


類似 NetworkTransform 的類別還有 NetworkAnimator、NetworkRigidbody。



一些有點雜的筆記

  1. 加了 Rigidbody 的物件,如果要同步物理狀態,要加上 Network Rigidbody。它會自動把 Client 端的 Rigibody 設定為 Kinematic,並且主動加上 Network Transform 來同步物件的座標和旋轉。因此所有 Client 看到的物理效果都會一模一樣。如果沒加 Network Rigidbody,則 Client 端會用自己的環境計算碰撞,物件最後的位置與旋轉會與 Server 不同。
  2. 取得自己的 Player 物件:NetworkObject playerObj = NetworkManager.Singleton.LocalClient.PlayerObject;。
  3. 以 Client Id 取得 Player 物件:NetworkManager.Singleton.ConnectedClients[clientId].PlayerObject;。
  4. 改變 Network Object 的 Parent 關係會觸發 NetworkBehaviour.OnNetworkObjectParentChanged。
  5. NetworkLog.LogInfoServer("Hello World!"); 可以在 Server 端寫 log。
  6. NetworkBehaviour.OnNetworkPostSpawn( ) 會在所有的 NetworkBehaviour.OnNetworkSpawn( ) 之後被呼叫。
  7. NetworkManager 幫每個 Client 建立的 Prefab 物件不用自己放到 DontDestroyOnLoad,NGO 會自動處理這件事。
  8. NetworkObject 不能再包含有 NetworkObject 的子物件。說的更詳細點,有 NetworkObject 的父子物件,必須各別生成,手動設定為父子。就是不能一起生成。所以不能做成 Prefab,因為 Prefab 就是一起生成。只有動態 Spawn 出來的 Network Object 可以被放到另一個 Network Object 下面,而且這個作為父物件的 Network Object 也要是被動態 Spawn 出來的。感覺有點複雜,希望我沒有理解錯誤。
  9. NetworkBehaviour 必須加在同時有 NetworkObject 的物件上,或者父層有 NetworkObject 的物件上。
  10. 一旦斷線,除了自動生成的 Player 物件會自動消滅,所有動態生成的物件也會自動消滅。
  11. 使用 NetworkHide( ) 來隱藏物件時,對應的 Client 只要是變成看不見該物件,則 GameObject 會直接被消滅。但對 Host 端無效,因為 Host 端握有全部的資料,如果要能對 Host 端「隱形」的話,這個部份的邏輯要自行處理。
  12. 因為 Despawn(false) 對 Client 來說沒差,物件一樣被消滅,對 Host 端也沒差,物件一樣停在原地,沒有任何變化。因此,若要實現 Host / Client 都能用的 Pool,得自己處理物件的回收 (例如移到場外,停止移動)。
  13. 關於 AnticipatedNetworkVariable,Client 修改了預測值不會同步到 Server,所以 Server 也沒有機會去「修正」。要能同步到 Server,必須透過 Rpc 告知,再由 Server 端去更新這個值,再同步回 Client,但這樣做,Client 還是得等 Server 同步回來的值做更新,就沒有「預測」的效果了。所以 AnticipatedNetworkVariable 的用法就只是在 Client 端先寫入預測值,然後就直接套用這個預測值做後續的運算,但同步也要發 Rpc 讓 Server 端有機會「修正」預測值。結論就是 AnticipatedNetworkVariable 的設計和想像中的運作方式不一樣,有點多此一舉,官方也把它的範例移除了,感覺沒打算教怎麼使用,是不是官方也覺得這個東西根本沒啥用。
  14. 如果使用 Load Scene 來切換場景,場上的物件都會被消滅。因此主要的邏輯最好放在 Don't Destroy OnLoad 裡面,確保它一直活著。這是一個程式架構的建議。
  15. 實測 NetworkVariable 的同步到 Client 端比想像中還要久,不要預期下一個 Frame 就會同步完成。



工具套件

Multiplayer Play Mode。單機模擬多個 Instance,在開發時期很方便,不用每次測試都要先 Build 執行檔。


Multiplayer Tools。提供 Network Simulator,可以模擬各種網路情境,讓你預先看到在爛網路的狀態遊戲運作會變成什麼樣子。


Multiplayer Services。前面提過的 Relay 整合在這個套件裡,要用 Relay 就要裝。


4) Clumsy。在系統模擬不良的網路環境。僅列出來,我沒用它。



相關資料

NGO 官方文件,必讀。

Relay 的文件。

Multiplayer Tools 文件。



沒有留言:

張貼留言