// Copyright (c) 2015 - 2023 Doozy Entertainment. All Rights Reserved. // This code can only be used under the standard Unity Asset Store End User License Agreement // A Copy of the EULA APPENDIX 1 is available at http://unity3d.com/company/legal/as_terms using System.Collections; using System.Linq; using Doozy.Runtime.Common; using Doozy.Runtime.Common.Attributes; using Doozy.Runtime.Common.Utils; using Doozy.Runtime.Signals; using Doozy.Runtime.UIManager.Components; using Doozy.Runtime.UIManager.ScriptableObjects; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.SceneManagement; #if INPUT_SYSTEM_PACKAGE using UnityEngine.InputSystem.UI; #endif // ReSharper disable MemberCanBePrivate.Global namespace Doozy.Runtime.UIManager.Input { /// /// The ‘Back’ Button functionality injects itself into the Input System and listens for the ‘Cancel’ action. /// It does that by automatically attaching a Input To Signal to the Event System. /// This is an automated system that is activated by any UI component in DoozyUI. /// [AddComponentMenu("Doozy/Input/Back Button")] [DisallowMultipleComponent] public class BackButton : SingletonBehaviour { #if UNITY_EDITOR [UnityEditor.MenuItem("GameObject/Doozy/Input/Back Button", false, 8)] private static void CreateComponent(UnityEditor.MenuCommand menuCommand) { GameObjectUtils.AddToScene("Back Button", false, true); } #endif #if LEGACY_INPUT_MANAGER public const KeyCode k_BackButtonKeyCode = KeyCode.Escape; #endif /// Default name of the virtual button that will be used for the 'Back' button, when LEGACY_INPUT_MANAGER is enabled public const string k_BackButtonVirtualButtonName = "Cancel"; /// Stream category name for the Back Button public const string k_StreamCategory = "Input"; /// /// Stream name for the 'Back' Button. /// Used by stream to listen for the Back Button. /// public const string k_StreamName = nameof(BackButton); /// /// Stream name for the 'Back' Button. /// Used by streamIgnoreDisabled to listen for the 'Back' Button, /// regardless if the 'Back' button functionality is enabled or not. /// public const string k_StreamNameIgnoreDisabled = k_StreamName + ".IgnoreDisabledState"; /// /// Stream name for the 'Back' Button. /// Used by streamOnEnabled to listen for when the 'Back' button functionality was enabled (from the disabled state) /// public const string k_StreamNameOnEnabled = k_StreamName + ".Enabled"; /// /// Stream name for the 'Back' Button. /// Used by streamOnDisabled to listen for when the 'Back' button functionality was disabled (from the enabled state) /// public const string k_StreamNameOnDisabled = k_StreamName + ".Disabled"; /// Default button name for the 'Back' Button public const string k_ButtonName = "Back"; //ToDo: maybe allow for different button names to be THE 'Back' button [ClearOnReload] private static SignalStream s_stream; [ClearOnReload] private static SignalStream s_streamIgnoreDisabled; [ClearOnReload] private static SignalStream s_streamOnEnabled; [ClearOnReload] private static SignalStream s_streamOnDisabled; /// /// Stream that sends signals when the 'Back' button is fired. /// This stream does not send signals when the 'Back' button is disabled. /// public static SignalStream stream => s_stream ??= SignalsService.GetStream(k_StreamCategory, k_StreamName); /// /// Stream that sends signals when the 'Back' button is fired. /// This stream sends signals regardless if the 'Back' button is disabled or not. /// public static SignalStream streamIgnoreDisabled => s_streamIgnoreDisabled ??= SignalsService.GetStream(k_StreamCategory, k_StreamNameIgnoreDisabled); /// /// Stream that sends signals when the 'Back' button functionality was enabled (from the disabled state) /// public static SignalStream streamOnEnabled => s_streamOnEnabled ??= SignalsService.GetStream(k_StreamCategory, k_StreamNameOnEnabled); /// /// Stream that sends signals when the 'Back' button functionality was disabled (from the enabled state) /// public static SignalStream streamOnDisabled => s_streamOnDisabled ??= SignalsService.GetStream(k_StreamCategory, k_StreamNameOnDisabled); [ClearOnReload] private static SignalReceiver inputStreamReceiver { get; set; } private static void ConnectToInputStream() { InputStream.Start(); InputStream.stream.ConnectReceiver(inputStreamReceiver); } private static void DisconnectFromInputStream() { InputStream.Stop(); InputStream.stream.DisconnectReceiver(inputStreamReceiver); } [ClearOnReload] private static SignalReceiver buttonStreamReceiver { get; set; } private static void ConnectToButtonStream() => UIButton.stream.ConnectReceiver(buttonStreamReceiver); private static void DisconnectFromButtonStream() => UIButton.stream.DisconnectReceiver(buttonStreamReceiver); /// Reference to the UIManager Input Settings public static UIManagerInputSettings inputSettings => UIManagerInputSettings.instance; /// True Multiplayer Mode is enabled public static bool multiplayerMode => inputSettings.multiplayerMode; private int m_BackButtonDisableLevel; //This is an additive bool so if == 0 --> false (the 'Back' button is NOT disabled) and if > 0 --> true (the 'Back' button is disabled). private double m_LastTimeBackButtonWasExecuted; //Internal variable used to keep track when the 'Back' button was executed the last time /// Cooldown after the 'Back' button was fired (to prevent spamming and accidental double execution) public static float cooldown => inputSettings.backButtonCooldown; /// True if the 'Back' button functionality is disabled public bool isDisabled { get { if (m_BackButtonDisableLevel < 0) m_BackButtonDisableLevel = 0; return m_BackButtonDisableLevel != 0; } } /// True if the 'Back' button functionality is enabled public bool isEnabled => !isDisabled; /// True if the 'Back' button can be triggered again public bool inCooldown => Time.realtimeSinceStartup - m_LastTimeBackButtonWasExecuted < cooldown; /// True if the 'Back' button functionality is enabled and is not in cooldown public bool canFire => isEnabled && !inCooldown; /// Flag marked as True if a InputToSignal set to listen for the 'Back' button exists in the scene public bool hasInput { get; private set; } // ReSharper disable once UnusedAutoPropertyAccessor.Local private bool initialized { get; set; } public static void Initialize() { _ = instance; } protected override void Awake() { base.Awake(); initialized = false; m_LastTimeBackButtonWasExecuted = Time.realtimeSinceStartup; SceneManager.sceneLoaded += OnSceneLoaded; } private void OnSceneLoaded(Scene scene, LoadSceneMode loadSceneMode) { CheckForInput(); } private IEnumerator Start() { yield return null; CheckForInput(); initialized = hasInput; inputStreamReceiver = new SignalReceiver().SetOnSignalCallback(signal => { if (!signal.hasValue) return; #if INPUT_SYSTEM_PACKAGE if (!(signal.valueAsObject is InputSignalData { inputActionName: UIInputActionName.Cancel } data)) return; Fire(data); #endif #if LEGACY_INPUT_MANAGER if (!(signal.valueAsObject is InputSignalData data)) return; switch (data.inputMode) { case LegacyInputMode.KeyCode: if (data.keyCode == BackButton.k_BackButtonKeyCode) Fire(data); break; case LegacyInputMode.VirtualButton: if (data.virtualButtonName == inputSettings.backButtonVirtualButtonName) Fire(data); break; case LegacyInputMode.None: break; default: throw new System.ArgumentOutOfRangeException(); } #endif }); ConnectToInputStream(); buttonStreamReceiver = new SignalReceiver().SetOnSignalCallback(signal => { if (!signal.hasValue) return; if (!(signal.valueAsObject is UIButtonSignalData data)) return; if (!data.buttonName.Equals(k_ButtonName)) return; #if INPUT_SYSTEM_PACKAGE Fire(new InputSignalData(UIInputActionName.Cancel, data.playerIndex)); #endif #if LEGACY_INPUT_MANAGER Fire(new InputSignalData(LegacyInputMode.KeyCode, BackButton.k_BackButtonKeyCode, BackButton.k_BackButtonVirtualButtonName, data.playerIndex)); #endif }); ConnectToButtonStream(); } protected override void OnDestroy() { base.OnDestroy(); DisconnectFromInputStream(); DisconnectFromButtonStream(); SceneManager.sceneLoaded -= OnSceneLoaded; } //ToDo: maybe -> add option to set default action name for 'Back' button (instead of Cancel) public void CheckForInput() { hasInput = false; if (EventSystem.current == null && !multiplayerMode) { Debug.LogWarning ( $"{nameof(EventSystem)}.current is null. " + $"Add it to the scene to fix this issue." ); return; } if (EventSystem.current != null) { AddInputToSignalToGameObject(EventSystem.current.gameObject); hasInput = true; } if (!multiplayerMode) //multiplayer mode disabled -> stop here return; #if INPUT_SYSTEM_PACKAGE MultiplayerEventSystem[] multiplayerEventSystems = FindObjectsOfType(); if (!hasInput || multiplayerEventSystems == null || multiplayerEventSystems.Length == 0) { Debug.LogWarning ( $"MultiplayerMode -> No {nameof(MultiplayerEventSystem)} found. " + $"Add at least one to the scene to fix this issue." ); return; } foreach (MultiplayerEventSystem eventSystem in multiplayerEventSystems) AddInputToSignalToGameObject(eventSystem.gameObject); #endif hasInput = true; } /// /// Execute the 'Back' button event, only if can fire and is enabled. /// This method is used to simulate a 'Back' button /// public static void Fire(InputSignalData data) { if (instance == null) return; //instance is null when the application is quitting if (instance.inCooldown) return; //if the 'Back' button is in cooldown, don't execute anything (to prevent spamming) instance.m_LastTimeBackButtonWasExecuted = Time.realtimeSinceStartup; //update the last time the 'Back' button was executed streamIgnoreDisabled.SendSignal(data); //send a MetaSignal (with input data) on the secondary stream, regardless of the 'Back' button's disabled state or if it is in cooldown if (!instance.isEnabled) return; //if the 'Back' button is disabled, don't send the signal on the primary stream stream.SendSignal(data); //send a MetaSignal (with input data) on the main stream } /// /// Execute the 'Back' button event, only if can fire and is enabled. /// This method is used to simulate a 'Back' button /// public static void Fire() { if (instance == null) return; //instance is null when the application is quitting if (instance.inCooldown) return; //if the 'Back' button is in cooldown, don't execute anything (to prevent spamming) streamIgnoreDisabled.SendSignal(); //send a Signal (without input data) (ping) on the secondary stream, regardless of the 'Back' button's disabled state or if it is in cooldown instance.m_LastTimeBackButtonWasExecuted = Time.realtimeSinceStartup; //update the last time the 'Back' button was executed if (!instance.isEnabled) return; //if the 'Back' button is disabled, don't send the signal on the primary stream stream.SendSignal(); //send a Signal (without input data) (ping) on the main stream } /// True if the 'Back' button functionality is enabled public static bool IsEnabled() => instance != null && instance.isEnabled; /// True if the 'Back' button functionality is disabled public static bool IsDisabled() => instance != null && instance.isDisabled; /// Disable the 'Back' button functionality public static void Disable() { if (instance == null) return; //instance is null when the application is quitting if (instance.isEnabled) streamOnDisabled.SendSignal($"{nameof(BackButton)}.{nameof(Disable)}"); //send a signal on the streamOnDisabled stream instance.m_BackButtonDisableLevel++; //if == 0 --> false (back button is not disabled) if > 0 --> true (back button is disabled) } /// Enable the 'Back' button functionality public static void Enable() { if (instance == null) return; //instance is null when the application is quitting instance.m_BackButtonDisableLevel--; //if == 0 --> false (back button is not disabled) if > 0 --> true (back button is disabled) if (instance.m_BackButtonDisableLevel < 0) instance.m_BackButtonDisableLevel = 0; //clamp the value to zero if (instance.isEnabled) streamOnEnabled.SendSignal($"{nameof(BackButton)}.{nameof(Enable)}"); //send a signal on the streamOnEnabled stream } /// /// Enable the 'Back' button functionality by resetting the additive bool to zero. backButtonDisableLevel = 0. /// Use this ONLY for special cases when something wrong happens and the back button is stuck in disabled mode. /// public static void EnableByForce() { if (instance == null) return; //instance is null when the application is quitting instance.m_BackButtonDisableLevel = 0; //reset the additive bool to zero streamOnEnabled.SendSignal($"{nameof(BackButton)}.{nameof(EnableByForce)}"); //send a signal on the streamOnEnabled stream } /// /// Checks if the given target has an InputToSignal that triggers the 'Back' button. /// If no such InputToSignal is found, one is added automatically /// /// Target gameObject private static void AddInputToSignalToGameObject(GameObject target) { #if !INPUT_SYSTEM_PACKAGE && !LEGACY_INPUT_MANAGER return; #endif #pragma warning disable CS0162 //search that there is at least one InputToSignal able to trigger the 'Back' button; if not, add it InputToSignal[] inputsToSignal = target.GetComponents(); if ( inputsToSignal == null || inputsToSignal.Length == 0 || !inputsToSignal.Any(i => i.SendsBackButtonSignal()) ) { #if INPUT_SYSTEM_PACKAGE target .AddComponent() .ConnectToAction(UIInputActionName.Cancel); #endif #if LEGACY_INPUT_MANAGER InputToSignal its = target.AddComponent(); its.inputMode = LegacyInputMode.KeyCode; its.keyCode = BackButton.k_BackButtonKeyCode; its.virtualButtonName = BackButton.k_BackButtonVirtualButtonName; #endif } #pragma warning restore CS0162 } } public static class BackButtonExtras { public static bool SendsBackButtonSignal(this T target) where T : InputToSignal { #if INPUT_SYSTEM_PACKAGE return target != null && target.isConnected && target.inputActionName.Equals(UIInputActionName.Cancel.ToString()); #endif #if LEGACY_INPUT_MANAGER return target != null && target.inputMode switch { LegacyInputMode.None => false, LegacyInputMode.KeyCode => target.keyCode == BackButton.k_BackButtonKeyCode, LegacyInputMode.VirtualButton => target.virtualButtonName == BackButton.k_BackButtonVirtualButtonName, _ => false }; #endif #pragma warning disable CS0162 // ReSharper disable once HeuristicUnreachableCode return false; #pragma warning restore CS0162 } } }