RebindActionUI.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using UnityEngine.Events;
  5. using UnityEngine.InputSystem.Utilities;
  6. using UnityEngine.UI;
  7. ////TODO: localization support
  8. ////TODO: deal with composites that have parts bound in different control schemes
  9. namespace UnityEngine.InputSystem.Samples.RebindUI
  10. {
  11. /// <summary>
  12. /// A reusable component with a self-contained UI for rebinding a single action.
  13. /// </summary>
  14. public class RebindActionUI : MonoBehaviour
  15. {
  16. /// <summary>
  17. /// Reference to the action that is to be rebound.
  18. /// </summary>
  19. public InputActionReference actionReference
  20. {
  21. get => m_Action;
  22. set
  23. {
  24. m_Action = value;
  25. UpdateActionLabel();
  26. UpdateBindingDisplay();
  27. }
  28. }
  29. /// <summary>
  30. /// ID (in string form) of the binding that is to be rebound on the action.
  31. /// </summary>
  32. /// <seealso cref="InputBinding.id"/>
  33. public string bindingId
  34. {
  35. get => m_BindingId;
  36. set
  37. {
  38. m_BindingId = value;
  39. UpdateBindingDisplay();
  40. }
  41. }
  42. public InputBinding.DisplayStringOptions displayStringOptions
  43. {
  44. get => m_DisplayStringOptions;
  45. set
  46. {
  47. m_DisplayStringOptions = value;
  48. UpdateBindingDisplay();
  49. }
  50. }
  51. /// <summary>
  52. /// Text component that receives the name of the action. Optional.
  53. /// </summary>
  54. public Text actionLabel
  55. {
  56. get => m_ActionLabel;
  57. set
  58. {
  59. m_ActionLabel = value;
  60. UpdateActionLabel();
  61. }
  62. }
  63. /// <summary>
  64. /// Text component that receives the display string of the binding. Can be <c>null</c> in which
  65. /// case the component entirely relies on <see cref="updateBindingUIEvent"/>.
  66. /// </summary>
  67. public Text bindingText
  68. {
  69. get => m_BindingText;
  70. set
  71. {
  72. m_BindingText = value;
  73. UpdateBindingDisplay();
  74. }
  75. }
  76. /// <summary>
  77. /// Optional text component that receives a text prompt when waiting for a control to be actuated.
  78. /// </summary>
  79. /// <seealso cref="startRebindEvent"/>
  80. /// <seealso cref="rebindOverlay"/>
  81. public Text rebindPrompt
  82. {
  83. get => m_RebindText;
  84. set => m_RebindText = value;
  85. }
  86. /// <summary>
  87. /// Optional UI that is activated when an interactive rebind is started and deactivated when the rebind
  88. /// is finished. This is normally used to display an overlay over the current UI while the system is
  89. /// waiting for a control to be actuated.
  90. /// </summary>
  91. /// <remarks>
  92. /// If neither <see cref="rebindPrompt"/> nor <c>rebindOverlay</c> is set, the component will temporarily
  93. /// replaced the <see cref="bindingText"/> (if not <c>null</c>) with <c>"Waiting..."</c>.
  94. /// </remarks>
  95. /// <seealso cref="startRebindEvent"/>
  96. /// <seealso cref="rebindPrompt"/>
  97. public GameObject rebindOverlay
  98. {
  99. get => m_RebindOverlay;
  100. set => m_RebindOverlay = value;
  101. }
  102. /// <summary>
  103. /// Event that is triggered every time the UI updates to reflect the current binding.
  104. /// This can be used to tie custom visualizations to bindings.
  105. /// </summary>
  106. public UpdateBindingUIEvent updateBindingUIEvent
  107. {
  108. get
  109. {
  110. if (m_UpdateBindingUIEvent == null)
  111. m_UpdateBindingUIEvent = new UpdateBindingUIEvent();
  112. return m_UpdateBindingUIEvent;
  113. }
  114. }
  115. /// <summary>
  116. /// Event that is triggered when an interactive rebind is started on the action.
  117. /// </summary>
  118. public InteractiveRebindEvent startRebindEvent
  119. {
  120. get
  121. {
  122. if (m_RebindStartEvent == null)
  123. m_RebindStartEvent = new InteractiveRebindEvent();
  124. return m_RebindStartEvent;
  125. }
  126. }
  127. /// <summary>
  128. /// Event that is triggered when an interactive rebind has been completed or canceled.
  129. /// </summary>
  130. public InteractiveRebindEvent stopRebindEvent
  131. {
  132. get
  133. {
  134. if (m_RebindStopEvent == null)
  135. m_RebindStopEvent = new InteractiveRebindEvent();
  136. return m_RebindStopEvent;
  137. }
  138. }
  139. /// <summary>
  140. /// When an interactive rebind is in progress, this is the rebind operation controller.
  141. /// Otherwise, it is <c>null</c>.
  142. /// </summary>
  143. public InputActionRebindingExtensions.RebindingOperation ongoingRebind => m_RebindOperation;
  144. /// <summary>
  145. /// Return the action and binding index for the binding that is targeted by the component
  146. /// according to
  147. /// </summary>
  148. /// <param name="action"></param>
  149. /// <param name="bindingIndex"></param>
  150. /// <returns></returns>
  151. public bool ResolveActionAndBinding(out InputAction action, out int bindingIndex)
  152. {
  153. bindingIndex = -1;
  154. action = m_Action?.action;
  155. if (action == null)
  156. return false;
  157. if (string.IsNullOrEmpty(m_BindingId))
  158. return false;
  159. // Look up binding index.
  160. var bindingId = new Guid(m_BindingId);
  161. bindingIndex = action.bindings.IndexOf(x => x.id == bindingId);
  162. if (bindingIndex == -1)
  163. {
  164. Debug.LogError($"Cannot find binding with ID '{bindingId}' on '{action}'", this);
  165. return false;
  166. }
  167. return true;
  168. }
  169. /// <summary>
  170. /// Trigger a refresh of the currently displayed binding.
  171. /// </summary>
  172. public void UpdateBindingDisplay()
  173. {
  174. var displayString = string.Empty;
  175. var deviceLayoutName = default(string);
  176. var controlPath = default(string);
  177. // Get display string from action.
  178. var action = m_Action?.action;
  179. if (action != null)
  180. {
  181. var bindingIndex = action.bindings.IndexOf(x => x.id.ToString() == m_BindingId);
  182. if (bindingIndex != -1)
  183. displayString = action.GetBindingDisplayString(bindingIndex, out deviceLayoutName, out controlPath, displayStringOptions);
  184. }
  185. // Set on label (if any).
  186. if (m_BindingText != null)
  187. m_BindingText.text = displayString;
  188. // Give listeners a chance to configure UI in response.
  189. m_UpdateBindingUIEvent?.Invoke(this, displayString, deviceLayoutName, controlPath);
  190. }
  191. /// <summary>
  192. /// Remove currently applied binding overrides.
  193. /// </summary>
  194. public void ResetToDefault()
  195. {
  196. if (!ResolveActionAndBinding(out var action, out var bindingIndex))
  197. return;
  198. if (action.bindings[bindingIndex].isComposite)
  199. {
  200. // It's a composite. Remove overrides from part bindings.
  201. for (var i = bindingIndex + 1; i < action.bindings.Count && action.bindings[i].isPartOfComposite; ++i)
  202. action.RemoveBindingOverride(i);
  203. }
  204. else
  205. {
  206. action.RemoveBindingOverride(bindingIndex);
  207. }
  208. UpdateBindingDisplay();
  209. }
  210. /// <summary>
  211. /// Initiate an interactive rebind that lets the player actuate a control to choose a new binding
  212. /// for the action.
  213. /// </summary>
  214. public void StartInteractiveRebind()
  215. {
  216. if (!ResolveActionAndBinding(out var action, out var bindingIndex))
  217. return;
  218. // If the binding is a composite, we need to rebind each part in turn.
  219. if (action.bindings[bindingIndex].isComposite)
  220. {
  221. var firstPartIndex = bindingIndex + 1;
  222. if (firstPartIndex < action.bindings.Count && action.bindings[firstPartIndex].isPartOfComposite)
  223. PerformInteractiveRebind(action, firstPartIndex, allCompositeParts: true);
  224. }
  225. else
  226. {
  227. PerformInteractiveRebind(action, bindingIndex);
  228. }
  229. }
  230. private void PerformInteractiveRebind(InputAction action, int bindingIndex, bool allCompositeParts = false)
  231. {
  232. m_RebindOperation?.Cancel(); // Will null out m_RebindOperation.
  233. void CleanUp()
  234. {
  235. m_RebindOperation?.Dispose();
  236. m_RebindOperation = null;
  237. }
  238. // Configure the rebind.
  239. m_RebindOperation = action.PerformInteractiveRebinding(bindingIndex)
  240. .OnCancel(
  241. operation =>
  242. {
  243. m_RebindStopEvent?.Invoke(this, operation);
  244. m_RebindOverlay?.SetActive(false);
  245. UpdateBindingDisplay();
  246. CleanUp();
  247. })
  248. .OnComplete(
  249. operation =>
  250. {
  251. m_RebindOverlay?.SetActive(false);
  252. m_RebindStopEvent?.Invoke(this, operation);
  253. UpdateBindingDisplay();
  254. CleanUp();
  255. // If there's more composite parts we should bind, initiate a rebind
  256. // for the next part.
  257. if (allCompositeParts)
  258. {
  259. var nextBindingIndex = bindingIndex + 1;
  260. if (nextBindingIndex < action.bindings.Count && action.bindings[nextBindingIndex].isPartOfComposite)
  261. PerformInteractiveRebind(action, nextBindingIndex, true);
  262. }
  263. });
  264. // If it's a part binding, show the name of the part in the UI.
  265. var partName = default(string);
  266. if (action.bindings[bindingIndex].isPartOfComposite)
  267. partName = $"Binding '{action.bindings[bindingIndex].name}'. ";
  268. // Bring up rebind overlay, if we have one.
  269. m_RebindOverlay?.SetActive(true);
  270. if (m_RebindText != null)
  271. {
  272. var text = !string.IsNullOrEmpty(m_RebindOperation.expectedControlType)
  273. ? $"{partName}Waiting for {m_RebindOperation.expectedControlType} input..."
  274. : $"{partName}Waiting for input...";
  275. m_RebindText.text = text;
  276. }
  277. // If we have no rebind overlay and no callback but we have a binding text label,
  278. // temporarily set the binding text label to "<Waiting>".
  279. if (m_RebindOverlay == null && m_RebindText == null && m_RebindStartEvent == null && m_BindingText != null)
  280. m_BindingText.text = "<Waiting...>";
  281. // Give listeners a chance to act on the rebind starting.
  282. m_RebindStartEvent?.Invoke(this, m_RebindOperation);
  283. m_RebindOperation.Start();
  284. }
  285. protected void OnEnable()
  286. {
  287. if (s_RebindActionUIs == null)
  288. s_RebindActionUIs = new List<RebindActionUI>();
  289. s_RebindActionUIs.Add(this);
  290. if (s_RebindActionUIs.Count == 1)
  291. InputSystem.onActionChange += OnActionChange;
  292. }
  293. protected void OnDisable()
  294. {
  295. m_RebindOperation?.Dispose();
  296. m_RebindOperation = null;
  297. s_RebindActionUIs.Remove(this);
  298. if (s_RebindActionUIs.Count == 0)
  299. {
  300. s_RebindActionUIs = null;
  301. InputSystem.onActionChange -= OnActionChange;
  302. }
  303. }
  304. // When the action system re-resolves bindings, we want to update our UI in response. While this will
  305. // also trigger from changes we made ourselves, it ensures that we react to changes made elsewhere. If
  306. // the user changes keyboard layout, for example, we will get a BoundControlsChanged notification and
  307. // will update our UI to reflect the current keyboard layout.
  308. private static void OnActionChange(object obj, InputActionChange change)
  309. {
  310. if (change != InputActionChange.BoundControlsChanged)
  311. return;
  312. var action = obj as InputAction;
  313. var actionMap = action?.actionMap ?? obj as InputActionMap;
  314. var actionAsset = actionMap?.asset ?? obj as InputActionAsset;
  315. for (var i = 0; i < s_RebindActionUIs.Count; ++i)
  316. {
  317. var component = s_RebindActionUIs[i];
  318. var referencedAction = component.actionReference?.action;
  319. if (referencedAction == null)
  320. continue;
  321. if (referencedAction == action ||
  322. referencedAction.actionMap == actionMap ||
  323. referencedAction.actionMap?.asset == actionAsset)
  324. component.UpdateBindingDisplay();
  325. }
  326. }
  327. [Tooltip("Reference to action that is to be rebound from the UI.")]
  328. [SerializeField]
  329. private InputActionReference m_Action;
  330. [SerializeField]
  331. private string m_BindingId;
  332. [SerializeField]
  333. private InputBinding.DisplayStringOptions m_DisplayStringOptions;
  334. [Tooltip("Text label that will receive the name of the action. Optional. Set to None to have the "
  335. + "rebind UI not show a label for the action.")]
  336. [SerializeField]
  337. private Text m_ActionLabel;
  338. [Tooltip("Text label that will receive the current, formatted binding string.")]
  339. [SerializeField]
  340. private Text m_BindingText;
  341. [Tooltip("Optional UI that will be shown while a rebind is in progress.")]
  342. [SerializeField]
  343. private GameObject m_RebindOverlay;
  344. [Tooltip("Optional text label that will be updated with prompt for user input.")]
  345. [SerializeField]
  346. private Text m_RebindText;
  347. [Tooltip("Event that is triggered when the way the binding is display should be updated. This allows displaying "
  348. + "bindings in custom ways, e.g. using images instead of text.")]
  349. [SerializeField]
  350. private UpdateBindingUIEvent m_UpdateBindingUIEvent;
  351. [Tooltip("Event that is triggered when an interactive rebind is being initiated. This can be used, for example, "
  352. + "to implement custom UI behavior while a rebind is in progress. It can also be used to further "
  353. + "customize the rebind.")]
  354. [SerializeField]
  355. private InteractiveRebindEvent m_RebindStartEvent;
  356. [Tooltip("Event that is triggered when an interactive rebind is complete or has been aborted.")]
  357. [SerializeField]
  358. private InteractiveRebindEvent m_RebindStopEvent;
  359. private InputActionRebindingExtensions.RebindingOperation m_RebindOperation;
  360. private static List<RebindActionUI> s_RebindActionUIs;
  361. // We want the label for the action name to update in edit mode, too, so
  362. // we kick that off from here.
  363. #if UNITY_EDITOR
  364. protected void OnValidate()
  365. {
  366. UpdateActionLabel();
  367. UpdateBindingDisplay();
  368. }
  369. #endif
  370. private void UpdateActionLabel()
  371. {
  372. if (m_ActionLabel != null)
  373. {
  374. var action = m_Action?.action;
  375. m_ActionLabel.text = action != null ? action.name : string.Empty;
  376. }
  377. }
  378. [Serializable]
  379. public class UpdateBindingUIEvent : UnityEvent<RebindActionUI, string, string, string>
  380. {
  381. }
  382. [Serializable]
  383. public class InteractiveRebindEvent : UnityEvent<RebindActionUI, InputActionRebindingExtensions.RebindingOperation>
  384. {
  385. }
  386. }
  387. }