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