一句話省流:很麻煩也很抽象,能用內建支援的型別就儘量用。
首先看文件。官方文件裡一開頭就列出了所有內建的支援的型別: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內部序列化實現細節有關的玩意兒,不必深究。
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怎麼計算嗎?
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為啥會設計成這個樣子,畢竟要支援的功能確實有點多,又想要同時保證高效能,自然省不了事。還好這套玩意兒也就是第一次上手的時候理解起來比較累,跨過了這個坎之後,就…………就特麼再也不想碰它了😂
程式碼能工作不?能!好了!別動了!就這樣了!