Netcode for Entities如何添加自定義序列化,讓GhostField支持任意類型?以int3為例(1.2.3版本)
一句話省流:很麻煩也很抽象,能用內置支持的類型就盡量用。
首先看文檔。官方文檔里一開頭就列出了所有內置的支持的類型: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里,直接就可以在項目中找到??梢匀タ匆幌吕锩?code>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是拿來進行代碼塊標記的,這幾坨代碼的含義也就很清晰了,它們分別定義了四坨代碼:直接的寫入;將數據變化量壓縮后寫入;普通的讀?。粔嚎s后的變化量數據的讀取。
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:第一行模板代碼里指定的#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這個概念需要另開一篇文章,而且這文章寫到最后也難免變成官方文檔的漢化版。所以我就偷懶不寫了>_<
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為啥會設計成這個樣子,畢竟要支持的功能確實有點多,又想要同時保證高性能,自然省不了事。還好這套玩意兒也就是第一次上手的時候理解起來比較累,跨過了這個坎之后,就…………就特么再也不想碰它了??
代碼能工作不?能!好了!別動了!就這樣了!

浙公網安備 33010602011771號