<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      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內部序列化實現細節有關的玩意兒,不必深究。
      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是拿來進行代碼塊標記的,這幾坨代碼的含義也就很清晰了,它們分別定義了四坨代碼:直接的寫入;將數據變化量壓縮后寫入;普通的讀?。粔嚎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:第一行模板代碼里指定的#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為啥會設計成這個樣子,畢竟要支持的功能確實有點多,又想要同時保證高性能,自然省不了事。還好這套玩意兒也就是第一次上手的時候理解起來比較累,跨過了這個坎之后,就…………就特么再也不想碰它了??
      代碼能工作不?能!好了!別動了!就這樣了!

      posted @ 2024-07-17 18:44  horeaper  閱讀(265)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 亚洲AV天天做在线观看| 粉嫩jk制服美女啪啪| 国产成人亚洲综合91精品| 无码人妻一区二区三区AV| 蜜臀午夜一区二区在线播放| 国精产品999国精产| 亚洲天堂成人黄色在线播放| 亚洲尤码不卡av麻豆| 国产三级黄色的在线观看| 国产成人无码午夜视频在线播放| 国产精品污双胞胎在线观看| 91福利国产午夜亚洲精品| a4yy私人毛片| 河北真实伦对白精彩脏话| 日韩放荡少妇无码视频| 亚洲日本韩国欧美云霸高清| 久久免费偷拍视频有没有| 巴马| 国产精品中文字幕第一区| 日韩人妻一区中文字幕| 日本一卡二卡3卡四卡网站精品 | 无遮无挡爽爽免费视频| 亚洲人成电影网站 久久影视| 国产亚洲精品第一综合麻豆| 亚洲国产精品久久久久4婷婷| 一本色道国产在线观看二区| 国产丰满麻豆videossexhd| 精品偷自拍另类精品在线| 亚洲国产精品综合久久2007| 四川丰满少妇无套内谢| 国产激情国产精品久久源| 99久久国产综合精品女图图等你| 中文字幕日本一区二区在线观看| 久久久成人毛片无码| 国产97视频人人做人人爱| 亚洲午夜亚洲精品国产成人| 国产精品视频一区二区不卡| 中文字幕国产精品日韩| 18禁裸乳无遮挡自慰免费动漫| 久久人妻精品国产| 国产对白熟女受不了了|