一句话省流:很麻烦也很抽象,能用内置支持的类型就尽量用。
首先看文档。官方文档里一开头就列出了所有内置的支持的类型:Ghost Type Templates
其中Entity类型需要特别注意一下:在同步这个类型的时候,如果是刚刚Instantiate的Ghost(也就是GhostId尚未生效,上一篇文章里说过这个问题),那么客户端收到的Entity值会是Entity.Null。之后就算GhostId同步过来了也不会再刷新。可以说有用,但不那么好用。
另外实测除了float2/float3/float4以外,double2/double3/double4也是支持的。
对于其他类型想要让[GhostField]
支持它的话,就需要自己写序列化逻辑了。为了性能和功能,Netcode for Entities的自定义序列化方式搞的特别的复杂,这里需要仔细阅读文档和NetcodeSamples里Translation2d/Rotation2d自定义序列化的做法。这俩分别是对2D对象的位置/旋转的序列化,前者是两个int值的坐标,后者是一个int值的旋转。
什么?官方的Sample项目Unity里打不开?看这里。
当然直接看肯定会一头雾水。毕竟Netcode用的方法不那么常规(或者说,有点复古)。这里以int3为例写一份引导:
首先要确定我们拿这个int3来干什么。我想让它功能尽可能丰富,除了Quantization以外(这东西对int类型也没啥意义),float3支持啥它就支持啥,比方说支持GhostFieldAttribute.Smoothing
、Prediction等等。然后我准备拿它当位置坐标来用。
自定义序列化的原理是:提供一个代码模板文件,然后Netcode for Entities就会拿着这个模板通过C#的Source Generator生成它想要的代码,最后再编译。所以我们需要先编写这个模板文件。
同时因为代码设计上的原因,你自定义的这个模板文件是通过写一个partial class添加到Netcode的处理队列里面的。从全局来看,就像是你把一堆代码“插入”到了Netcode原来的代码里一样。
首先随便找个地方建立一个文件夹,就直接叫Unity.NetCode好了。然后在里面创建一个Assembly Definition Reference,起名Unity.NetCode.Ref。接着在其Assembly Definition属性里选择Unity.NetCode。
然后在Unity.NetCode文件夹里建立一个新文件夹,叫Templates。再到Templates文件夹里建立一个新文件,叫“IntPosition.NetCodeSourceGenerator.additionalfile”。注意扩展名不要写错了。这个文件在Unity右键菜单里找不到的,去文件目录里面自己新建吧。
最后回到Unity.NetCode文件夹,建立一个空C#脚本文件:UserDefinedTemplates.cs
(看过我前面文章的会发现这个流程和解决代码注释不在IDE里显示的流程非常类似,其实这里说的才是这个功能本来的用法)
回到IDE内,以Visual Studio为例,会发现能在Solution Explorer里看到Unity.NetCode项目,其中包含Netcode的(部分)源代码,同时这个项目里还有刚才创建的UserDefinedTemplates.cs文件。
另外每个项目底下都多了前面创建的additionalfile文件。很乱,但没办法╮( ̄▽ ̄")╭
从头开始编写一个Template很麻烦,有很多“脚手架代码”需要搭建,一般都是拿官方的示例代码过来,在其基础上修改。所以我这里要打破我不喜欢贴大段代码的习惯,贴一个大段代码进来。先不要尝试阅读这段代码,先Ctrl+C/Ctrl+V到IntPosition.NetCodeSourceGenerator.additionalfile文件里面去,后面来一段一段分析:
#templateid: Custom.IntPositionTemplate
#region __GHOST_IMPORTS__
#endregion
namespace Generated
{
public struct GhostSnapshotData
{
struct Snapshot
{
#region __GHOST_FIELD__
public int __GHOST_FIELD_NAME__X;
public int __GHOST_FIELD_NAME__Y;
public int __GHOST_FIELD_NAME__Z;
#endregion
}
public void PredictDelta(uint tick, ref GhostSnapshotData baseline1, ref GhostSnapshotData baseline2)
{
var predictor = new GhostDeltaPredictor(tick, this.tick, baseline1.tick, baseline2.tick);
#region __GHOST_PREDICT__
snapshot.__GHOST_FIELD_NAME__X = predictor.PredictInt(snapshot.__GHOST_FIELD_NAME__X, baseline1.__GHOST_FIELD_NAME__X, baseline2.__GHOST_FIELD_NAME__X);
snapshot.__GHOST_FIELD_NAME__Y = predictor.PredictInt(snapshot.__GHOST_FIELD_NAME__Y, baseline1.__GHOST_FIELD_NAME__Y, baseline2.__GHOST_FIELD_NAME__Y);
snapshot.__GHOST_FIELD_NAME__Z = predictor.PredictInt(snapshot.__GHOST_FIELD_NAME__Z, baseline1.__GHOST_FIELD_NAME__Z, baseline2.__GHOST_FIELD_NAME__Z);
#endregion
}
public void Serialize(int networkId, ref GhostSnapshotData baseline, ref DataStreamWriter writer, StreamCompressionModel compressionModel)
{
#region __GHOST_WRITE__
if ((changeMask & (1 << __GHOST_MASK_INDEX__)) != 0) {
writer.WritePackedIntDelta(snapshot.__GHOST_FIELD_NAME__X, baseline.__GHOST_FIELD_NAME__X, compressionModel);
writer.WritePackedIntDelta(snapshot.__GHOST_FIELD_NAME__Y, baseline.__GHOST_FIELD_NAME__Y, compressionModel);
writer.WritePackedIntDelta(snapshot.__GHOST_FIELD_NAME__Z, baseline.__GHOST_FIELD_NAME__Z, compressionModel);
}
#endregion
}
public void Deserialize(uint tick, ref GhostSnapshotData baseline, ref DataStreamReader reader, StreamCompressionModel compressionModel)
{
#region __GHOST_READ__
if ((changeMask & (1 << __GHOST_MASK_INDEX__)) != 0) {
snapshot.__GHOST_FIELD_NAME__X = reader.ReadPackedIntDelta(baseline.__GHOST_FIELD_NAME__X, compressionModel);
snapshot.__GHOST_FIELD_NAME__Y = reader.ReadPackedIntDelta(baseline.__GHOST_FIELD_NAME__Y, compressionModel);
snapshot.__GHOST_FIELD_NAME__Z = reader.ReadPackedIntDelta(baseline.__GHOST_FIELD_NAME__Z, compressionModel);
}
else {
snapshot.__GHOST_FIELD_NAME__X = baseline.__GHOST_FIELD_NAME__X;
snapshot.__GHOST_FIELD_NAME__Y = baseline.__GHOST_FIELD_NAME__Y;
snapshot.__GHOST_FIELD_NAME__Z = baseline.__GHOST_FIELD_NAME__Z;
}
#endregion
}
public void SerializeCommand(ref DataStreamWriter writer, in IComponentData data, in IComponentData baseline, StreamCompressionModel compressionModel)
{
#region __COMMAND_WRITE__
writer.WriteInt(data.__COMMAND_FIELD_NAME__.x);
writer.WriteInt(data.__COMMAND_FIELD_NAME__.y);
writer.WriteInt(data.__COMMAND_FIELD_NAME__.z);
#endregion
#region __COMMAND_WRITE_PACKED__
writer.WritePackedIntDelta(data.__COMMAND_FIELD_NAME__.x, baseline.__COMMAND_FIELD_NAME__.x, compressionModel);
writer.WritePackedIntDelta(data.__COMMAND_FIELD_NAME__.y, baseline.__COMMAND_FIELD_NAME__.y, compressionModel);
writer.WritePackedIntDelta(data.__COMMAND_FIELD_NAME__.z, baseline.__COMMAND_FIELD_NAME__.z, compressionModel);
#endregion
}
public void DeserializeCommand(ref DataStreamReader reader, ref IComponentData data, in IComponentData baseline, StreamCompressionModel compressionModel)
{
#region __COMMAND_READ__
data.__COMMAND_FIELD_NAME__.x = reader.ReadInt();
data.__COMMAND_FIELD_NAME__.y = reader.ReadInt();
data.__COMMAND_FIELD_NAME__.z = reader.ReadInt();
#endregion
#region __COMMAND_READ_PACKED__
data.__COMMAND_FIELD_NAME__.x = reader.ReadPackedIntDelta(baseline.__COMMAND_FIELD_NAME__.x, compressionModel);
data.__COMMAND_FIELD_NAME__.y = reader.ReadPackedIntDelta(baseline.__COMMAND_FIELD_NAME__.y, compressionModel);
data.__COMMAND_FIELD_NAME__.z = reader.ReadPackedIntDelta(baseline.__COMMAND_FIELD_NAME__.z, compressionModel);
#endregion
}
public unsafe void CopyToSnapshot(ref Snapshot snapshot, ref IComponentData component)
{
if (true) {
#region __GHOST_COPY_TO_SNAPSHOT__
snapshot.__GHOST_FIELD_NAME__X = component.__GHOST_FIELD_REFERENCE__.x;
snapshot.__GHOST_FIELD_NAME__Y = component.__GHOST_FIELD_REFERENCE__.y;
snapshot.__GHOST_FIELD_NAME__Z = component.__GHOST_FIELD_REFERENCE__.z;
#endregion
}
}
public unsafe void CopyFromSnapshot(ref Snapshot snapshotBefore, ref Snapshot snapshotAfter, float snapshotInterpolationFactor, ref IComponentData component)
{
if (true) {
#region __GHOST_COPY_FROM_SNAPSHOT__
component.__GHOST_FIELD_REFERENCE__ = new int3(snapshotBefore.__GHOST_FIELD_NAME__X, snapshotBefore.__GHOST_FIELD_NAME__Y, snapshotBefore.__GHOST_FIELD_NAME__Z));
#endregion
#region __GHOST_COPY_FROM_SNAPSHOT_INTERPOLATE_SETUP__
var __GHOST_FIELD_NAME___Before = new int3(snapshotBefore.__GHOST_FIELD_NAME__X, snapshotBefore.__GHOST_FIELD_NAME__Y, snapshotBefore.__GHOST_FIELD_NAME__Z);
var __GHOST_FIELD_NAME___After = new int3(snapshotAfter.__GHOST_FIELD_NAME__X, snapshotAfter.__GHOST_FIELD_NAME__Y, snapshotAfter.__GHOST_FIELD_NAME__Z);
#endregion
#region __GHOST_COPY_FROM_SNAPSHOT_INTERPOLATE_DISTSQ__
var __GHOST_FIELD_NAME___DistSq = UMath.PVector.DistanceSquared(__GHOST_FIELD_NAME___Before, __GHOST_FIELD_NAME___After);
#endregion
#region __GHOST_COPY_FROM_SNAPSHOT_INTERPOLATE__
component.__GHOST_FIELD_REFERENCE__ = UMath.PVector.Lerp(__GHOST_FIELD_NAME___Before, __GHOST_FIELD_NAME___After, snapshotInterpolationFactor);
#endregion
}
}
public unsafe void RestoreFromBackup(ref IComponentData component, in IComponentData backup)
{
#region __GHOST_RESTORE_FROM_BACKUP__
component.__GHOST_FIELD_REFERENCE__ = backup.__GHOST_FIELD_REFERENCE__;
#endregion
}
public void CalculateChangeMask(ref Snapshot snapshot, ref Snapshot baseline, uint changeMask)
{
#region __GHOST_CALCULATE_INPUT_CHANGE_MASK__
changeMask |= (snapshot.__COMMAND_FIELD_NAME__.x != baseline.__COMMAND_FIELD_NAME__.x ||
snapshot.__COMMAND_FIELD_NAME__.y != baseline.__COMMAND_FIELD_NAME__.y ||
snapshot.__COMMAND_FIELD_NAME__.z != baseline.__COMMAND_FIELD_NAME__.z) ? 1u : 0;
#endregion
#region __GHOST_CALCULATE_CHANGE_MASK_ZERO__
changeMask = (snapshot.__GHOST_FIELD_NAME__X != baseline.__GHOST_FIELD_NAME__X ||
snapshot.__GHOST_FIELD_NAME__Y != baseline.__GHOST_FIELD_NAME__Y ||
snapshot.__GHOST_FIELD_NAME__Z != baseline.__GHOST_FIELD_NAME__Z) ? 1u : 0;
#endregion
#region __GHOST_CALCULATE_CHANGE_MASK__
changeMask |= (snapshot.__GHOST_FIELD_NAME__X != baseline.__GHOST_FIELD_NAME__X ||
snapshot.__GHOST_FIELD_NAME__Y != baseline.__GHOST_FIELD_NAME__Y ||
snapshot.__GHOST_FIELD_NAME__Z != baseline.__GHOST_FIELD_NAME__Z) ? (1u << __GHOST_MASK_INDEX__) : 0;
#endregion
}
#if UNITY_EDITOR || NETCODE_DEBUG
private static void ReportPredictionErrors(ref IComponentData component, in IComponentData backup, ref UnsafeList<float> errors, ref int errorIndex)
{
#region __GHOST_REPORT_PREDICTION_ERROR__
errors[errorIndex] = math.max(errors[errorIndex], UMath.PVector.Distance(component.__GHOST_FIELD_REFERENCE__, backup.__GHOST_FIELD_REFERENCE__));
++errorIndex;
#endregion
}
private static int GetPredictionErrorNames(ref FixedString512Bytes names, ref int nameCount)
{
#region __GHOST_GET_PREDICTION_ERROR_NAME__
if (nameCount != 0) {
names.Append(new FixedString32Bytes(","));
}
names.Append(new FixedString64Bytes("__GHOST_FIELD_REFERENCE__"));
++nameCount;
#endregion
}
#endif
}
}
第一眼看过去绝对大脑爆炸,毕竟这一堆一堆的下划线实在是太不C#了。其实这只是为了防止标识符重复而做的妥协罢了,玩过C++的肯定很熟悉这种做法。
我们一段一段的来看:
#templateid: Custom.IntPositionTemplate
给这个模板一个字符串ID。这里用了“Custom.IntPositionTemplate”这个ID,其实你给它起任何名字都是可以的,只要名字别和其他模板重复就好。比方说起个“MyAwsomeGame.ThisIsJustATemplate”都行。
#region __GHOST_IMPORTS__
#endregion
啊?搞毛?一个空的region?
实际上Netcode就是通过这些region来确定你提供的代码在什么地方的,有些时候也会利用这些region标记代码插入的位置。这里这个空region就是告诉Netcode的Source Generator:把Ghost Imports相关的代码插入到这个地方。
了解了这个特点之后,后面很多乍一看乱七八糟的代码就突然变得有逻辑了。
namespace Generated
{
public struct GhostSnapshotData
{
模板硬性规定,照着写就行。
struct Snapshot
{
#region __GHOST_FIELD__
public int __GHOST_FIELD_NAME__X;
public int __GHOST_FIELD_NAME__Y;
public int __GHOST_FIELD_NAME__Z;
#endregion
}
这里定义保存在Snapshot里的数据格式,int3有三个int字段,所以这里也准备三个int。__GHOST_FIELD_NAME__X
这些名字其实可以自己随便改。但注意#region __GHOST_FIELD__
这一行不要改它。就像前面说的那样,这里是给Source Generator的标记,改了它就不认识了。
public void PredictDelta(uint tick, ref GhostSnapshotData baseline1, ref GhostSnapshotData baseline2)
{
var predictor = new GhostDeltaPredictor(tick, this.tick, baseline1.tick, baseline2.tick);
#region __GHOST_PREDICT__
snapshot.__GHOST_FIELD_NAME__X = predictor.PredictInt(snapshot.__GHOST_FIELD_NAME__X, baseline1.__GHOST_FIELD_NAME__X, baseline2.__GHOST_FIELD_NAME__X);
snapshot.__GHOST_FIELD_NAME__Y = predictor.PredictInt(snapshot.__GHOST_FIELD_NAME__Y, baseline1.__GHOST_FIELD_NAME__Y, baseline2.__GHOST_FIELD_NAME__Y);
snapshot.__GHOST_FIELD_NAME__Z = predictor.PredictInt(snapshot.__GHOST_FIELD_NAME__Z, baseline1.__GHOST_FIELD_NAME__Z, baseline2.__GHOST_FIELD_NAME__Z);
#endregion
}
给Predict系统提供的代码。
GhostSnapshotData
这个类型并不存在,是个占位符,最后会被Source Generator替换成其他的类型。
GhostDeltaPredictor
这个类型的源代码就在GhostDeltaPredictor.cs里,直接就可以在项目中找到。可以去看一下里面PredictInt
的实现,了解一下Prediction系统背后的数学算法。
你可能想问:GhostDeltaPredictor里没有float和double相关的实现啊!我要是float类型这里应该怎么写?
答案是:不用写。
更进一步的,如果你的类型里所有数据都是float或者double,只要留一个空的#region __GHOST_PREDICT__
即可。
public void Serialize(int networkId, ref GhostSnapshotData baseline, ref DataStreamWriter writer, StreamCompressionModel compressionModel)
{
#region __GHOST_WRITE__
if ((changeMask & (1 << __GHOST_MASK_INDEX__)) != 0) {
writer.WritePackedIntDelta(snapshot.__GHOST_FIELD_NAME__X, baseline.__GHOST_FIELD_NAME__X, compressionModel);
writer.WritePackedIntDelta(snapshot.__GHOST_FIELD_NAME__Y, baseline.__GHOST_FIELD_NAME__Y, compressionModel);
writer.WritePackedIntDelta(snapshot.__GHOST_FIELD_NAME__Z, baseline.__GHOST_FIELD_NAME__Z, compressionModel);
}
#endregion
}
public void Deserialize(uint tick, ref GhostSnapshotData baseline, ref DataStreamReader reader, StreamCompressionModel compressionModel)
{
#region __GHOST_READ__
if ((changeMask & (1 << __GHOST_MASK_INDEX__)) != 0) {
snapshot.__GHOST_FIELD_NAME__X = reader.ReadPackedIntDelta(baseline.__GHOST_FIELD_NAME__X, compressionModel);
snapshot.__GHOST_FIELD_NAME__Y = reader.ReadPackedIntDelta(baseline.__GHOST_FIELD_NAME__Y, compressionModel);
snapshot.__GHOST_FIELD_NAME__Z = reader.ReadPackedIntDelta(baseline.__GHOST_FIELD_NAME__Z, compressionModel);
}
else {
snapshot.__GHOST_FIELD_NAME__X = baseline.__GHOST_FIELD_NAME__X;
snapshot.__GHOST_FIELD_NAME__Y = baseline.__GHOST_FIELD_NAME__Y;
snapshot.__GHOST_FIELD_NAME__Z = baseline.__GHOST_FIELD_NAME__Z;
}
#endregion
}
向网络数据里序列化,和从网络数据里反序列化的代码。
if什么什么mask的那一堆直接照抄,这些都是和Netcode内部序列化实现细节有关的玩意儿,不必深究。
DataStreamWriter
和DataStreamReader
都是实际存在的类型,里面有一堆WriteXXX()/ReadXXX()这样的方法。你用哪个类型就调用哪个方法。注意这里用的不是常见的WriteInt()
和ReadInt()
,而是WritePackedIntDelta()
和ReadPackedIntDelta()
,也就是说写到网络数据里的并不是绝对值,而是相对于上一个Snapshot的变化量。这样有助于数据压缩,减少最终网络数据的字节数。
后面的StreamCompressionModel
顾名思义就是个流压缩算法,在意实现的可以自己去翻源代码。
public void SerializeCommand(ref DataStreamWriter writer, in IComponentData data, in IComponentData baseline, StreamCompressionModel compressionModel)
{
#region __COMMAND_WRITE__
writer.WriteInt(data.__COMMAND_FIELD_NAME__.x);
writer.WriteInt(data.__COMMAND_FIELD_NAME__.y);
writer.WriteInt(data.__COMMAND_FIELD_NAME__.z);
#endregion
#region __COMMAND_WRITE_PACKED__
writer.WritePackedIntDelta(data.__COMMAND_FIELD_NAME__.x, baseline.__COMMAND_FIELD_NAME__.x, compressionModel);
writer.WritePackedIntDelta(data.__COMMAND_FIELD_NAME__.y, baseline.__COMMAND_FIELD_NAME__.y, compressionModel);
writer.WritePackedIntDelta(data.__COMMAND_FIELD_NAME__.z, baseline.__COMMAND_FIELD_NAME__.z, compressionModel);
#endregion
}
public void DeserializeCommand(ref DataStreamReader reader, ref IComponentData data, in IComponentData baseline, StreamCompressionModel compressionModel)
{
#region __COMMAND_READ__
data.__COMMAND_FIELD_NAME__.x = reader.ReadInt();
data.__COMMAND_FIELD_NAME__.y = reader.ReadInt();
data.__COMMAND_FIELD_NAME__.z = reader.ReadInt();
#endregion
#region __COMMAND_READ_PACKED__
data.__COMMAND_FIELD_NAME__.x = reader.ReadPackedIntDelta(baseline.__COMMAND_FIELD_NAME__.x, compressionModel);
data.__COMMAND_FIELD_NAME__.y = reader.ReadPackedIntDelta(baseline.__COMMAND_FIELD_NAME__.y, compressionModel);
data.__COMMAND_FIELD_NAME__.z = reader.ReadPackedIntDelta(baseline.__COMMAND_FIELD_NAME__.z, compressionModel);
#endregion
}
我打算让int3类型支持在ICommandData里面使用,所以有了这么一堆代码。
注意看data.__COMMAND_FIELD_NAME__.x
这里,为什么后面跟了个小写的x?实际上你把__COMMAND_FIELD_NAME__
看做是int3类型的一个变量,是不是就懂了?__COMMAND_FIELD_NAME__
也不过是Netcode的Source Generator预留的占位符,最后会替换成你想序列化的类型。
了解了region是拿来进行代码块标记的,这几坨代码的含义也就很清晰了,它们分别定义了四坨代码:直接的写入;将数据变化量压缩后写入;普通的读取;压缩后的变化量数据的读取。
public unsafe void CopyToSnapshot(ref Snapshot snapshot, ref IComponentData component)
{
if (true) {
#region __GHOST_COPY_TO_SNAPSHOT__
snapshot.__GHOST_FIELD_NAME__X = component.__GHOST_FIELD_REFERENCE__.x;
snapshot.__GHOST_FIELD_NAME__Y = component.__GHOST_FIELD_REFERENCE__.y;
snapshot.__GHOST_FIELD_NAME__Z = component.__GHOST_FIELD_REFERENCE__.z;
#endregion
}
}
看过前面的代码之后,这堆玩意儿也就显得亲切了不少,__GHOST_FIELD_REFERENCE__
很明显也是int3类型的。这些代码就是把“外面的”int3数据复制到“里面的”Snapshot数据的过程。至于为啥有个if (true),别问我,我也没搞懂╮( ̄▽ ̄")╭
public unsafe void CopyFromSnapshot(ref Snapshot snapshotBefore, ref Snapshot snapshotAfter, float snapshotInterpolationFactor, ref IComponentData component)
{
if (true) {
#region __GHOST_COPY_FROM_SNAPSHOT__
component.__GHOST_FIELD_REFERENCE__ = new int3(snapshotBefore.__GHOST_FIELD_NAME__X, snapshotBefore.__GHOST_FIELD_NAME__Y, snapshotBefore.__GHOST_FIELD_NAME__Z));
#endregion
#region __GHOST_COPY_FROM_SNAPSHOT_INTERPOLATE_SETUP__
var __GHOST_FIELD_NAME___Before = new int3(snapshotBefore.__GHOST_FIELD_NAME__X, snapshotBefore.__GHOST_FIELD_NAME__Y, snapshotBefore.__GHOST_FIELD_NAME__Z);
var __GHOST_FIELD_NAME___After = new int3(snapshotAfter.__GHOST_FIELD_NAME__X, snapshotAfter.__GHOST_FIELD_NAME__Y, snapshotAfter.__GHOST_FIELD_NAME__Z);
#endregion
#region __GHOST_COPY_FROM_SNAPSHOT_INTERPOLATE_DISTSQ__
var __GHOST_FIELD_NAME___DistSq = UMath.PVector.DistanceSquared(__GHOST_FIELD_NAME___Before, __GHOST_FIELD_NAME___After);
#endregion
#region __GHOST_COPY_FROM_SNAPSHOT_INTERPOLATE__
component.__GHOST_FIELD_REFERENCE__ = UMath.PVector.Lerp(__GHOST_FIELD_NAME___Before, __GHOST_FIELD_NAME___After, snapshotInterpolationFactor);
#endregion
}
}
哦豁,还有高手?我们一块一块来分析。
__GHOST_COPY_FROM_SNAPSHOT__
代码块:顾名思义是从“里面的”Snapshot将数据传递回“外面的”int3的。
__GHOST_COPY_FROM_SNAPSHOT_INTERPOLATE_SETUP__
代码块:又是两行往外传递代码的。
__GHOST_COPY_FROM_SNAPSHOT_INTERPOLATE_DISTSQ__
代码块:拿前一块代码“提取”出来的“什么什么Before”和“什么什么After”计算了一下距离的平方,UMath.PVector.DistanceSquared
是我自己的代码,初中数学课本上的距离的平方的算法:
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static long DistanceSquared(in int3 left, in int3 right)
{
long x = left.x - right.x;
long y = left.y - right.y;
long z = left.z - right.z;
return x * x + y * y + z * z;
}
别问我UMath是啥意思……历史遗留产物……PVector的意思就是Position Vector。
啊咧?最后计算出来的__GHOST_FIELD_NAME___DistSq
好像没有用到?嘛,也只是咱们用不到罢了,Netcode会把这段代码插入到它自己想用的地方去的。
最后__GHOST_COPY_FROM_SNAPSHOT_INTERPOLATE__
代码块,顾名思义就是做线性插值,UMath.PVector.Lerp
代码如下:
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int3 Lerp(in int3 value1, in int3 value2, float amount)
{
return new int3(
Lerp(value1.x, value2.x, amount),
Lerp(value1.y, value2.y, amount),
Lerp(value1.z, value2.z, amount)
);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Lerp(int value1, int value2, float amount)
{
return LerpUnchecked(value1, value2, Clamp01(amount));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int LerpUnchecked(int value1, int value2, float amount)
{
return value1 + (int)((value2 - value1) * (double)amount);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float Clamp01(float value)
{
if (value > 1) {
return 1;
}
else if (value < 0) {
return 0;
}
else {
return value;
}
}
就是把常见的基于float的Lerp算法改成了int的,中间其实是用double进行的运算,为了尽可能的保存精度。
public unsafe void RestoreFromBackup(ref IComponentData component, in IComponentData backup)
{
#region __GHOST_RESTORE_FROM_BACKUP__
component.__GHOST_FIELD_REFERENCE__ = backup.__GHOST_FIELD_REFERENCE__;
#endregion
}
__GHOST_FIELD_REFERENCE__
这个标识符前面已经见过了,这行代码是干什么的也就很清楚了。
public void CalculateChangeMask(ref Snapshot snapshot, ref Snapshot baseline, uint changeMask)
{
#region __GHOST_CALCULATE_INPUT_CHANGE_MASK__
changeMask |= (snapshot.__COMMAND_FIELD_NAME__.x != baseline.__COMMAND_FIELD_NAME__.x ||
snapshot.__COMMAND_FIELD_NAME__.y != baseline.__COMMAND_FIELD_NAME__.y ||
snapshot.__COMMAND_FIELD_NAME__.z != baseline.__COMMAND_FIELD_NAME__.z) ? 1u : 0;
#endregion
#region __GHOST_CALCULATE_CHANGE_MASK_ZERO__
changeMask = (snapshot.__GHOST_FIELD_NAME__X != baseline.__GHOST_FIELD_NAME__X ||
snapshot.__GHOST_FIELD_NAME__Y != baseline.__GHOST_FIELD_NAME__Y ||
snapshot.__GHOST_FIELD_NAME__Z != baseline.__GHOST_FIELD_NAME__Z) ? 1u : 0;
#endregion
#region __GHOST_CALCULATE_CHANGE_MASK__
changeMask |= (snapshot.__GHOST_FIELD_NAME__X != baseline.__GHOST_FIELD_NAME__X ||
snapshot.__GHOST_FIELD_NAME__Y != baseline.__GHOST_FIELD_NAME__Y ||
snapshot.__GHOST_FIELD_NAME__Z != baseline.__GHOST_FIELD_NAME__Z) ? (1u << __GHOST_MASK_INDEX__) : 0;
#endregion
}
还记得前面见过的那个“if什么什么mask”吗?这里就是mask的生成过程,__COMMAND_FIELD_NAME__
和__GHOST_FIELD_NAME__X/Y/Z
都是已经见过的标识符了,代码应该不难理解。
由于和Netcode网络数据流的实现细节紧密相关,这一块儿抄的时候要仔细,看看文档里是怎么写的,看看NetcodeSamples是怎么写的,看看我这里是怎么写的,举一反三。
#if UNITY_EDITOR || NETCODE_DEBUG
private static void ReportPredictionErrors(ref IComponentData component, in IComponentData backup, ref UnsafeList<float> errors, ref int errorIndex)
{
#region __GHOST_REPORT_PREDICTION_ERROR__
errors[errorIndex] = math.max(errors[errorIndex], UMath.PVector.Distance(component.__GHOST_FIELD_REFERENCE__, backup.__GHOST_FIELD_REFERENCE__));
++errorIndex;
#endregion
}
private static int GetPredictionErrorNames(ref FixedString512Bytes names, ref int nameCount)
{
#region __GHOST_GET_PREDICTION_ERROR_NAME__
if (nameCount != 0) {
names.Append(new FixedString32Bytes(","));
}
names.Append(new FixedString64Bytes("__GHOST_FIELD_REFERENCE__"));
++nameCount;
#endregion
}
#endif
最后一段代码,看见#if UNITY_EDITOR || NETCODE_DEBUG
就明白,只在Editor或者Debug的时候起作用,用来输出错误信息的。UMath.PVector.Distance
的代码就不贴了,DistanceSqaured都有了还能不知道Distance怎么计算吗?
打开UserDefinedTemplates.cs文件,直接照抄:
using System.Collections.Generic;
namespace Unity.NetCode.Generators
{
public static partial class UserDefinedTemplates
{
static partial void RegisterTemplates(List<TypeRegistryEntry> templates, string defaultRootPath)
{
templates.AddRange(new[] {
new TypeRegistryEntry {
Type = "Unity.Mathematics.int3",
Quantized = false,
Smoothing = SmoothingAction.InterpolateAndExtrapolate,
SupportCommand = true,
Composite = false,
Template = "Custom.IntPositionTemplate",
TemplateOverride = "",
}
});
}
}
}
partial class?partial void方法?另一半去哪里了?
你能在Library\PackageCache\com.unity.netcode\Runtime\Authoring\UserDefinedTemplates.cs
找到这个类的另一半。你会发现Netcode写了个RegisterTemplates却没写实现。这个实现就是在这里由我们提供的了。
至于为什么要用这么弯弯绕的方法把这个函数“插入”进去?是因为Unity的Source Generator限制,它需要在Netcode库编译的时候就能看到这些代码,因此才会搞的这么复杂。
然后我们来分析TypeRegistryEntry
每一项都是干啥的:
Type
:你需要序列化的类型,这里我们填上int3的带上namespace的完整类型名。Quantized
:对于int类型没有意义,所以是false。如果你想加入这方面的支持,可以看看官方文档里面,__GHOST_QUANTIZE_SCALE__
和__GHOST_DEQUANTIZE_SCALE__
这两个标识符分别用在了什么地方,照着做就好。或者去看我后面会提到的一堆“示例文件”。Smoothing
:之所以我们费这么大劲写这么一大堆代码就是为了让int类型支持Smoothing,否则我就不用int3了,直接摆三个int不也一样么。所以这里当然要用SmoothingAction.InterpolateAndExtrapolate
。SupportCommand
:如果你这里写成false,那么Template里就可以少些一些代码。那些标识符上带着COMMAND的代码块就都可以不要。我们代码都写完了,当然是true。Composite
:建议就用false。用true的话,Source Generator使用Template生成代码的方式会有变化,在像int3这种,其内部所有字段都是相同的类型的场合,能让你省点事,少打一些Template代码。但是生成的规则会变得更复杂一些,我懒得想那么多,一般就false了。Template
:第一行模板代码里指定的#templateidTemplateOverride
:作用是让你写的这个模板替换掉Netcode自带的模板,只不过没有详细的文档和示例说明这玩意儿该怎么用。不管它(~ ̄▽ ̄)~Netcode自带的模板位于这个文件夹里:Library\PackageCache\com.unity.netcode\Editor\Templates\DefaultTypes
。这些文件也是非常棒的示例文件,只不过大部分文件都不完整(Netcode最后会自己拼成完整的)。比较完整的有:
GhostSnapshotValueInt.cs
GhostSnapshotValueUInt.cs
GhostSnapshotValueFloat.cs
GhostSnapshotValueFloatUnquantized.cs
GhostSnapshotValueQuaternion.cs
GhostSnapshotValueQuaternionUnquantized.cs
另外GhostSnapshotValueEntity.cs也很值得一看,毕竟和数学类型不同,Entity是一个逻辑类型,模板的编写方式自然也不太一样。
除了上面说的那些以外,还有一个TypeRegistryEntry.SubType
,怎么用可以去看官方文档和NetcodeSamples。其实用起来很简单,只需要写两行代码,然后点一个选项。但是解释SubType这个概念需要另开一篇文章,而且这文章写到最后也难免变成官方文档的汉化版。所以我就偷懒不写了>_<
我们来创建一个类型:
public struct WorldEntityTransform : IComponentData
{
[GhostField(Composite = true, Smoothing = SmoothingAction.Interpolate)]
public int3 Position;
}
然后让Unity去编译。如果没出问题,编译通过,就能用了。
注意这里的GhostField.Composite
和前面的TypeRegistryEntry.Composite
完全不是一码事。这里是设置“数据有变化之后,Netcode要怎么在网络数据流里进行标记”的。我这里设置为true,是因为对于三维空间的位置坐标来说,经常是XYZ三个值一起变,用Composite在大部分情况下可以节省两个bit。
如果编译出现问题了呢?
大概率就是你Template文件没写好,怎么改?错误信息提示的行数根本找不到啊!
实际上这里错误信息给出的行数并不是Template文件里的行数,而是Source Generator生成的代码里的行数。这个代码在Visual Studio里是找不到的,要去这个地方找:Temp\NetCodeGenerated\Assembly-CSharp
。
在这里你会找到一个以WorldEntityTransformSerializer.cs结尾的C#代码文件。打开后,往下翻一翻,有没有觉得有点眼熟?这不就是刚才写的模板文件,加了一堆有的没的之后的东西嘛!
找到这个文件以后,就可以根据错误提示的行数,找到出错的地方,然后回到Template文件里找到对应的地方,进行修改即可。
接下来,你可以回到UserDefinedTemplates那边,把Composite改成true,然后看看生成的代码变成了什么鬼样子。折腾几次之后,就应该能明白这个玩意儿要怎么用了。如果还是搞不懂,那就放着不管,反正也不是什么不用不行的东西。
也可以给WorldEntityTransform加几个别的字段,看看最后会生成什么。借此了解一下Netcode的底层实现。
总算是结束了。我能理解Netcode为啥会设计成这个样子,毕竟要支持的功能确实有点多,又想要同时保证高性能,自然省不了事。还好这套玩意儿也就是第一次上手的时候理解起来比较累,跨过了这个坎之后,就…………就特么再也不想碰它了😂
代码能工作不?能!好了!别动了!就这样了!