Sin descripción
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

RebindActionUI.cs 16KB

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