Unity Metaverse(三)、Protobuf & Socket 实现多人在线

举报
CoderZ1010 发表于 2022/07/29 09:13:58 2022/07/29
【摘要】 使用Scoket TCP和Protobuf通信协议实现多人在线。

🎈 Protobuf

🔸 简介

Google Protocol Buffer(简称Protobuf)是Google公司一种轻便高效的结构化数据存储格式,可作为数据的序列化工具,经常被用于通讯协议,与JsonXML相比,Protobuf的优点在于性能更高,它更小、更快,以高效的二进制方式存储,生成的二进制消息非常紧凑,因此在网络上传输的字节数更少。

🔸 使用

我们使用Protobuf作为通信协议,创建一个协议类需要经过以下步骤:

  • 根据语法规则编写.proto文件;
  • 通过编译工具protoc.exe将.proto文件编译成.cs文件;

🎯 编写.proto文件

语法规则如下:

  • 使用message定义类,相当于c#中的class
  • 使用三种字段修饰符修饰字段:
    • required 表示是一个必选字段,必须初始化;
    • optional 表示是一个可选字段,可以不进行初始化;
    • repeated 表示该字段可以包含多个元素,可以看作是在传递一个数组的值;
  • 字段类型,与C#的对应关系如下:
proto c# 备注
bool bool 布尔类型
string string 字符串类型
double double 64位浮点数
float float 32位浮点数
int32 int 32位整数
uint32 uint 无符号32位整数
int64 long 64位整数
uint64 ulong 无符号64位整数
sint32 int 编码时比通常的int32高效
sint64 long 编码时比通常的int64高效
fixed32 uint 无符号32位整数
fixed64 ulong 无符号64位整数
sfixed32 int 总是4个字节
sfixed64 long 总是8个字节
bytes ByteString 字节数据
  • 字段标识号

每个字段都有唯一的标识号,这些标识是用来在消息的二进制格式中识别各个字段的,使用后便不能更改。[1,15]之内的标识号在编码的时候会占用1字节。[16,2047]之内的标识号则占用2字节,所以应该为那些频繁出现的消息元素保留[1,15]之内的标识号。
:不可以使用[19000-19999]标识号,protobuf协议实现中对这些进行了预留。

例如,我们需要定义AvatarProperty协议类来对Avatar人物的属性进行通讯,proto文件编写如下

message AvatarProperty
{
	required string userId = 1;
	required int32 avatarId = 2;
	required string name = 3;
	required float posX = 4;
	required float posY = 5;
	required float posZ = 6;
	required float rotX = 7;
	required float rotY = 8;
	required float rotZ = 9;
	required float speed = 10;
}

🎯 编译.proto文件

  • 运行cmdcd 打开protoc.exe所在路径

cd
protoc.exe所在路径

  • 输入编译命令protoc -I=./ --csharp_out=./ AvatarProperty.proto

编译命令
编译成功后,可以看到AvatarProperty.cs文件已经生成到目录下,将其导入到Unity中即可。

AvatarProperty.cs

AvatarProperty.cs由protobuf的编译工具生成,导入到Unity后便不可修改

🎈 Socket

我们通过Socket TCP实现网络通讯,使用了我的小型开发框架SKFramework中的网络通讯模块:

SKFramework PackageManager

SKFramework框架开源地址:https://github.com/136512892/SKFramework

🔸 客户端发送Avatar数据

//每间隔一定时长发送一次Avatar数据
timer = Timer.EverySeconds(interval, () =>
{
    if (GameServer != null)
    {
        var ap = new AvatarProperty()
        {
            UserId = UserId,
            PosX = AvatarController.transform.position.x,
            PosY = AvatarController.transform.position.y,
            PosZ = AvatarController.transform.position.z,
            RotX = AvatarController.transform.eulerAngles.x,
            RotY = AvatarController.transform.eulerAngles.y,
            RotZ = AvatarController.transform.eulerAngles.z,
            Speed = AvatarController.Instance.Speed,
        };
        //发送数据
        GameServer.Send(ap);
    }
});
timer.Launch();

Timer模块为SKFramework框架中的计时类工具,也可以通过框架中的Packaga Manager下载,EverySeconds表示每隔多少秒执行一次回调函数,这里我们将internal设为0.025,也就是1秒将发送40次数据,可适当调整。

🔸 客户端接收Avatar数据

客户端接收到服务端的消息后,会将消息内容通过事件系统进行抛出:

//抛出消息
Messenger.Publish(msg.name, msg.content);

Messenger则是SKFramework框架中的事件系统,Publish表示发布消息,第一个参数表示消息的主题,第二个参数表示消息的内容

订阅主题为AvatarProperty的消息,当该主题的消息发布后,订阅事件OnAvatarPropertyMsgEvent将会被执行。

//订阅AvatarProperty消息
Messenger.Subscribe<ByteString>(typeof(AvatarProperty).Name, OnAvatarPropertyMsgEvent);

OnAvatarPropertyMsgEvent事件中,根据消息的用户ID判断相应的Avatar人物实例是否存在,如果不存在则进行创建并初始化:

private void OnAvatarPropertyMsgEvent(ByteString bs)
{
    //反序列化
    var ap = AvatarProperty.Parser.ParseFrom(bs);
    if (!avatarDic.ContainsKey(ap.UserId))
    {
        //第一次接收该Avatar数据 首先创建Avatar人物
        var instance = Object.Instantiate(Resources.Load<AvatarInstance>(typeof(AvatarInstance).Name));
        //存入Avatar字典
        avatarDic.Add(ap.UserId, instance);
        //初始化
        instance.Init(ap);
    }
    //目标Avatar
    avatarDic[ap.UserId].Set(ap);
}

AvatarInstance接收到数据后,使用插值方式计算坐标、旋转,以及同步Animator动画信息:

using UnityEngine;
using UnityEngine.UI;

using SK.Framework;

namespace Metaverse
{
    /// <summary>
    /// Avatar实例
    /// </summary>
    public class AvatarInstance : MonoBehaviour
    {
        public string UserId { get; private set; }

        [SerializeField] private Animator animator;
        [SerializeField] private Canvas worldCanvas;
        private Vector3 targetPos;
        private Vector3 targetRot;
        private const float lerpSpeed = 5f;

        /// <summary>
        /// 初始化
        /// </summary>
        /// <param name="ap"></param>
        public void Init(AvatarProperty ap)
        {
            UserId = ap.UserId;
            Camera mainCamera = Camera.main != null ? Camera.main : FindObjectOfType<Camera>();
            worldCanvas.worldCamera = mainCamera;
            //挂载Face2Camera组件 使其始终朝向相机
            worldCanvas.GetComponent<Face2Camera>().Set(mainCamera, false, false);
            worldCanvas.GetComponentInChildren<Text>().text = UserId;
        }

        /// <summary>
        /// 接收数据
        /// </summary>
        /// <param name="ap"></param>
        public void Set(AvatarProperty ap)
        {
            targetPos = new Vector3(ap.PosX, ap.PosY, ap.PosZ);
            targetRot = new Vector3(ap.RotX, ap.RotY, ap.RotZ);
            animator.SetFloat("Speed", ap.Speed);
        }

        private void Update()
        {
            //插值运算
            transform.position = Vector3.Lerp(transform.position, targetPos, Time.deltaTime * lerpSpeed);
            transform.rotation = Quaternion.Lerp(transform.rotation, Quaternion.Euler(targetRot), lerpSpeed);
        }
    }
}

Face2CameraSKFramework中的一个组件工具,其功能是使物体始终朝向相机

PackageManager - Face2Camera
Face2Camera
同步效果:

Avatar同步

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。