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

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();

//TransportConnectionManager.cs

private Queue<HostCommand> _hostCommands = null;
private NativeList<NetworkConnection> m_Connections;
private NetworkPipeline m_Pipeline = m_Driver.CreatePipeline(typeof(ReliableSequencedPipelineStage));

// Inside Host's Update function
bool goToMainMenuAfterSend = false;
bool waitUntilNextUpdate = false;
while (_hostCommands.Count > 0 && waitUntilNextUpdate == false)
{
    HostCommand hostCmd = _hostCommands.Peek();
    if ((CommandType)hostCmd.CmdType == CommandType.MainMenu)
        goToMainMenuAfterSend = true;

    // 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.
    byte[] cmdBytes = hostCmd.PackToBytes();
    byte[] lengthAndCmdBytes = new byte[4 + cmdBytes.Length];
    Array.Copy(BitConverter.GetBytes(cmdBytes.Length), 0, lengthAndCmdBytes, 0, 4);
    Array.Copy(cmdBytes, 0, lengthAndCmdBytes, 4, cmdBytes.Length);

    NativeArray<byte> bytes = new NativeArray<byte>(lengthAndCmdBytes, Allocator.Temp);
    // Get a reference to the internal state or shared context of the reliability
    var reliableStageId = NetworkPipelineStageCollection.GetStageId(typeof(ReliableSequencedPipelineStage));
    m_Driver.GetPipelineBuffers(m_Pipeline, reliableStageId, hostCmd.DataDevice.GetNetworkConnection(), out var tmpReceiveBuffer, out var tmpSendBuffer, out NativeArray<byte> serverReliableBuffer);
 
    unsafe {
        var serverReliableCtx = (ReliableUtility.SharedContext*) serverReliableBuffer.GetUnsafePtr();
        int sendResult = m_Driver.BeginSend(m_Pipeline, hostCmd.DataDevice.GetNetworkConnection(), out DataStreamWriter writer);
        if (sendResult == 0)
        {
            writer.WriteBytes(bytes);
            m_Driver.EndSend(writer);
    
            if (serverReliableCtx->errorCode != 0)
            {
                waitUntilNextUpdate = true;
                goToMainMenuAfterSend = false;
                Debug.LogWarning("Failed to send with reliability : " + serverReliableCtx->errorCode);
                // Failed to send with reliability, error code will be ReliableUtility.ErrorCodes.OutgoingQueueIsFull if no buffer space is left to store the packet
            } else {
                _lastMsgTimeToClient[hostCmd.DataDevice.GetNetworkConnection()] = Time.time;
            }
        }
        else
        {
            waitUntilNextUpdate = true;
            goToMainMenuAfterSend = false;
        }
    }
 

    if (waitUntilNextUpdate == false)
    {
        _hostCommands.Dequeue();
    }
}

if (goToMainMenuAfterSend == true)
{
    GoToMainMenuOnDevice();
}

When the client receive the “MainMenu” command, it will call GoToMainMenuOnDevice(); function (Both the host and the client call this same function)

public void GoToMainMenuOnDevice()
{
    // Miscs cleanup
    ................................

    // I WANT TO CLOSE THE CONNECTIONS HERE BUT IT WON'T WORK
    SceneManager.LoadScene("MainMenuScene");
}

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?

Hm… I would like to know this too, if anyone has any ideas? :eyes:

Not sure I fully understand the problem here, but NetworkDriver has a Disconnect method to cleanly close a connection. NetworkConnections 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.

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)?

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.

1 Like

Thank you Simon Lemay for your thorough answer. But how do you suggest me to implement, especially on a lossy network? The scenario is:

  • The host press Main Menu button.
  • All the client should leave the game and load Main Menu scene.
  • The host also have to load Main Menu scene, but need to make sure that no client is still stuck at Game Play scene.
  • Close/destroy all connections as soon as possible.

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:

  • Host presses main menu button.
  • Host sends a reliable message to all clients telling them to go to the main menu.
  • Upon receiving that message, client goes to the main menu and sends a reliable message to the host indicating so.
  • 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.)

2 Likes