首页 > 其他分享 >Netcode for Entities如何添加自定义序列化,让GhostField支持任意类型?以int3为例(1.2.3版本)

Netcode for Entities如何添加自定义序列化,让GhostField支持任意类型?以int3为例(1.2.3版本)

时间:2024-07-17 18:53:06浏览次数:17  
标签:__ Netcode .__ NAME 自定义 GHOST FIELD COMMAND 序列化

一句话省流:很麻烦也很抽象,能用内置支持的类型就尽量用。

首先看文档。官方文档里一开头就列出了所有内置的支持的类型: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等等。然后我准备拿它当位置坐标来用。

1、创建Template文件

自定义序列化的原理是:提供一个代码模板文件,然后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里显示的流程非常类似,其实这里说的才是这个功能本来的用法)

2、编辑Template文件

回到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
    }
}

3、这一大坨特喵的到底是个啥

第一眼看过去绝对大脑爆炸,毕竟这一堆一堆的下划线实在是太不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内部序列化实现细节有关的玩意儿,不必深究。
DataStreamWriterDataStreamReader都是实际存在的类型,里面有一堆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怎么计算吗?

4、编写UserDefinedTemplates

打开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:第一行模板代码里指定的#templateid
  • TemplateOverride:作用是让你写的这个模板替换掉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这个概念需要另开一篇文章,而且这文章写到最后也难免变成官方文档的汉化版。所以我就偷懒不写了>_<

5、好了,能用了吗?

我们来创建一个类型:

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的底层实现。

6、666

总算是结束了。我能理解Netcode为啥会设计成这个样子,毕竟要支持的功能确实有点多,又想要同时保证高性能,自然省不了事。还好这套玩意儿也就是第一次上手的时候理解起来比较累,跨过了这个坎之后,就…………就特么再也不想碰它了

标签:__,Netcode,.__,NAME,自定义,GHOST,FIELD,COMMAND,序列化
From: https://www.cnblogs.com/horeaper/p/18303729

相关文章

  • 关于在vue2中使用LogicFlow自定义节点
    主要参考LogicFlow官方文档在基础流程图搭建起来后,我们想要构建自己的需求风格,例如:那么该如何对节点进行自定义设定呢?文档当中有着详细的解释,本文以实际需求为例大体介绍:import{RectNode,RectNodeModel,h}from"@logicflow/core";classCustomNodeViewextendsR......
  • Hive自定义函数编写方法(含源代码解读,超详细,易理解)
    一、Hive自定义函数介绍        1.内置函数        Hive自带了一些函数。比如:max/min等,但是数量有限,自己可以通过自定义UDF来方便的扩展。2.自定义函数        当Hive提供的内置函数无法满足你的业务处理需要时,此时就可以考虑使用用户自定义函数(UD......
  • 三分钟了解自定义表单自定义工作流的多个优势
    降本、提高效率、解决信息孤岛是很多企业亟需要解决的问题。什么样的软件平台可以实现这一目标?可以随时来了解低代码技术平台。它当中的自定义表单自定义工作流拥有多个优势特点,可以为企业降低技术门槛、提高工作效率,可视化操作界面的便利性更让职场朋友们深知是实现流程化办公的......
  • Nuxt.js头部魔法:轻松自定义页面元信息,提升用户体验
    扫描二维码关注或者微信搜一搜:编程智域前端至全栈交流与成长useHead 函数概述useHead是一个用于在Nuxt应用中自定义页面头部属性的函数。它由Unhead库提供支持,允许开发者以编程和响应式的方式设置每个页面的头部信息。useHead 函数类型useHead(meta:MaybeComputedRef<......
  • C++自定义双向迭代器
    #include<cassert>#include<memory>#include<vector>#include<iostream>classRange{public:usingIndex=uint64_t;usingSignedIndex=int64_t;usingOffset=int64_t;usingSize=uint64_t;Range()=d......
  • 自定义localStorage监听事件
    一、问题在项目开发过程中,发现有很多时候进行localStorage.setItem()操作设置本地存储后,页面必须刷新才能够获取到存储数据,而有些时候本地缓存更新后,页面无法通过再次刷新以获取本地缓存,这就导致依赖本地缓存的数据无法进行更新。为了解决这个问题,就必须要用到自定义localStorage......
  • elementui的el-cascader-panel在jsx里如何自定义label和props属性
    render(){return(<el-cascader-panelonChange={(val)=>{this.handleFormatChange(val,'format','dataColumns',indexInMap)}}props={{renderLabel:(params)=>{......
  • 木舟0基础学习Java的第十九天(装饰设计模式,转换流,对象操作流(序列化),Properties集合)
    装饰设计模式创建一个接口用一个类实现接口再创建一个类实现这个接口第二个类中包含第一个类中的方法和自己的方法还可以增强案例:publicinterfaceCar{publicvoidrun();publicvoidcarry();}publicclassTaxiimplementsCar{@Overridepub......
  • ollama 模型国内加速下载,制作自定义Modelfile模型文件
    参考:https://www.zhihu.com/question/640579563/answer/3562899008https://github.com/ollama/ollama/blob/main/docs/modelfile.mdgguf格式介绍:https://www.datalearner.com/blog/10517057188355861、ollama模型国内加速下载ollama主要的模型文件格式是gguf,可以在mo......
  • 【扣子coze+微信开发者工具】实现ai自定义对话03:微信小程序js逻辑和接口实现
    目录摘要一、前言二、扣子API1. 扣子的API文档理解2.对话API深度理解2.1 bot_id2.2 additional_messages2.2.1role2.2.2 type2.2.3 content_type2.2.4 content2.3stream三、.js文件——发起对话(逻辑代码)1.纯文本text对话1.1content传入内容包装2.......