🎈 Protobuf
🔸 简介
Google Protocol Buffer(简称Protobuf
)是Google公司一种轻便高效的结构化数据存储格式,可作为数据的序列化
工具,经常被用于通讯协议
,与Json
、XML
相比,Protobuf的优点在于性能更高
,它更小、更快,以高效的二进制
方式存储,生成的二进制消息非常紧凑,因此在网络上传输的字节数更少。
🔸 使用
我们使用Protobuf作为通信协议,创建一个协议类需要经过以下步骤:
- 根据语法规则编写
.proto
文件; - 通过编译工具
protoc.exe
将.proto文件编译成.cs文件;
🎯 编写.proto文件
语法规则如下:
- 使用
message
定义类,相当于c#中的class
; 使用三种字段修饰符修饰字段:
- required 表示是一个
必选
字段,必须初始化; - optional 表示是一个
可选
字段,可以不进行初始化; - repeated 表示该字段可以包含多个元素,可以看作是在传递一个
数组
的值;
- required 表示是一个
- 字段类型,与
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文件
- 运行
cmd
,cd
打开protoc.exe
所在路径
- 输入编译命令
protoc -I=./ --csharp_out=./ AvatarProperty.proto
编译成功后,可以看到AvatarProperty.cs
文件已经生成到目录下,将其导入到Unity中即可。
注
:AvatarProperty.cs
由protobuf的编译工具生成,导入到Unity后便不可修改
。
🎈 Socket
我们通过Socket TCP
实现网络通讯,使用了我的小型开发框架SKFramework
中的网络通讯模块:
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);
}
}
}
Face2Camera
为SKFramework
中的一个组件工具,其功能是使物体始终朝向相机
。
同步效果: