zoukankan      html  css  js  c++  java
  • 实现一个简易的Unity网络同步引擎——netgo

    实现一个简易的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
    • .....
  • 相关阅读:
    第四章
    第三章
    第二章
    实验5-2: 编制程序,输入m、n(m≥n≥0)后,计算下列表达式的值并输出。 要求将计算阶乘的运算编写作函数fact(n),函数返回值的类型为float
    作业
    多人电费
    单人电费
    圆柱体积
    圆面积
    第七章
  • 原文地址:https://www.cnblogs.com/harlanc/p/12103801.html
Copyright © 2011-2022 走看看