天天看點

實作一個簡易的Unity網絡同步引擎Netgo實作一個簡易的Unity網絡同步引擎Netgo

實作一個簡易的Unity網絡同步引擎Netgo

目前GOLANG有大行其道的趨勢,尤其是在網絡程式設計方面。因為和c/c++比較起來,雖然GC占用了一部分機器性能,但是出錯機率小了,開發效率大大提升,而且應用其原生支援的協程很容易就能開發出高并發的服務端程式。筆者接觸VR行業兩年有餘,接觸了一些商業unity網絡引擎,總覺的用的東西都落伍了,于是自己寫了一個簡單的引擎。目前實作了的基本功能:

  • 支援房間概念。
  • 支援靈活的資料同步方式,包括幀同步和RPC。
  • 支援自定義事件的發送。

也實作了一個簡單的demo,同步效果見下圖,後面會有更詳細的介紹。

實作一個簡易的Unity網絡同步引擎Netgo實作一個簡易的Unity網絡同步引擎Netgo

項目位址:

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

來實作。

實作一個簡易的Unity網絡同步引擎Netgo實作一個簡易的Unity網絡同步引擎Netgo

參考上圖,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基本上是面向使用者程式設計,筆者以為,清晰的代碼結構,好的命名方式能省掉大部分注釋,代碼寫的亂隻能靠注釋來拯救,代碼結構看下圖:

實作一個簡易的Unity網絡同步引擎Netgo實作一個簡易的Unity網絡同步引擎Netgo

按照命名空間,分為 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);
           

視圖同步需要自己實作元件腳本,實作序列化反序列化接口,并且需要挂載到物體上:

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函數:

[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和端口
實作一個簡易的Unity網絡同步引擎Netgo實作一個簡易的Unity網絡同步引擎Netgo
切換Android平台
實作一個簡易的Unity網絡同步引擎Netgo實作一個簡易的Unity網絡同步引擎Netgo
編譯生成APK

安裝APK後的初始化界面如下:

實作一個簡易的Unity網絡同步引擎Netgo實作一個簡易的Unity網絡同步引擎Netgo

功能測試

兩個Client進入同一個房間,每個Client會執行個體化出來兩個Cube,一個為本機實體(Mine Cube),一個為對方的實體(Clone Cube)。

View SYnc

點選按鈕Move後,會通過視圖同步的方式進行postion和rotation同步。也就是文章剛開始的動圖展示的樣子:

實作一個簡易的Unity網絡同步引擎Netgo實作一個簡易的Unity網絡同步引擎Netgo

點選Mine Cube之後,Cube的顔色會發生變化,同時同步到别的機器上,這裡的顔色同步是通過RPC來實作的。

實作一個簡易的Unity網絡同步引擎Netgo實作一個簡易的Unity網絡同步引擎Netgo

點選Clone Cube之後,會向對方實體發送消息,效果是對方的Mine Cube Scale會增加。

Road Map

接下來考慮會加入或者需要優化的功能:

  • 支援大廳功能
  • 支援負載均衡
  • 增加支援UDP等網絡傳輸協定
  • 增加支援json等多種資料編碼格式
  • View Sync資料傳輸優化
  • 支援跨房間Custom Event
  • .....