123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446 |
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using UnityEngine.Events;
- using UnityEngine.InputSystem.Utilities;
- using UnityEngine.UI;
- ////TODO: localization support
- ////TODO: deal with composites that have parts bound in different control schemes
- namespace UnityEngine.InputSystem.Samples.RebindUI
- {
- /// <summary>
- /// A reusable component with a self-contained UI for rebinding a single action.
- /// </summary>
- public class RebindActionUI : MonoBehaviour
- {
- /// <summary>
- /// Reference to the action that is to be rebound.
- /// </summary>
- public InputActionReference actionReference
- {
- get => m_Action;
- set
- {
- m_Action = value;
- UpdateActionLabel();
- UpdateBindingDisplay();
- }
- }
- /// <summary>
- /// ID (in string form) of the binding that is to be rebound on the action.
- /// </summary>
- /// <seealso cref="InputBinding.id"/>
- public string bindingId
- {
- get => m_BindingId;
- set
- {
- m_BindingId = value;
- UpdateBindingDisplay();
- }
- }
- public InputBinding.DisplayStringOptions displayStringOptions
- {
- get => m_DisplayStringOptions;
- set
- {
- m_DisplayStringOptions = value;
- UpdateBindingDisplay();
- }
- }
- /// <summary>
- /// Text component that receives the name of the action. Optional.
- /// </summary>
- public Text actionLabel
- {
- get => m_ActionLabel;
- set
- {
- m_ActionLabel = value;
- UpdateActionLabel();
- }
- }
- /// <summary>
- /// Text component that receives the display string of the binding. Can be <c>null</c> in which
- /// case the component entirely relies on <see cref="updateBindingUIEvent"/>.
- /// </summary>
- public Text bindingText
- {
- get => m_BindingText;
- set
- {
- m_BindingText = value;
- UpdateBindingDisplay();
- }
- }
- /// <summary>
- /// Optional text component that receives a text prompt when waiting for a control to be actuated.
- /// </summary>
- /// <seealso cref="startRebindEvent"/>
- /// <seealso cref="rebindOverlay"/>
- public Text rebindPrompt
- {
- get => m_RebindText;
- set => m_RebindText = value;
- }
- /// <summary>
- /// Optional UI that is activated when an interactive rebind is started and deactivated when the rebind
- /// is finished. This is normally used to display an overlay over the current UI while the system is
- /// waiting for a control to be actuated.
- /// </summary>
- /// <remarks>
- /// If neither <see cref="rebindPrompt"/> nor <c>rebindOverlay</c> is set, the component will temporarily
- /// replaced the <see cref="bindingText"/> (if not <c>null</c>) with <c>"Waiting..."</c>.
- /// </remarks>
- /// <seealso cref="startRebindEvent"/>
- /// <seealso cref="rebindPrompt"/>
- public GameObject rebindOverlay
- {
- get => m_RebindOverlay;
- set => m_RebindOverlay = value;
- }
- /// <summary>
- /// Event that is triggered every time the UI updates to reflect the current binding.
- /// This can be used to tie custom visualizations to bindings.
- /// </summary>
- public UpdateBindingUIEvent updateBindingUIEvent
- {
- get
- {
- if (m_UpdateBindingUIEvent == null)
- m_UpdateBindingUIEvent = new UpdateBindingUIEvent();
- return m_UpdateBindingUIEvent;
- }
- }
- /// <summary>
- /// Event that is triggered when an interactive rebind is started on the action.
- /// </summary>
- public InteractiveRebindEvent startRebindEvent
- {
- get
- {
- if (m_RebindStartEvent == null)
- m_RebindStartEvent = new InteractiveRebindEvent();
- return m_RebindStartEvent;
- }
- }
- /// <summary>
- /// Event that is triggered when an interactive rebind has been completed or canceled.
- /// </summary>
- public InteractiveRebindEvent stopRebindEvent
- {
- get
- {
- if (m_RebindStopEvent == null)
- m_RebindStopEvent = new InteractiveRebindEvent();
- return m_RebindStopEvent;
- }
- }
- /// <summary>
- /// When an interactive rebind is in progress, this is the rebind operation controller.
- /// Otherwise, it is <c>null</c>.
- /// </summary>
- public InputActionRebindingExtensions.RebindingOperation ongoingRebind => m_RebindOperation;
- /// <summary>
- /// Return the action and binding index for the binding that is targeted by the component
- /// according to
- /// </summary>
- /// <param name="action"></param>
- /// <param name="bindingIndex"></param>
- /// <returns></returns>
- public bool ResolveActionAndBinding(out InputAction action, out int bindingIndex)
- {
- bindingIndex = -1;
- action = m_Action?.action;
- if (action == null)
- return false;
- if (string.IsNullOrEmpty(m_BindingId))
- return false;
- // Look up binding index.
- var bindingId = new Guid(m_BindingId);
- bindingIndex = action.bindings.IndexOf(x => x.id == bindingId);
- if (bindingIndex == -1)
- {
- Debug.LogError($"Cannot find binding with ID '{bindingId}' on '{action}'", this);
- return false;
- }
- return true;
- }
- /// <summary>
- /// Trigger a refresh of the currently displayed binding.
- /// </summary>
- public void UpdateBindingDisplay()
- {
- var displayString = string.Empty;
- var deviceLayoutName = default(string);
- var controlPath = default(string);
- // Get display string from action.
- var action = m_Action?.action;
- if (action != null)
- {
- var bindingIndex = action.bindings.IndexOf(x => x.id.ToString() == m_BindingId);
- if (bindingIndex != -1)
- displayString = action.GetBindingDisplayString(bindingIndex, out deviceLayoutName, out controlPath, displayStringOptions);
- }
- // Set on label (if any).
- if (m_BindingText != null)
- m_BindingText.text = displayString;
- // Give listeners a chance to configure UI in response.
- m_UpdateBindingUIEvent?.Invoke(this, displayString, deviceLayoutName, controlPath);
- }
- /// <summary>
- /// Remove currently applied binding overrides.
- /// </summary>
- public void ResetToDefault()
- {
- if (!ResolveActionAndBinding(out var action, out var bindingIndex))
- return;
- if (action.bindings[bindingIndex].isComposite)
- {
- // It's a composite. Remove overrides from part bindings.
- for (var i = bindingIndex + 1; i < action.bindings.Count && action.bindings[i].isPartOfComposite; ++i)
- action.RemoveBindingOverride(i);
- }
- else
- {
- action.RemoveBindingOverride(bindingIndex);
- }
- UpdateBindingDisplay();
- }
- /// <summary>
- /// Initiate an interactive rebind that lets the player actuate a control to choose a new binding
- /// for the action.
- /// </summary>
- public void StartInteractiveRebind()
- {
- if (!ResolveActionAndBinding(out var action, out var bindingIndex))
- return;
- // If the binding is a composite, we need to rebind each part in turn.
- if (action.bindings[bindingIndex].isComposite)
- {
- var firstPartIndex = bindingIndex + 1;
- if (firstPartIndex < action.bindings.Count && action.bindings[firstPartIndex].isPartOfComposite)
- PerformInteractiveRebind(action, firstPartIndex, allCompositeParts: true);
- }
- else
- {
- PerformInteractiveRebind(action, bindingIndex);
- }
- }
- private void PerformInteractiveRebind(InputAction action, int bindingIndex, bool allCompositeParts = false)
- {
- m_RebindOperation?.Cancel(); // Will null out m_RebindOperation.
- void CleanUp()
- {
- m_RebindOperation?.Dispose();
- m_RebindOperation = null;
- }
- // Configure the rebind.
- m_RebindOperation = action.PerformInteractiveRebinding(bindingIndex)
- .OnCancel(
- operation =>
- {
- m_RebindStopEvent?.Invoke(this, operation);
- m_RebindOverlay?.SetActive(false);
- UpdateBindingDisplay();
- CleanUp();
- })
- .OnComplete(
- operation =>
- {
- m_RebindOverlay?.SetActive(false);
- m_RebindStopEvent?.Invoke(this, operation);
- UpdateBindingDisplay();
- CleanUp();
- // If there's more composite parts we should bind, initiate a rebind
- // for the next part.
- if (allCompositeParts)
- {
- var nextBindingIndex = bindingIndex + 1;
- if (nextBindingIndex < action.bindings.Count && action.bindings[nextBindingIndex].isPartOfComposite)
- PerformInteractiveRebind(action, nextBindingIndex, true);
- }
- });
- // If it's a part binding, show the name of the part in the UI.
- var partName = default(string);
- if (action.bindings[bindingIndex].isPartOfComposite)
- partName = $"Binding '{action.bindings[bindingIndex].name}'. ";
- // Bring up rebind overlay, if we have one.
- m_RebindOverlay?.SetActive(true);
- if (m_RebindText != null)
- {
- var text = !string.IsNullOrEmpty(m_RebindOperation.expectedControlType)
- ? $"{partName}Waiting for {m_RebindOperation.expectedControlType} input..."
- : $"{partName}Waiting for input...";
- m_RebindText.text = text;
- }
- // If we have no rebind overlay and no callback but we have a binding text label,
- // temporarily set the binding text label to "<Waiting>".
- if (m_RebindOverlay == null && m_RebindText == null && m_RebindStartEvent == null && m_BindingText != null)
- m_BindingText.text = "<Waiting...>";
- // Give listeners a chance to act on the rebind starting.
- m_RebindStartEvent?.Invoke(this, m_RebindOperation);
- m_RebindOperation.Start();
- }
- protected void OnEnable()
- {
- if (s_RebindActionUIs == null)
- s_RebindActionUIs = new List<RebindActionUI>();
- s_RebindActionUIs.Add(this);
- if (s_RebindActionUIs.Count == 1)
- InputSystem.onActionChange += OnActionChange;
- }
- protected void OnDisable()
- {
- m_RebindOperation?.Dispose();
- m_RebindOperation = null;
- s_RebindActionUIs.Remove(this);
- if (s_RebindActionUIs.Count == 0)
- {
- s_RebindActionUIs = null;
- InputSystem.onActionChange -= OnActionChange;
- }
- }
- // When the action system re-resolves bindings, we want to update our UI in response. While this will
- // also trigger from changes we made ourselves, it ensures that we react to changes made elsewhere. If
- // the user changes keyboard layout, for example, we will get a BoundControlsChanged notification and
- // will update our UI to reflect the current keyboard layout.
- private static void OnActionChange(object obj, InputActionChange change)
- {
- if (change != InputActionChange.BoundControlsChanged)
- return;
- var action = obj as InputAction;
- var actionMap = action?.actionMap ?? obj as InputActionMap;
- var actionAsset = actionMap?.asset ?? obj as InputActionAsset;
- for (var i = 0; i < s_RebindActionUIs.Count; ++i)
- {
- var component = s_RebindActionUIs[i];
- var referencedAction = component.actionReference?.action;
- if (referencedAction == null)
- continue;
- if (referencedAction == action ||
- referencedAction.actionMap == actionMap ||
- referencedAction.actionMap?.asset == actionAsset)
- component.UpdateBindingDisplay();
- }
- }
- [Tooltip("Reference to action that is to be rebound from the UI.")]
- [SerializeField]
- private InputActionReference m_Action;
- [SerializeField]
- private string m_BindingId;
- [SerializeField]
- private InputBinding.DisplayStringOptions m_DisplayStringOptions;
- [Tooltip("Text label that will receive the name of the action. Optional. Set to None to have the "
- + "rebind UI not show a label for the action.")]
- [SerializeField]
- private Text m_ActionLabel;
- [Tooltip("Text label that will receive the current, formatted binding string.")]
- [SerializeField]
- private Text m_BindingText;
- [Tooltip("Optional UI that will be shown while a rebind is in progress.")]
- [SerializeField]
- private GameObject m_RebindOverlay;
- [Tooltip("Optional text label that will be updated with prompt for user input.")]
- [SerializeField]
- private Text m_RebindText;
- [Tooltip("Event that is triggered when the way the binding is display should be updated. This allows displaying "
- + "bindings in custom ways, e.g. using images instead of text.")]
- [SerializeField]
- private UpdateBindingUIEvent m_UpdateBindingUIEvent;
- [Tooltip("Event that is triggered when an interactive rebind is being initiated. This can be used, for example, "
- + "to implement custom UI behavior while a rebind is in progress. It can also be used to further "
- + "customize the rebind.")]
- [SerializeField]
- private InteractiveRebindEvent m_RebindStartEvent;
- [Tooltip("Event that is triggered when an interactive rebind is complete or has been aborted.")]
- [SerializeField]
- private InteractiveRebindEvent m_RebindStopEvent;
- private InputActionRebindingExtensions.RebindingOperation m_RebindOperation;
- private static List<RebindActionUI> s_RebindActionUIs;
- // We want the label for the action name to update in edit mode, too, so
- // we kick that off from here.
- #if UNITY_EDITOR
- protected void OnValidate()
- {
- UpdateActionLabel();
- UpdateBindingDisplay();
- }
- #endif
- private void UpdateActionLabel()
- {
- if (m_ActionLabel != null)
- {
- var action = m_Action?.action;
- m_ActionLabel.text = action != null ? action.name : string.Empty;
- }
- }
- [Serializable]
- public class UpdateBindingUIEvent : UnityEvent<RebindActionUI, string, string, string>
- {
- }
- [Serializable]
- public class InteractiveRebindEvent : UnityEvent<RebindActionUI, InputActionRebindingExtensions.RebindingOperation>
- {
- }
- }
- }
|