實現一個簡易的Unity網絡同步引擎Netgo
目前GOLANG有大行其道的趨勢,尤其是在網絡編程方面。因為和c/c++比較起來,雖然GC占用了一部分機器性能,但是出錯概率小了,開發效率大大提升,而且應用其原生支持的協程很容易就能開發出高並發的服務端程序。筆者接觸VR行業兩年有余,接觸了一些商業unity網絡引擎,總覺的用的東西都落伍了,於是自己寫了一個簡單的引擎。目前實現了的基本功能:
- 支持房間概念。
- 支持靈活的數據同步方式,包括幀同步和RPC。
- 支持自定義事件的發送。
也實現了一個簡單的demo,同步效果見下圖,后面會有更詳細的介紹。

項目地址:https://github.com/harlanc/netgo-unity-client
下面是一個簡單的項目復盤。
數據通信格式
數據通信格式的定義是整個項目的基石。我們這里的客戶端和服務端是跨平台,跨語言通信。因此要定義一種語言無關,平台無關並且簡單易用,高效不費流量的數據格式。這里我們選用了Google的 Protobuf,詳細介紹參考這篇帖子。
Protobuf的C#代碼庫有兩種選擇,一種是protobuf-net,一種是protobuf-csharp-port,前者的接口書寫更加符合C# 語法規范,會讓人看起來更舒服一些。如果需要跨平台的話,推薦使用后者,因為不同語言的接口書寫比較類似,開發起來會更容易一些。看看原作者的回復。
定義proto文件
如何使用protobuf呢,首先要書寫proto文件,定義自己的結構化數據,在netgo中,下面是netgo中定義的消息體的一部分:
enum CacheOptions{
AddToRoomCache = 0;
RemoveFromRoomCache = 1;
}
message NGVector3{
float x = 1;
float y = 2;
float z = 3;
}
message NGQuaternion{
float x = 1;
float y = 2;
float z = 3;
float w = 4;
}
message NGColor{
float r = 1;
float g = 2;
float b = 3;
float a = 4;
}
完整定義參考。
生成c#和golang API接口文件
更新好命名空間后,執行下面的命令生成API文件:
-
golang
protoc --go_out=. *.proto
-
c#
protoc --csharp_out=. *.proto
服務端網絡模型
一個Unity網絡同步引擎的實現包括服務端和客戶端兩部分。Nego 是Unity網絡同步引擎的服務端,使用golang實現,充分利用了它的原生協程來實現高並發。其網絡模型基於gotcp來實現。

參考上圖,netgo會為每個socket鏈接建立一個協程,一個socket協程內部建立三個協程:
- ReadLoop 用於從網絡端讀取數據並放入Channel中。
- HandleLoop 用於解析應用層數據並完成相應處理,並將處理后的數據通過Channel發送給WriteLoop。
- WriteLoop 負責將處理結果forward給其它客戶端或者response給本客戶端。
參考代碼:
func (c *Conn) Do() {
if !c.srv.callback.OnConnect(c) {
return
}
asyncDo(c.handleLoop, c.srv.waitGroup)
asyncDo(c.readLoop, c.srv.waitGroup)
asyncDo(c.writeLoop, c.srv.waitGroup)
}
客戶端代碼結構
寫API基本上是面向用戶編程,筆者以為,清晰的代碼結構,好的命名方式能省掉大部分注釋,代碼寫的亂只能靠注釋來拯救,代碼結構看下圖:

按照命名空間,分為 Library,網絡層和應用層(以后用戶接口層會分出來).
相關概念
數據同步
這里的同步是指一個房間內的數據同步,一個房間內存在着來自網絡上的多個終端用戶,每個Client都會將房間內其它人的數據在本地做一個Clone,而數據同步是指將你自己的數據同步到其他Cient你自己的Clone上面,因此發送范圍是其它用戶都會接收。
數據同步分為一下兩種:
- View Sync
View Sync是毫秒級別的數據同步。可用於虛擬角色動作同步。
- RPC
每次同步由用戶手動觸發。可用於換裝等同步。
Custom Event
Custom Event不是向所有其它Client的Clone實體發送同步消息,而是向一個或者幾個指定的Client發送消息。
接口介紹
房間相關接口
請求接口
//加入或者創建房間
public static void JoinOrCreateRoom(string roomid,uint maxnumber)
//創建房間
public static void CreateRoom(string roomid, uint maxnumber)
//加入房間
public static void JoinRoom(string roomid)
//離開房間
public static void LeaveRoom()
回調接口
//創建房間成功
void OnGreatedRoom();
//創建房間失敗
void OnGreateRoomFailed(string errmsg);
//加入房間成功
void OnJoinedRoom();
//加入房間失敗
void OnJoinRoomFailed();
//離開房間成功
void OnLeftRoom();
Player相關接口
//實例化一個物體
public static void Instantiate(string prefabname, Vector3 position, Quaternion rotation, uint[] viewids)
//有其它用戶進入房間
void OnOtherPlayerEnteredRoom(NGPlayer player);
//有其它用戶離開房間
void OnOtherPlayerLeftRoom(NGPlayer player);
CustomEvent接口
請求接口
//發送事件
public static void SendCustomEvent(uint eventid, uint[] targetpeerids, NGAny[] customdata)
回調接口
//接收事件
void OnCustomEvent(uint eventID, NGAny[] data);
View Sync
視圖同步需要自己實現組件腳本,實現序列化反序列化接口,並且需要掛載到物體上:
public interface INGSerialize
{
void SerializeViewComponent(NGViewStream stream);
void DeserializeViewComponent(NGViewStream stream);
}
public class CubeViewComponent : NGIncomingEvent, INGSerialize
{
public void SerializeViewComponent(NGViewStream stream)
{
stream.Send(this.transform.position);
stream.Send(this.transform.rotation);
}
public void DeserializeViewComponent(NGViewStream stream)
{
mCorrentPosition = (NGVector3)stream.Receive();
mCorrentRotation = (NGQuaternion)stream.Receive();
}
}
Clone實體接受數據反序列化后在Update中實時更新即可:
void Update()
{
if (!view.IsMine)
{
transform.position = mCorrentPosition;//Vector3.Lerp(transform.position, mCorrentPosition, Time.deltaTime * 5);
transform.rotation = mCorrentRotation;//Quaternion.Lerp(transform.rotation, mCorrentRotation, Time.deltaTime * 5);
}
}
RPC
使用RPC需要在視圖腳本中寫一個RPC函數:
[NGRPCMethod]
public void OnColor(NGAny[] c)
{
mMat.color = c[0].NgColor;
}
調用下面的接口向其它Clone實體發送RPC調用:
public static void SendRPC(uint viewID, string methodname, RPCTarget target, params NGAny[] parameters)
有關RPC,View Sync和Custom Event 的詳細使用方法 參考源碼
Demo演示
服務端部署
Clone代碼
git clone https://github.com/harlanc/netgo.git
安裝依賴
go get -d ./...
更新監聽端口號
打開main.go
tcpAddr, err := net.ResolveTCPAddr("tcp", "0.0.0.0:8686")
啟動服務
go run main.go
客戶端編譯安裝
客戶端支持windows/MacOS/Andorid/IOS多平台。下面在Android和MacOS上測試:
配置IP和端口

切換Android平台

編譯生成APK
安裝APK后的初始化界面如下:

功能測試
兩個Client進入同一個房間,每個Client會實例化出來兩個Cube,一個為本機實體(Mine Cube),一個為對方的實體(Clone Cube)。
View SYnc
點擊按鈕Move后,會通過視圖同步的方式進行postion和rotation同步。也就是文章剛開始的動圖展示的樣子:

RPC
點擊Mine Cube之后,Cube的顏色會發生變化,同時同步到別的機器上,這里的顏色同步是通過RPC來實現的。

Custom Event
點擊Clone Cube之后,會向對方實體發送消息,效果是對方的Mine Cube Scale會增加。
Road Map
接下來考慮會加入或者需要優化的功能:
- 支持大廳功能
- 支持負載均衡
- 增加支持UDP等網絡傳輸協議
- 增加支持json等多種數據編碼格式
- View Sync數據傳輸優化
- 支持跨房間Custom Event
- .....
