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

[NetCode] Proper disconnect method for crashed client

Discussion in 'NetCode for ECS' started by Neodamus, Dec 26, 2020.

  1. Neodamus

    Neodamus

    Joined:
    Jan 12, 2018
    Posts:
    14
    The netcode docs explains that the way to disconnect is to add a NetworkStreamRequestDisconnect component to the connection entity. This works fine and as expected when this is done.

    The issue I'm running into is if the client's application crashes, there is no way for the client to add this component, and the connection entity is not automatically destroyed. Default behavior appears to be that the connection entity will stay around for some time then automatically timeout after ~30s. It seems that if the client reconnects while the previous connection entity still exists, the new connection will encounter strange errors such as:

    - The new connection will be destroyed when the old connection is timed out, disconnecting the reconnected client.
    - Trying to send an RPC on the new connection will immediately disconnect the client and server will produce error similar to this: RpcSystem received invalid rpc from connection 0

    I made a system on the server that can detect that the client stops sending snapshots, and then add the NetworkStreamRequestDisconnect component from the server side to dispose of the connection, which does work in getting rid of the connection entity, but the errors still occur.

    Has anyone experienced anything similar, and/or know of any fixes to this behavior? The code is working as expected if clients only ever connect once, but if they exit/crash and reconnect, error start occurring.
     
  2. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    450
    @Neodamus I just dealt with the same issue.

    When a client connects the new Network Connection Entity is assigned a NetworkId. This NetworkId is used for all client-created-predicted-entities through the value of NetworkId in GhostOwnerComponent.

    When a client disconnects, the server will wait ~30 seconds for a snapshot, at which point if there is no snapshot NetCode automatically adds a "NetworkStreamDisconnected" component onto the Server's NCE for that client.

    What you need to do is "clean up" when a client disconnects. This is because the same NetworkId will be re-used by the server for a new client. Any client-spawned entities need to be found and destroyed. If it is a player object, this means the server must reset the NetworkConnectionEntity commandTarget component. Or else the Server will take the new commands from the connected client and apply them to entities from the disconnected client.

    Here is my DisconnectSystem I use to check for disconnected entities. If I find a disconnected NCE, i save the NetworkId. I then have a .ForEach that checks GhostOwnerComponents to see if there are any entities that match this NetworkId and I add a "DestroyTag".

    Code (CSharp):
    1. [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
    2. [UpdateAfter(typeof(TriggerEventConversionSystem))]
    3. public class DisconnectSystem : SystemBase
    4. {
    5.     private EndFixedStepSimulationEntityCommandBufferSystem m_CommandBufferSystem;
    6.     protected override void OnCreate()
    7.     {
    8.         m_CommandBufferSystem = World.GetOrCreateSystem<EndFixedStepSimulationEntityCommandBufferSystem>();
    9.  
    10.     }
    11.     protected override void OnUpdate()
    12.     {
    13.         var commandBuffer = m_CommandBufferSystem.CreateCommandBuffer();
    14.         var commandTargetFromEntity = GetComponentDataFromEntity<CommandTargetComponent>(true);
    15.         int disconnectId = 0;
    16.         Entities
    17.         .WithAll<NetworkStreamDisconnected>()
    18.         .ForEach((in NetworkIdComponent networkId) => {
    19.             disconnectId = networkId.Value;
    20.  
    21.         }).Run();
    22.        
    23.         Entities
    24.         .WithAll<PlayerTag>()
    25.         .ForEach((Entity entity, in GhostOwnerComponent ghostOwner) => {
    26.             if (ghostOwner.NetworkId == disconnectId)
    27.             {
    28.                 commandBuffer.AddComponent<DestroyTag>(entity);
    29.             }
    30.         }).Schedule();
    31.         m_CommandBufferSystem.AddJobHandleForProducer(Dependency);
    32.     }
    33. }
    Here is my PlayerDestructionSystem job for cleaning the CommandTargetComponent on the server:

    Code (CSharp):
    1.         var deletePlayerJob = Entities
    2.         .WithNativeDisableParallelForRestriction(playerScoresNative)
    3.         .WithNativeDisableParallelForRestriction(commandTargetFromEntity)
    4.         .WithReadOnly(ghostOwnerFromEntity)
    5.         .WithAll<DestroyTag, PlayerTag>()
    6.         .ForEach((Entity entity, int nativeThreadIndex, in PlayerEntityComponent playerEntity) =>
    7.         {        
    8.             // Reset the CommandTargetComponent on the Network Connection Entity to the player
    9.             var state = commandTargetFromEntity[playerEntity.PlayerEntity];
    10.             state.targetEntity = Entity.Null;
    11.             commandTargetFromEntity[playerEntity.PlayerEntity] = state;
    12.             commandBuffer.DestroyEntity(nativeThreadIndex, entity);
    13.  
    14.             // we are also going to reset the player current score to 0
    15.             var playerNetworkId = ghostOwnerFromEntity[entity].NetworkId;
    16.             for (int j = 0; j < playerScoresNative.Length; j++)
    17.             {
    18.                 if(playerScoresNative[j].networkId == playerNetworkId)
    19.                 {
    20.                     var newPlayerScore = new PlayerScore{
    21.                         networkId = playerScoresNative[j].networkId,
    22.                         playerName = playerScoresNative[j].playerName,
    23.                         currentScore = 0,
    24.                         highScore = playerScoresNative[j].highScore,
    25.                         };
    26.                     playerScoresNative[j] = newPlayerScore;
    27.                 }
    28.             }
    29.  
    30.         }).ScheduleParallel(JobHandle.CombineDependencies(Dependency, playerScoreDep));
     
    Krooq likes this.
  3. Neodamus

    Neodamus

    Joined:
    Jan 12, 2018
    Posts:
    14
    Thank you for taking the time to respond @adammpolak

    I reviewed your solution, and I believe I do essentially the same thing you're doing and destroy the player ghost entity on disconnect. I didn't ever try setting the CommandTarget to Entity.Null on the connection entity, but I tried this now, and it did not help the situation for me.

    You're saying you had this RPC connection error before, but were able to resolve it with above code? I cannot see any other differences between what I'm doing and what you've shown here, so possibly I'm just missing it here, or there's a difference outside of this that took care of it?

    I did do some more testing and discovered now that the error only is occurring for me when I am trying to reconnect to the server if both server and client are on the same machine. If I have the server on one machine, and client on another, then I get no errors without any changes in my code.

    Ultimately, server and client on different machines will be the actual use case for me in the end, so this error only appears to be a testing issue at this point.

    In any case I've submitted a bug report and will try to remember to update this when Unity responds there.
     
    adammpolak likes this.