Search Unity

Help Wanted How does the host close connections properly and send everyone to main menu?

Discussion in 'Unity Transport' started by kanpot2002, Oct 16, 2021.

  1. kanpot2002

    kanpot2002

    Joined:
    May 10, 2012
    Posts:
    8
    Currently I'm having a class called TransportConnectionManager which is in DontDestroyOnLoad (to make it available in both MainMenuScene and GamePlayScene) and it manages both the matchmaking via local LAN IP and sending and receiving messages.

    Only the host can press "Main Menu" button. Then the host will send the "MainMenu" message to everyone. Please note that I also wait for BeginSend result and ReliableSequencedPipelineStage result to be 0 before the host itself call GoToMainMenuOnDevice();

    Code (CSharp):
    1. //TransportConnectionManager.cs
    2.  
    3. private Queue<HostCommand> _hostCommands = null;
    4. private NativeList<NetworkConnection> m_Connections;
    5. private NetworkPipeline m_Pipeline = m_Driver.CreatePipeline(typeof(ReliableSequencedPipelineStage));
    6.  
    7. // Inside Host's Update function
    8. bool goToMainMenuAfterSend = false;
    9. bool waitUntilNextUpdate = false;
    10. while (_hostCommands.Count > 0 && waitUntilNextUpdate == false)
    11. {
    12.     HostCommand hostCmd = _hostCommands.Peek();
    13.     if ((CommandType)hostCmd.CmdType == CommandType.MainMenu)
    14.         goToMainMenuAfterSend = true;
    15.  
    16.     // Note that the messages are batched, so the first 4 bytes of a message always contains the length of the message. After the entire message is received it is put together and handled.
    17.     byte[] cmdBytes = hostCmd.PackToBytes();
    18.     byte[] lengthAndCmdBytes = new byte[4 + cmdBytes.Length];
    19.     Array.Copy(BitConverter.GetBytes(cmdBytes.Length), 0, lengthAndCmdBytes, 0, 4);
    20.     Array.Copy(cmdBytes, 0, lengthAndCmdBytes, 4, cmdBytes.Length);
    21.  
    22.     NativeArray<byte> bytes = new NativeArray<byte>(lengthAndCmdBytes, Allocator.Temp);
    23.     // Get a reference to the internal state or shared context of the reliability
    24.     var reliableStageId = NetworkPipelineStageCollection.GetStageId(typeof(ReliableSequencedPipelineStage));
    25.     m_Driver.GetPipelineBuffers(m_Pipeline, reliableStageId, hostCmd.DataDevice.GetNetworkConnection(), out var tmpReceiveBuffer, out var tmpSendBuffer, out NativeArray<byte> serverReliableBuffer);
    26.  
    27.     unsafe {
    28.         var serverReliableCtx = (ReliableUtility.SharedContext*) serverReliableBuffer.GetUnsafePtr();
    29.         int sendResult = m_Driver.BeginSend(m_Pipeline, hostCmd.DataDevice.GetNetworkConnection(), out DataStreamWriter writer);
    30.         if (sendResult == 0)
    31.         {
    32.             writer.WriteBytes(bytes);
    33.             m_Driver.EndSend(writer);
    34.    
    35.             if (serverReliableCtx->errorCode != 0)
    36.             {
    37.                 waitUntilNextUpdate = true;
    38.                 goToMainMenuAfterSend = false;
    39.                 Debug.LogWarning("Failed to send with reliability : " + serverReliableCtx->errorCode);
    40.                 // Failed to send with reliability, error code will be ReliableUtility.ErrorCodes.OutgoingQueueIsFull if no buffer space is left to store the packet
    41.             } else {
    42.                 _lastMsgTimeToClient[hostCmd.DataDevice.GetNetworkConnection()] = Time.time;
    43.             }
    44.         }
    45.         else
    46.         {
    47.             waitUntilNextUpdate = true;
    48.             goToMainMenuAfterSend = false;
    49.         }
    50.     }
    51.  
    52.  
    53.     if (waitUntilNextUpdate == false)
    54.     {
    55.         _hostCommands.Dequeue();
    56.     }
    57. }
    58.  
    59. if (goToMainMenuAfterSend == true)
    60. {
    61.     GoToMainMenuOnDevice();
    62. }
    When the client receive the "MainMenu" command, it will call GoToMainMenuOnDevice(); function (Both the host and the client call this same function)

    Code (CSharp):
    1. public void GoToMainMenuOnDevice()
    2. {
    3.     // Miscs cleanup
    4.     ................................
    5.  
    6.     // I WANT TO CLOSE THE CONNECTIONS HERE BUT IT WON'T WORK
    7.     SceneManager.LoadScene("MainMenuScene");
    8. }
    9.  
    Since the script is in DontDestroyOnLoad, if I don't close the connection, it will still exist in main menu. All the remaining messages (if any) will still be sent. However, I cannot close the connection at the above commented spot :
    • If I call m_Connections.Dispose(); -> it will throw error about connection already closed
    • If I call Destroy(this.gameObject); -> Only the host can go back to MainMenuScene and the clients stuck at GamePlayScene
    • If I leave everything as it is -> I cannot create a new connection. The hack I'm doing is to try-catch delete the connection just before creating a new one.

    So, what is a proper way to send everyone back to MainMenuScene and close all the connections?
     
    Last edited: Oct 16, 2021
  2. SimonVigMillard

    SimonVigMillard

    Joined:
    Jul 25, 2018
    Posts:
    5
    Hm... I would like to know this too, if anyone has any ideas? o_O
     
  3. simon-lemay-unity

    simon-lemay-unity

    Unity Technologies

    Joined:
    Jul 19, 2021
    Posts:
    16
    Not sure I fully understand the problem here, but
    NetworkDriver
    has a
    Disconnect
    method to cleanly close a connection.
    NetworkConnection
    s also have
    Close
    and
    Disconnect
    methods to do the same.

    Also keep in mind that even if there's no error, the message will only be sent on the reliable pipeline on the next driver update (you can also force a send with
    ScheduleFlushSend
    ). In your code, it's likely the very first send "succeeds" without any error code, which immediately leads to
    GoToMainMenuOnDevice
    being called. If connections are closed there, they'll be closed before the message has actually been sent.
     
  4. SimonVigMillard

    SimonVigMillard

    Joined:
    Jul 25, 2018
    Posts:
    5
    So kanpot2002 needs to call
    GoToMainMenuOnDevice
    after next driver update? Is there a way to trigger an event after the next driver update so he can call it there? Or is there a way to check if a message has been sent (completely) yet?
    How does
    ScheduleFlushSend
    work? Does it force the message to be sent immediately (without waiting for next driver update)?
     
  5. simon-lemay-unity

    simon-lemay-unity

    Unity Technologies

    Joined:
    Jul 19, 2021
    Posts:
    16
    Yes and no. Calling
    GoToMainMenuOnDevice
    after the next driver update (or flushed send) will ensure that the message is sent, but not necessarily received by the remote peer. Also, on a lossy network it might take multiple updates since the message might need to be resent if lost.

    No. Although you can always schedule an update whenever you want. Driver updates don't have to align with a behaviour's
    Update
    method. There can be multiple updates in a frame too, that's fine. So in OP's code, they could just schedule an update (and complete it) right after the
    EndSend
    call and call
    GoToMainMenuOnDevice
    afterwards like they are doing. (Although
    ScheduleFlushSend
    would probably make more sense here since full driver updates require you to handle the generated events before the next update.)

    There's no good clean way to know if a message has been sent and received by the other peer. I think you might be able to somewhat hack it though, if using a reliable pipeline. Right after the
    EndSend
    call, you could check
    serverReliableCtx->SentPackets.Sequence
    to figure out what the sequence number is of the packet you just sent, and then wait until
    serverReliableCtx->ReceivedPackets.Acked
    is greater than or equal to that to know that it was delivered. Fair warning though: I've never tried it and have no idea if it actually works. And even if it works, you probably don't want to just stall there until the packet is delivered, as that might take a while (at least a full roundtrip to the remote peer).

    Yes, it forces messages to be sent immediately (if you
    Complete()
    the job handle it returns). A driver update (
    ScheduleUpdate
    ) consists of a couple things: receiving data, sending data, and internal housekeeping.
    ScheduleFlushSend
    is just the sending data part of that.
     
    SimonVigMillard likes this.
  6. kanpot2002

    kanpot2002

    Joined:
    May 10, 2012
    Posts:
    8
    Thank you Simon Lemay for your thorough answer. But how do you suggest me to implement, especially on a lossy network? The scenario is:
    1. The host press Main Menu button.
    2. All the client should leave the game and load Main Menu scene.
    3. The host also have to load Main Menu scene, but need to make sure that no client is still stuck at Game Play scene.
    4. Close/destroy all connections as soon as possible.
     
  7. simon-lemay-unity

    simon-lemay-unity

    Unity Technologies

    Joined:
    Jul 19, 2021
    Posts:
    16
    If you need this to be reliable, a solution would be for clients to send a message reporting that they are back at the main menu:
    1. Host presses main menu button.
    2. Host sends a reliable message to all clients telling them to go to the main menu.
    3. Upon receiving that message, client goes to the main menu and sends a reliable message to the host indicating so.
    4. When host gets the confirmation message from a client, it disconnects it.
    If you don't care too much about reliability (or can live with a short inactivity timeout), you can also just disconnect all clients from the host (using the
    Disconnect
    method of
    NetworkDriver
    or
    NetworkConnection
    ). This will generate a
    Disconnect
    event on the clients, which they can use to go back to the main menu.

    I will also note that a lot of this could be simplified by using a higher-level networking library (like Netcode for GameObject, which can use Unity Transport under the hood). You'd get access to RPCs and network variables to simplify synchronization between host and clients. (Although I assume you have your own reasons for building directly on top of the transport library.)
     
    kanpot2002 and SimonVigMillard like this.
unityunity