// ---------------------------------------------------------------------------- // // Loadbalancing Framework for Photon - Copyright (C) 2018 Exit Games GmbH // // // If the game logic does not call Service() for whatever reason, this keeps the connection. // // developer@photonengine.com // ---------------------------------------------------------------------------- #if UNITY_4_7 || UNITY_5 || UNITY_5_3_OR_NEWER #define SUPPORTED_UNITY #endif namespace Photon.Realtime { using System; using System.Threading; using System.Diagnostics; using SupportClass = ExitGames.Client.Photon.SupportClass; #if SUPPORTED_UNITY using UnityEngine; #endif #if SUPPORTED_UNITY public class ConnectionHandler : MonoBehaviour #else public class ConnectionHandler #endif { /// /// Photon client to log information and statistics from. /// public LoadBalancingClient Client { get; set; } /// Option to let the fallback thread call Disconnect after the KeepAliveInBackground time. Default: false. /// /// If set to true, the thread will disconnect the client regularly, should the client not call SendOutgoingCommands / Service. /// This may happen due to an app being in background (and not getting a lot of CPU time) or when loading assets. /// /// If false, a regular timeout time will have to pass (on top) to time out the client. /// public bool DisconnectAfterKeepAlive = false; /// Defines for how long the Fallback Thread should keep the connection, before it may time out as usual. /// We want to the Client to keep it's connection when an app is in the background (and doesn't call Update / Service Clients should not keep their connection indefinitely in the background, so after some milliseconds, the Fallback Thread should stop keeping it up. public int KeepAliveInBackground = 60000; /// Counts how often the Fallback Thread called SendAcksOnly, which is purely of interest to monitor if the game logic called SendOutgoingCommands as intended. public int CountSendAcksOnly { get; private set; } /// True if a fallback thread is running. Will call the client's SendAcksOnly() method to keep the connection up. public bool FallbackThreadRunning { get; private set; } /// Keeps the ConnectionHandler, even if a new scene gets loaded. public bool ApplyDontDestroyOnLoad = true; /// Indicates that the app is closing. Set in OnApplicationQuit(). [NonSerialized] public static bool AppQuits; /// Indicates that the (Unity) app is Paused. This means the main thread is not running. [NonSerialized] public static bool AppPause; /// Indicates that the app was paused within the last 5 seconds. [NonSerialized] public static bool AppPauseRecent; /// Indicates that the app is not in focus. [NonSerialized] public static bool AppOutOfFocus; /// Indicates that the app was out of focus within the last 5 seconds. [NonSerialized] public static bool AppOutOfFocusRecent; private bool didSendAcks; private readonly Stopwatch backgroundStopwatch = new Stopwatch(); private Timer stateTimer; #if SUPPORTED_UNITY #if UNITY_2019_4_OR_NEWER /// /// Resets statics for Domain Reload /// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void StaticReset() { AppQuits = false; AppPause = false; AppPauseRecent = false; AppOutOfFocus = false; AppOutOfFocusRecent = false; } #endif /// protected virtual void Awake() { if (this.ApplyDontDestroyOnLoad) { DontDestroyOnLoad(this.gameObject); } } /// Called by Unity when the application gets closed. Disconnects if OnApplicationQuit() was called before. protected virtual void OnDisable() { this.StopFallbackSendAckThread(); if (AppQuits) { if (this.Client != null && this.Client.IsConnected) { this.Client.Disconnect(DisconnectCause.ApplicationQuit); this.Client.LoadBalancingPeer.StopThread(); } SupportClass.StopAllBackgroundCalls(); } } /// Called by Unity when the application gets closed. The UnityEngine will also call OnDisable, which disconnects. public void OnApplicationQuit() { AppQuits = true; } /// Called by Unity when the application gets paused or resumed. public void OnApplicationPause(bool pause) { AppPause = pause; if (pause) { AppPauseRecent = true; this.CancelInvoke(nameof(this.ResetAppPauseRecent)); } else { Invoke(nameof(this.ResetAppPauseRecent), 5f); } } private void ResetAppPauseRecent() { AppPauseRecent = false; } /// Called by Unity when the application changes focus. public void OnApplicationFocus(bool focus) { AppOutOfFocus = !focus; if (!focus) { AppOutOfFocusRecent = true; this.CancelInvoke(nameof(this.ResetAppOutOfFocusRecent)); } else { this.Invoke(nameof(this.ResetAppOutOfFocusRecent), 5f); } } private void ResetAppOutOfFocusRecent() { AppOutOfFocusRecent = false; } #endif /// /// When run in Unity, this returns Application.internetReachability != NetworkReachability.NotReachable. /// /// Application.internetReachability != NetworkReachability.NotReachable public static bool IsNetworkReachableUnity() { #if SUPPORTED_UNITY return Application.internetReachability != NetworkReachability.NotReachable; #else return true; #endif } /// Starts periodic calls of RealtimeFallbackThread. public void StartFallbackSendAckThread() { #if UNITY_WEBGL if (!this.FallbackThreadRunning) this.InvokeRepeating(nameof(this.RealtimeFallbackInvoke), 0.05f, 0.05f); #else if (this.stateTimer != null) { return; } stateTimer = new Timer(this.RealtimeFallback, null, 50, 50); #endif this.FallbackThreadRunning = true; } /// Stops the periodic calls of RealtimeFallbackThread. public void StopFallbackSendAckThread() { #if UNITY_WEBGL if (this.FallbackThreadRunning) this.CancelInvoke(nameof(this.RealtimeFallbackInvoke)); #else if (this.stateTimer != null) { this.stateTimer.Dispose(); this.stateTimer = null; } #endif this.FallbackThreadRunning = false; } /// Used in WebGL builds which can't call RealtimeFallback(object state = null) with the state context parameter. public void RealtimeFallbackInvoke() { this.RealtimeFallback(); } /// A thread which runs independently of the Update() calls. Keeps connections online while loading or in background. See . public void RealtimeFallback(object state = null) { if (this.Client == null) { return; } if (this.Client.IsConnected && this.Client.LoadBalancingPeer.ConnectionTime - this.Client.LoadBalancingPeer.LastSendOutgoingTime > 100) { if (!this.didSendAcks) { this.backgroundStopwatch.Reset(); this.backgroundStopwatch.Start(); } // check if the client should disconnect after some seconds in background if (this.backgroundStopwatch.ElapsedMilliseconds > this.KeepAliveInBackground) { if (this.DisconnectAfterKeepAlive) { this.Client.Disconnect(); } return; } this.didSendAcks = true; this.CountSendAcksOnly++; this.Client.LoadBalancingPeer.SendAcksOnly(); } else { // not connected or the LastSendOutgoingTimestamp was below the threshold if (this.backgroundStopwatch.IsRunning) { this.backgroundStopwatch.Reset(); } this.didSendAcks = false; } } } }