How to reconnect and rejoin when mobile app goes to background. Best practice?

Hi guys, we are using PUN for almost a year and are very happy with it. The only problem we have is handling the case where the mobile app is going to the background and to trigger a successful reconnect and rejoin. We can’t seem to find some best practices or code snippets so we cooked up something ourselves. The following implementation seems to work most of the times but it’s not 100% reliable. We still have cases where the app is not successfully reconnecting and rejoining.

We hope you could take the time and spot some problems with this script. We also hope you could provide us with a best practice for reconnecting and rejoining.

https://pastebin.com/X83mubxf

FYI:
- The room we create has a PlayerTTL of 300000.
- We are only targeting iOS.
- PUN v2.7
- Unity 2018.3.5

Thanks!

Comments

  • JohnTube
    JohnTube ✭✭✭✭✭
    edited May 2019
    Hi @Burny123,

    Try this UtilityScript.
    Do you have the same issue described here?
  • @JohnTube Thanks for helping out. The link you provided leads to an empty page on hastebin, I guess something went wrong?

    The issue from the link is not the same. We can successfully reconnect and rejoin most of the times, but not always.. It seems random and is hard for us to debug.

    I love to see the UtilityScript.

    Thanks!
  • JohnTube
    JohnTube ✭✭✭✭✭
    Hey @Burny123,

    I have checked again and I think the link works fine now.
    Maybe there was an issue yesterday.
    Anyways find the code also here:
    using System;
    using Photon.Realtime;
    using UnityEngine;
    
    namespace Photon.Pun.UtilityScripts
    {
        /// <summary>
        /// Unexpected disconnects recovery
        /// </summary>
        public class DisconnectsRecovery : MonoBehaviourPunCallbacks
        {
            [Tooltip("Whether or not attempt a rejoin without doing any checks.")]
            [SerializeField]
            private bool skipRejoinChecks;
            [Tooltip("Whether or not realtime webhooks are configured with persistence enabled")]
            [SerializeField]
            private bool persistenceEnabled;
    
            private bool rejoinCalled;
    
            private int minTimeRequiredToRejoin = 0; // TODO: set dynamically based on PhotonNetwork.NetworkingClient.LoadBalancingPeer.RoundTripTime
    
            private DisconnectCause lastDisconnectCause;
            private bool wasInRoom;
    
            private bool reconnectCalled;
    
            public override void OnEnable()
            {
                base.OnEnable();
                PhotonNetwork.NetworkingClient.StateChanged += this.OnStateChanged;
            }
    
            public override void OnDisable()
            {
                base.OnDisable();
                PhotonNetwork.NetworkingClient.StateChanged -= this.OnStateChanged;
            }
    
            private void OnStateChanged(ClientState fromState, ClientState toState)
            {
                if (toState == ClientState.Disconnected)
                {
    
                    Debug.LogFormat("OnStateChanged from {0} to {1}, PeerState={2}", fromState, toState, 
                        PhotonNetwork.NetworkingClient.LoadBalancingPeer.PeerState);
                    this.HandleDisconnect();
                }
            }
    
            public override void OnDisconnected(DisconnectCause cause)
            {
                Debug.LogFormat("OnDisconnected(cause={0}) ClientState={1} PeerState={2}", 
                    cause, 
                    PhotonNetwork.NetworkingClient.State, 
                    PhotonNetwork.NetworkingClient.LoadBalancingPeer.PeerState);
                if (rejoinCalled)
                {
                    Debug.LogError("Rejoin failed, client disconnected");
                    rejoinCalled = false;
                    return;
                }
    
                if (reconnectCalled)
                {
                    Debug.LogError("Reconnect failed, client disconnected");
                    reconnectCalled = false;
                    return;
                }
                lastDisconnectCause = cause;
                wasInRoom = PhotonNetwork.CurrentRoom != null;
                if (PhotonNetwork.NetworkingClient.State == ClientState.Disconnected)
                {
                    this.HandleDisconnect();
                }
            }
    
            private void HandleDisconnect()
            {
                switch (lastDisconnectCause)
                {
                    case DisconnectCause.Exception:
                    case DisconnectCause.ServerTimeout:
                    case DisconnectCause.ClientTimeout:
                    case DisconnectCause.DisconnectByServerLogic:
                    case DisconnectCause.AuthenticationTicketExpired:
                    case DisconnectCause.DisconnectByServerReasonUnknown:
                        if (wasInRoom)
                        {
                            this.CheckAndRejoin();
                        }
                        else
                        {
                            Debug.Log("PhotonNetwork.Reconnect called");
                            reconnectCalled = PhotonNetwork.Reconnect();
                        }
                        break;
                    case DisconnectCause.OperationNotAllowedInCurrentState:
                    case DisconnectCause.CustomAuthenticationFailed:
                    case DisconnectCause.DisconnectByClientLogic:
                    case DisconnectCause.InvalidAuthentication:
                    case DisconnectCause.ExceptionOnConnect:
                    case DisconnectCause.MaxCcuReached:
                    case DisconnectCause.InvalidRegion:
                    case DisconnectCause.None:
                        break;
                    default:
                        throw new ArgumentOutOfRangeException("cause", lastDisconnectCause, null);
                }
                lastDisconnectCause = DisconnectCause.None;
                wasInRoom = false;
            }
    
            public override void OnJoinRoomFailed(short returnCode, string message)
            {
                if (!rejoinCalled)
                {
                    return;
                }
                rejoinCalled = false;
                Debug.LogErrorFormat("Quick rejoin failed with error code: {0} & error message: {1}", returnCode, message);
            }
    
            public override void OnJoinedRoom()
            {
                if (rejoinCalled)
                {
                    Debug.Log("Rejoin successful");
                    rejoinCalled = false;
                }
            }
    
            private void CheckAndRejoin()
            {
                if (skipRejoinChecks)
                {
                    Debug.Log("PhotonNetwork.ReconnectAndRejoin called");
                    rejoinCalled = PhotonNetwork.ReconnectAndRejoin();
                }
                else
                {
                    bool wasLastActivePlayer = true;
                    if (!persistenceEnabled)
                    {
                        for (int i = 0; i < PhotonNetwork.PlayerListOthers.Length; i++)
                        {
                            if (!PhotonNetwork.PlayerListOthers[i].IsInactive)
                            {
                                wasLastActivePlayer = false;
                                break;
                            }
                        }
                    }
                    if ((PhotonNetwork.CurrentRoom.PlayerTtl < 0 || PhotonNetwork.CurrentRoom.PlayerTtl > minTimeRequiredToRejoin) // PlayerTTL checks
                      && (!wasLastActivePlayer || PhotonNetwork.CurrentRoom.EmptyRoomTtl > minTimeRequiredToRejoin || persistenceEnabled)) // EmptyRoomTTL checks
                    {
                        Debug.Log("PhotonNetwork.ReconnectAndRejoin called");
                        rejoinCalled = PhotonNetwork.ReconnectAndRejoin();
                    }
                    else
                    {
                        Debug.Log("PhotonNetwork.ReconnectAndRejoin not called, PhotonNetwork.Reconnect is called instead.");
                        reconnectCalled = PhotonNetwork.Reconnect();
                    }
                }
            }
    
            public override void OnConnectedToMaster()
            {
                if (reconnectCalled)
                {
                    Debug.Log("Reconnect successful");
                    reconnectCalled = false;
                }
            }
        }
    }
  • @JohnTube The hastebin still shows an empty page for me. But I'm going to try out the snippet you provided.

    I'll report the results, thanks!
  • @JohnTube I tried the script but it not works on my iPad. The main issue seems to be:

    ’Reconnect() failed. Can only connect while in state 'Disconnected'. Current state: Connected’

    Full log: https://pastebin.com/xZCZJibz

    We had this issue too and added a coroutine which waits for the player to be disconnected fully.

    What do you suggest to do from here?

    Thanks!
  • JohnTube
    JohnTube ✭✭✭✭✭
    Hi @Burny123,

    I can only think of adding a coroutine to detect when a client + peer are fully properly and disconnected before attempting a reconnect or a rejoin. So it's like your solution, however, you said;
    implementation seems to work most of the times but it’s not 100% reliable. We still have cases where the app is not successfully reconnecting and rejoining.
    can we try to find out how to reproduce those cases? Let's try to narrow it down.
  • @JohnTube Sure thing, one of these days I'm going to create a full test environment to try and narrow it down.

    To be continued, thanks!
  • JohnTube
    JohnTube ✭✭✭✭✭
    Hi @Burny123,

    In order to prioritize this you can send an email to deverloper@photonengine.com with all the delails.
    Thanks.
  • ok,

    just for the record should other face this issue while we discuss this internally:

    when you get a disconnect call back, raise a boolean provate variable, and during an update call, try to reconnect if that bool is true, this is the shortest way to reconnect and avoid the issue explained above.

    It's going to be tricky, because it could be a case where it's a race condition situation, if we change the order of property sets and callbacks, it could break other projects.

    Bye,

    Jean
  • @jeanfabre

    Is the Utility Script you provided still valid, or does it need updating?
  • Hi,

    yes, it needs updating. here's the code featuring the update for the reconnecting to occur,

    https://pastebin.com/wk39tgzA

    Let me know how it goes.

    Bye,

    Jean


  • JohnTube
    JohnTube ✭✭✭✭✭
    edited June 2019
    I want to clarify/rectify something:
    the script shared by @jeanfabre has some extra lines we used for testing internally:

    remove them from OnEnable:

    before:
            public override void OnEnable()
            {
                base.OnEnable();
                PhotonNetwork.NetworkingClient.StateChanged += this.OnStateChanged;
                PhotonNetwork.KeepAliveInBackground = 0f;
                PhotonNetwork.PhotonServerSettings.RunInBackground = false;
                Application.runInBackground = false;
            }
    after:
            public override void OnEnable()
            {
                base.OnEnable();
                PhotonNetwork.NetworkingClient.StateChanged += this.OnStateChanged;
            }
    @jeanfabre
    so if I understood correctly, we need to wait 1 frame before trying to reconnect and rejoin?
    we could do this using a coroutine and WaitForEndOfFrame?
    hmm will test this.
  • Hi,

    yes. That's what I am witnessing here. We are currently investigating as to what exactly happens during that frame and if we can remove this and be able to reconnect straight from a disconnect callback.

    Bye,

    Jean
  • Hello @JohnTube, i'm not much into coding and sorry for my english grammer if any mistakes, actually even i'm running same problem, but i'm using pun1 classic, im trying to reconnect when one of host or client mobile disconnected from internet and then reconnected, then i couldnt able to run the game. but in the editor even after reconnecting internet also the game works, but not in android mobile. can you please help me out as i'm in very urgent need...
  • Nithish wrote: »
    Hello @JohnTube, i'm not much into coding and sorry for my english grammer if any mistakes, actually even i'm running same problem, but i'm using pun1 classic, im trying to reconnect when one of host or client mobile disconnected from internet and then reconnected, then i couldnt able to run the game. but in the editor even after reconnecting internet also the game works, but not in android mobile. can you please help me out as i'm in very urgent need...

    anyone there to help......
  • Jackinabox
    edited September 2020
    Hi @Nitish,

    Hope this is not too late for you. I came across the same issue a few days ago and manage to solve it using jeanfabre's code posted above.

    In gist, the blackscreen on android arises when your client disconnects from the server, which causes the server to despawn/destroy the game character of that client. Upon reconnect, whether via detection in OnApplicationPause or from the OnDisconnectFromPhoton callback, you may get an issue as stated above "Reconnect() failed. Can only connect while in state 'Disconnected'. Current state: Connected" (or in my case the current state is disconnecting), which cause the reconnect call to fail.

    The trick is to call reconnect in the next frame as your callbacks. This is achieved by setting a flag in your server logic, and checking that flag in the update call. Once that flag has been set, Update function will take care of reconnect logic in the next frame, therefore circumnavigate the issue.

    I'm not sure whether there is a conclusion to the official investigation on this issue since August of 2019, but as of now, I can confirm I'm still running into it.

    Hope it helps