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 Debugging a Player Spawned Ghost (Spawning multiple times on client)

Discussion in 'NetCode for ECS' started by EphiBL, Jun 19, 2023.

  1. EphiBL

    EphiBL

    Joined:
    Mar 29, 2023
    Posts:
    5
    Hey there,

    I'm relatively new the Unity but have been loving DOTS and am really excited at the potential for Netcode for Entities.

    I'm having a specific issue in my project with spawning projectiles as a player owned ghosts. The briefest summary I can give is, when my player Left Clicks to spawn the projectile after a delay, the projectile is spawned multiple times on the client depending on RTT/Ping.

    Here's a video of that in action:



    You can see that in the client world, projectiles are spawned rapidly up until a round trip is completed. I can't for the life of me figure out why.

    Here's the system that handles spawning the projectile:

    Code (CSharp):
    1. using Unity.Burst;
    2. using Unity.Entities;
    3. using Unity.Logging.Sinks;
    4. using Unity.Mathematics;
    5. using Unity.NetCode;
    6. using Unity.Transforms;
    7. using UnityEngine;
    8.  
    9. [BurstCompile]
    10. [UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
    11. [UpdateAfter(typeof(CharacterCastingSystem))]
    12. public partial struct AbilitySystem : ISystem
    13. {
    14.     public void OnCreate(ref SystemState state)
    15.     {
    16.         state.RequireForUpdate<Abilities>();
    17.         state.RequireForUpdate<NetworkTime>();
    18.     }
    19.  
    20.     public void OnUpdate(ref SystemState state)
    21.     {
    22.         var ecb = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>()
    23.             .CreateCommandBuffer(state.WorldUnmanaged);
    24.         var networkTime = SystemAPI.GetSingleton<NetworkTime>();
    25.  
    26.         foreach (var (abilities, properties, transform, castData, characterEntity) in SystemAPI.Query<
    27.                      RefRW<Abilities>,
    28.                      RefRO<CharacterProperties>,
    29.                      RefRW<LocalTransform>,
    30.                      RefRW<CastData>>()
    31.                      .WithEntityAccess()
    32.                      .WithAll<Simulate>()
    33.                      .WithAll<GhostOwnerIsLocal>())
    34.         {
    35.            
    36.             // Don't run on re-sim
    37.             if (!networkTime.IsFirstTimeFullyPredictingTick)
    38.                 return;
    39.            
    40.             GhostOwner ghostOwner = SystemAPI.GetComponent<GhostOwner>(characterEntity);
    41.             int networkId = ghostOwner.NetworkId;
    42.            
    43.             if (abilities.ValueRO.M1 && castData.ValueRO.CastingState == CastingState.IsCasting)
    44.             {
    45.                 Debug.Log("Spawning Projectile");
    46.                 var projectile = ecb.Instantiate(properties.ValueRO.M1);
    47.                 ecb.AddComponent(projectile, new Projectile());
    48.                 ecb.AddComponent(projectile, new GhostOwner { NetworkId = networkId });
    49.                
    50.                 ecb.SetComponent(projectile, new LocalTransform
    51.                 {
    52.                     Position = transform.ValueRO.Position,
    53.                     Rotation = quaternion.identity,
    54.                     Scale = 1f
    55.                 });
    56.                
    57.                
    58.                
    59.                 castData.ValueRW.CastingState = CastingState.Idle;
    60.                 abilities.ValueRW.M1 = false;
    61.             }
    62.         }
    63.     }
    64. }
    65.  
    Have pulled my hair out trying to figure this out solo for enough time x')

    Hoping someone can help! Can provide any additional details
     
  2. philsa-unity

    philsa-unity

    Unity Technologies

    Joined:
    Aug 23, 2022
    Posts:
    113
    At the moment, my only theory would be that "abilities.ValueRO.M1" might be true on multiple frames in a row

    I see you've used the "if (!networkTime.IsFirstTimeFullyPredictingTick) return;" in order to prevent instantiating multiple times in prediction, which is good. But since you only set "abilities.ValueRO.M1" back to false when IsFirstTimeFullyPredictingTick, that means that all subsequent re-simulations of this tick will not set the value back to false, and so the value will stay true (at least that's my theory for now)

    A quick fix would be to:
    • remove your "if (!networkTime.IsFirstTimeFullyPredictingTick) return;"
    • wrap only the ecb commands part under a "if (networkTime.IsFirstTimeFullyPredictingTick)"
    Code (CSharp):
    1. using Unity.Burst;
    2. using Unity.Entities;
    3. using Unity.Logging.Sinks;
    4. using Unity.Mathematics;
    5. using Unity.NetCode;
    6. using Unity.Transforms;
    7. using UnityEngine;
    8. [BurstCompile]
    9. [UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
    10. [UpdateAfter(typeof(CharacterCastingSystem))]
    11. public partial struct AbilitySystem : ISystem
    12. {
    13.     public void OnCreate(ref SystemState state)
    14.     {
    15.         state.RequireForUpdate<Abilities>();
    16.         state.RequireForUpdate<NetworkTime>();
    17.     }
    18.     public void OnUpdate(ref SystemState state)
    19.     {
    20.         var ecb = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>()
    21.             .CreateCommandBuffer(state.WorldUnmanaged);
    22.         var networkTime = SystemAPI.GetSingleton<NetworkTime>();
    23.         foreach (var (abilities, properties, transform, castData, characterEntity) in SystemAPI.Query<
    24.                      RefRW<Abilities>,
    25.                      RefRO<CharacterProperties>,
    26.                      RefRW<LocalTransform>,
    27.                      RefRW<CastData>>()
    28.                      .WithEntityAccess()
    29.                      .WithAll<Simulate>()
    30.                      .WithAll<GhostOwnerIsLocal>())
    31.         {
    32.          
    33.             GhostOwner ghostOwner = SystemAPI.GetComponent<GhostOwner>(characterEntity);
    34.             int networkId = ghostOwner.NetworkId;
    35.          
    36.             if (abilities.ValueRO.M1 && castData.ValueRO.CastingState == CastingState.IsCasting)
    37.             {
    38.                 if (networkTime.IsFirstTimeFullyPredictingTick)
    39.                 {
    40.                     Debug.Log("Spawning Projectile");
    41.                     var projectile = ecb.Instantiate(properties.ValueRO.M1);
    42.                     ecb.AddComponent(projectile, new Projectile());
    43.                     ecb.AddComponent(projectile, new GhostOwner { NetworkId = networkId });
    44.              
    45.                     ecb.SetComponent(projectile, new LocalTransform
    46.                     {
    47.                         Position = transform.ValueRO.Position,
    48.                         Rotation = quaternion.identity,
    49.                         Scale = 1f
    50.                     });
    51.  
    52.                 }
    53.              
    54.              
    55.              
    56.                 castData.ValueRW.CastingState = CastingState.Idle;
    57.                 abilities.ValueRW.M1 = false;
    58.             }
    59.         }
    60.     }
    61. }
     
    Last edited: Jun 24, 2023
  3. EphiBL

    EphiBL

    Joined:
    Mar 29, 2023
    Posts:
    5
    Wow, kicking myself, that worked. I could've sworn I tried this! o_O You're the goat, thank you!

    Tagging on an extra question for my next steps, I've been getting some mixed messages about Predicted spawning of player owned ghosts.

    Someone in the Unity discord suggested it was handled automatically, but the HelloNetcode sample for Predicted Spawning uses a unique Classification System but cites something about this being a custom approach.

    Is there a recommended approach for handling player owned predicted spawns? Or somewhere I can read about it?

    Thanks as always
     
  4. CMarastoni

    CMarastoni

    Unity Technologies

    Joined:
    Mar 18, 2020
    Posts:
    762
    hey!

    Predicted spawning of ghosts has some default handling in Netcode (as described in the class
    DefaultGhostSpawnClassificationSystem) that uses a very basic range-check on the tick (that work for most basic case) to match the local spawned entity by the client with the authoritative one from the server.

    And this is by no mean what we use in Asteroids sample (that use predicted spawning for the bullet). But for more robust handling, you need to write your own custom classification system.

    In term of documentation we some introductory material here https://docs.unity3d.com/Packages/com.unity.netcode@1.0/manual/ghost-spawning.html.

    but I strongly suggest to look at the HelloNetcodeSample/PredictedSpawning demo https://github.com/Unity-Technologies/EntityComponentSystemSamples/tree/master/NetcodeSamples/Assets/Samples/HelloNetcode/2_Intermediate/02_PredictedSpawning

    It explain how a more robust spawning mechanism (that use a custom classification system, unique deterministic id) to match the spawned entity.