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