423 lines
19 KiB
C#
423 lines
19 KiB
C#
// 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
|
||
{
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
[AddComponentMenu("Doozy/Input/Back Button")]
|
||
[DisallowMultipleComponent]
|
||
public class BackButton : SingletonBehaviour<BackButton>
|
||
{
|
||
#if UNITY_EDITOR
|
||
[UnityEditor.MenuItem("GameObject/Doozy/Input/Back Button", false, 8)]
|
||
private static void CreateComponent(UnityEditor.MenuCommand menuCommand)
|
||
{
|
||
GameObjectUtils.AddToScene<BackButton>("Back Button", false, true);
|
||
}
|
||
#endif
|
||
|
||
#if LEGACY_INPUT_MANAGER
|
||
public const KeyCode k_BackButtonKeyCode = KeyCode.Escape;
|
||
#endif
|
||
/// <summary> Default name of the virtual button that will be used for the 'Back' button, when LEGACY_INPUT_MANAGER is enabled </summary>
|
||
public const string k_BackButtonVirtualButtonName = "Cancel";
|
||
|
||
/// <summary> Stream category name for the Back Button </summary>
|
||
public const string k_StreamCategory = "Input";
|
||
|
||
/// <summary>
|
||
/// Stream name for the 'Back' Button.
|
||
/// Used by stream to listen for the Back Button.
|
||
/// </summary>
|
||
public const string k_StreamName = nameof(BackButton);
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
public const string k_StreamNameIgnoreDisabled = k_StreamName + ".IgnoreDisabledState";
|
||
|
||
/// <summary>
|
||
/// Stream name for the 'Back' Button.
|
||
/// Used by streamOnEnabled to listen for when the 'Back' button functionality was enabled (from the disabled state)
|
||
/// </summary>
|
||
public const string k_StreamNameOnEnabled = k_StreamName + ".Enabled";
|
||
|
||
/// <summary>
|
||
/// Stream name for the 'Back' Button.
|
||
/// Used by streamOnDisabled to listen for when the 'Back' button functionality was disabled (from the enabled state)
|
||
/// </summary>
|
||
public const string k_StreamNameOnDisabled = k_StreamName + ".Disabled";
|
||
|
||
/// <summary> Default button name for the 'Back' Button </summary>
|
||
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;
|
||
|
||
/// <summary>
|
||
/// Stream that sends signals when the 'Back' button is fired.
|
||
/// This stream does not send signals when the 'Back' button is disabled.
|
||
/// </summary>
|
||
public static SignalStream stream => s_stream ??= SignalsService.GetStream(k_StreamCategory, k_StreamName);
|
||
|
||
/// <summary>
|
||
/// Stream that sends signals when the 'Back' button is fired.
|
||
/// This stream sends signals regardless if the 'Back' button is disabled or not.
|
||
/// </summary>
|
||
public static SignalStream streamIgnoreDisabled => s_streamIgnoreDisabled ??= SignalsService.GetStream(k_StreamCategory, k_StreamNameIgnoreDisabled);
|
||
|
||
/// <summary>
|
||
/// Stream that sends signals when the 'Back' button functionality was enabled (from the disabled state)
|
||
/// </summary>
|
||
public static SignalStream streamOnEnabled => s_streamOnEnabled ??= SignalsService.GetStream(k_StreamCategory, k_StreamNameOnEnabled);
|
||
|
||
/// <summary>
|
||
/// Stream that sends signals when the 'Back' button functionality was disabled (from the enabled state)
|
||
/// </summary>
|
||
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);
|
||
|
||
/// <summary> Reference to the UIManager Input Settings </summary>
|
||
public static UIManagerInputSettings inputSettings => UIManagerInputSettings.instance;
|
||
|
||
/// <summary> True Multiplayer Mode is enabled </summary>
|
||
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
|
||
|
||
/// <summary> Cooldown after the 'Back' button was fired (to prevent spamming and accidental double execution) </summary>
|
||
public static float cooldown => inputSettings.backButtonCooldown;
|
||
|
||
/// <summary> True if the 'Back' button functionality is disabled </summary>
|
||
public bool isDisabled
|
||
{
|
||
get
|
||
{
|
||
if (m_BackButtonDisableLevel < 0) m_BackButtonDisableLevel = 0;
|
||
return m_BackButtonDisableLevel != 0;
|
||
}
|
||
}
|
||
|
||
/// <summary> True if the 'Back' button functionality is enabled </summary>
|
||
public bool isEnabled => !isDisabled;
|
||
|
||
/// <summary> True if the 'Back' button can be triggered again </summary>
|
||
public bool inCooldown => Time.realtimeSinceStartup - m_LastTimeBackButtonWasExecuted < cooldown;
|
||
|
||
/// <summary> True if the 'Back' button functionality is enabled and is not in cooldown </summary>
|
||
public bool canFire => isEnabled && !inCooldown;
|
||
|
||
/// <summary> Flag marked as True if a InputToSignal set to listen for the 'Back' button exists in the scene </summary>
|
||
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<MultiplayerEventSystem>();
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Execute the 'Back' button event, only if can fire and is enabled.
|
||
/// This method is used to simulate a 'Back' button
|
||
/// </summary>
|
||
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
|
||
}
|
||
|
||
/// <summary>
|
||
/// Execute the 'Back' button event, only if can fire and is enabled.
|
||
/// This method is used to simulate a 'Back' button
|
||
/// </summary>
|
||
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
|
||
}
|
||
|
||
/// <summary> True if the 'Back' button functionality is enabled </summary>
|
||
public static bool IsEnabled() => instance != null && instance.isEnabled;
|
||
|
||
/// <summary> True if the 'Back' button functionality is disabled </summary>
|
||
public static bool IsDisabled() => instance != null && instance.isDisabled;
|
||
|
||
/// <summary> Disable the 'Back' button functionality </summary>
|
||
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)
|
||
}
|
||
|
||
/// <summary> Enable the 'Back' button functionality </summary>
|
||
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
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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
|
||
}
|
||
|
||
/// <summary>
|
||
/// Checks if the given target has an InputToSignal that triggers the 'Back' button.
|
||
/// If no such InputToSignal is found, one is added automatically
|
||
/// </summary>
|
||
/// <param name="target"> Target gameObject </param>
|
||
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<InputToSignal>();
|
||
if
|
||
(
|
||
inputsToSignal == null ||
|
||
inputsToSignal.Length == 0 ||
|
||
!inputsToSignal.Any(i => i.SendsBackButtonSignal())
|
||
)
|
||
{
|
||
#if INPUT_SYSTEM_PACKAGE
|
||
target
|
||
.AddComponent<InputToSignal>()
|
||
.ConnectToAction(UIInputActionName.Cancel);
|
||
#endif
|
||
|
||
#if LEGACY_INPUT_MANAGER
|
||
InputToSignal its = target.AddComponent<InputToSignal>();
|
||
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<T>(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
|
||
}
|
||
}
|
||
}
|