Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Classification ghost type in samples seems wrong

Discussion in 'NetCode for ECS' started by frimarichard, Aug 11, 2023.

  1. frimarichard

    frimarichard

    Joined:
    Jul 24, 2017
    Posts:
    31
    I'm integrating a classification system by following the samples (https://github.com/Unity-Technologi.../02_PredictedSpawning/ClassificationSystem.cs), and it seems that the newGhostSpawn.GhostType and the job's ghostType will never match.

    For me, newGhostSpawn.GhostType is an index counting up from 0, while the job's ghostType field is a hash of the four ints in GhostCollectionPrefab.GhostType.

    Also a couple of related questions:
    - Is the slotIndex parameter in snapshotDataLookup.TryGetComponentDataFromSnapshotHistory important? I tried following its usage but couldn't see why it should be used, and the sample uses the outer loop value which also feels wrong.
    - Despite using the SpawnId field from the sample, and my logs showing both client and server agree on the Id value, the classification system still doesn't reliably replace the client's predicted ghost with the server's ghost. It seems that the predictedSpawnList isn't always populated, or at the right moment.

    Any help in deciphering this feature would be much appreciated.
     
    omg_peter likes this.
  2. frimarichard

    frimarichard

    Joined:
    Jul 24, 2017
    Posts:
    31
    After further debugging, the issue appears to be entirely related to
    snapshotDataLookup.TryGetComponentDataFromSnapshotHistory
    . Despite logging on the server and client both agreeing with the custom spawn data ID (a uint, copied from the sample), the value retrieved from snapshotDataLookup.TryGetComponentDataFromSnapshotHistory always returns a nonsensical number.

    With a slotIndex of 0 (the default), the value is simply wrong.
    With a slotIndex of i (copied from the sample), the value is wrong, but also if there are more than 6 ghosts in the queue then an internal exception is thrown.

    @CMarastoni, I'd love some input :)
     
  3. frimarichard

    frimarichard

    Joined:
    Jul 24, 2017
    Posts:
    31
    This is what I'm using.

    Code (CSharp):
    1. [WithAll(typeof(GhostSpawnQueue))]
    2. [BurstCompile]
    3. partial struct SpawnDataClassificationJob : IJobEntity
    4. {
    5.     public SnapshotDataLookupHelper SnapshotDataLookupHelper;
    6.     public Entity SpawnListEntity;
    7.     public BufferLookup<PredictedGhostSpawn> PredictedSpawnListLookupRW;
    8.     [ReadOnly] public ComponentLookup<SpawnData> EventDataLookupRO;
    9.     public int GhostType;
    10.  
    11.     public void Execute(DynamicBuffer<GhostSpawnBuffer> ghosts, in DynamicBuffer<SnapshotDataBuffer> data)
    12.     {
    13.         var inspector = SnapshotDataLookupHelper.CreateSnapshotBufferLookup();
    14.         var predictedSpawnList = PredictedSpawnListLookupRW[SpawnListEntity];
    15.  
    16.         for (int i = 0; i < ghosts.Length; i++)
    17.         {
    18.             var newGhostSpawn = ghosts[i];
    19.             if (newGhostSpawn.SpawnType != GhostSpawnBuffer.Type.Predicted || newGhostSpawn.HasClassifiedPredictedSpawn || newGhostSpawn.PredictedSpawnEntity != Entity.Null)
    20.                 continue;
    21.  
    22.             // The value returned from here is always nonsense.
    23.             if (!inspector.TryGetComponentDataFromSnapshotHistory(newGhostSpawn.GhostType, data, out SpawnData snapshotData))
    24.                 continue;
    25.  
    26.             // This never matches (newGhostSpawn.GhostType is an increasing index, GhostType is a hash of 4 ints).
    27.             if (newGhostSpawn.GhostType == GhostType)
    28.                 newGhostSpawn.HasClassifiedPredictedSpawn = true;
    29.  
    30.             for (int j = 0; j < predictedSpawnList.Length; j++)
    31.             {
    32.                 ref var spawnData = ref predictedSpawnList.ElementAt(j);
    33.  
    34.                 if (newGhostSpawn.GhostType != spawnData.ghostType)
    35.                     continue;
    36.  
    37.                 ref readonly var lookupData = ref EventDataLookupRO.GetRefRO(spawnData.entity).ValueRO;
    38.                 if (snapshotData.Id == lookupData.Id)
    39.                 {
    40.                     newGhostSpawn.PredictedSpawnEntity = spawnData.entity;
    41.                     newGhostSpawn.HasClassifiedPredictedSpawn = true;
    42.  
    43.                     predictedSpawnList[j] = predictedSpawnList[^1];
    44.                     predictedSpawnList.RemoveAt(predictedSpawnList.Length - 1);
    45.                     break;
    46.                 }
    47.             }
    48.  
    49.             ghosts[i] = newGhostSpawn;
    50.         }
    51.     }
    52. }
    Code (CSharp):
    1. public struct SpawnData : IComponentData
    2. {
    3.     [GhostField] public uint Id;
    4. }
     
  4. CMarastoni

    CMarastoni

    Unity Technologies

    Joined:
    Mar 18, 2020
    Posts:
    774
    The newGhostSpawn.GhostType is an index in the GhostPrefabSerializer and GhostPrefabCollection list. As such is not the same value of the GhostType component.
    If you need to match the hash then it is necessary some extra steps and lookup into the GhostCollectionPrefab.
    The sample code is actually incorrect.

    The TryGetComponentDataFromSnapshotHistory(newGhostSpawn.GhostType, data, out SpawnData snapshotData) should be called only when the if (newGhostSpawn.GhostType != spawnData.ghostType) return true.

    It should not return nonsensical values in general. Should return false if the component is missing and in that case the value is 0.
    But if the component type is present on the ghost it should return its correct value.

    The slot index should be alway 0 for the classification system. The sample is again incorrect. The slot-index with a value different than 0 should be used to inspect the content of the Snapshot history buffer for a ghost (that contains up to 32 of them). The predicted spawned ghost only contains one snapshot (in 0) an an exception is even throw to advise.
     
  5. frimarichard

    frimarichard

    Joined:
    Jul 24, 2017
    Posts:
    31
    Ah that's great news, I thought I was going crazy.

    I did some more snooping at found an extremely useful function:
    TryGetComponentDataFromSpawnBuffer


    This seems to work nicely:

    Code (CSharp):
    1. [WithAll(typeof(GhostSpawnQueue))]
    2. [BurstCompile]
    3. partial struct SpawnDataClassificationJob : IJobEntity
    4. {
    5.     public SnapshotDataLookupHelper SnapshotDataLookupHelper;
    6.     public Entity SpawnListEntity;
    7.     public BufferLookup<PredictedGhostSpawn> PredictedSpawnListLookupRW;
    8.     [ReadOnly] public ComponentLookup<SpawnData> SpawnDataLookupRO;
    9.  
    10.     public void Execute(DynamicBuffer<GhostSpawnBuffer> ghosts, in DynamicBuffer<SnapshotDataBuffer> data)
    11.     {
    12.         if (ghosts.Length == 0)
    13.             return;
    14.  
    15.         var inspector = SnapshotDataLookupHelper.CreateSnapshotBufferLookup();
    16.         var predictedSpawnList = PredictedSpawnListLookupRW[SpawnListEntity];
    17.  
    18.         for (int i = 0; i < ghosts.Length; i++)
    19.         {
    20.             var newGhostSpawn = ghosts[i];
    21.             if (newGhostSpawn.SpawnType != GhostSpawnBuffer.Type.Predicted || newGhostSpawn.HasClassifiedPredictedSpawn || newGhostSpawn.PredictedSpawnEntity != Entity.Null)
    22.                 continue;
    23.  
    24.             if (!inspector.TryGetComponentDataFromSpawnBuffer(newGhostSpawn, data, out SpawnData snapshotData))
    25.                 continue;
    26.  
    27.             newGhostSpawn.HasClassifiedPredictedSpawn = true;
    28.  
    29.             for (int j = predictedSpawnList.Length - 1; j >= 0; j--)
    30.             {
    31.                 ref var spawnData = ref predictedSpawnList.ElementAt(j);
    32.  
    33.                 if (newGhostSpawn.GhostType != spawnData.ghostType)
    34.                     continue;
    35.  
    36.                 ref readonly var lookupData = ref SpawnDataLookupRO.GetRefRO(spawnData.entity).ValueRO;
    37.                 if (snapshotData.Id == lookupData.Id)
    38.                 {
    39.                     newGhostSpawn.PredictedSpawnEntity = spawnData.entity;
    40.                     predictedSpawnList.RemoveAtSwapBack(j);
    41.                     break;
    42.                 }
    43.             }
    44.  
    45.             ghosts[i] = newGhostSpawn;
    46.         }
    47.     }
    48. }
    Thanks very much for the assist!
     
  6. frimarichard

    frimarichard

    Joined:
    Jul 24, 2017
    Posts:
    31
    So I'm still seeing an issue with the client predicted spawning. Sometimes, the client predicted spawn is destroyed by
    Unity.NetCode.PredictedGhostSpawnSystem
    , but then later in the same frame, the server version is created by
    Unity.NetCode.GhostSpawnSystem
    . At this point, because the client predicted ghost has been destroyed, the systems which act upon it run again for the server version, creating further bugs.

    This same-frame destroy and create has been verified via journaling, which also shows that the client predicted spawn is destroyed after 3 frames.

    This doesn't happen every time, but is far from rare. Again, I'm setting the GhostOwner on spawn based on the sample (https://github.com/Unity-Technologi...redictedSpawning/ProcessFireCommandsSystem.cs).

    Code (CSharp):
    1. // Set the spawn ID for this particular local spawn so it can be used later in the classification system
    2. // Needs to include the network ID of the owner since everyone's counters/spawnId starts at 1
    3. var spawnId = input.AbilityButtonChanged.Count << 11 | (uint)owner.NetworkId;
    4. commandBuffer.SetComponent(triggeredAbility, new SpawnData { Id = spawnId });
    5.  
    6. // Set the owner so the prediction will work.
    7. commandBuffer.AddComponent(triggeredAbility, new GhostOwner { NetworkId = owner.NetworkId });
    I'm looking through the code for PredictedGhostSpawnSystem, and the only way the destroy is called is when the server's last interpolated tick is newer than the local predicted spawn. Is there anything obvious I'm missing?
     
  7. optimise

    optimise

    Joined:
    Jan 22, 2014
    Posts:
    2,029
    Hi @CMarastoni. Can u provide working code at there first while waiting for new sample update to fix this sample?
     
    omg_peter likes this.
  8. NikiWalker

    NikiWalker

    Unity Technologies

    Joined:
    May 18, 2021
    Posts:
    224
    I landed a fix for a few issues in the PredictedSpawn sample today. I've zipped the relevant code changes here if you want quick access.
     

    Attached Files:

    Richay, FaithlessOne and optimise like this.