A game about forced loneliness, made by TACStudios
at master 148 kB view raw
1using System; 2using System.Collections.Generic; 3using System.Text; 4using UnityEngine.InputSystem.Layouts; 5using UnityEngine.InputSystem.LowLevel; 6using UnityEngine.InputSystem.Utilities; 7 8// The way target bindings for overrides are found: 9// - If specified, directly by index (e.g. "apply this override to the third binding in the map") 10// - By path (e.g. "search for binding to '<Gamepad>/leftStick' and override it with '<Gamepad>/rightStick'") 11// - By group (e.g. "search for binding on action 'fire' with group 'keyboard&mouse' and override it with '<Keyboard>/space'") 12// - By action (e.g. "bind action 'fire' from whatever it is right now to '<Gamepad>/leftStick'") 13 14////TODO: make this work implicitly with PlayerInputs such that rebinds can be restricted to the device's of a specific player 15 16////TODO: allow rebinding by GUIDs now that we have IDs on bindings 17 18////TODO: make RebindingOperation dispose its memory automatically; re-allocating is not a problem 19 20////TODO: add simple method to RebindingOperation that will create keyboard binding paths by character rather than by key name 21 22////FIXME: properly work with composites 23 24////REVIEW: how well are we handling the case of rebinding to joysticks? (mostly auto-generated HID layouts) 25 26namespace UnityEngine.InputSystem 27{ 28 /// <summary> 29 /// Extensions to help with dynamically rebinding <see cref="InputAction"/>s in 30 /// various ways. 31 /// </summary> 32 /// <remarks> 33 /// Unlike <see cref="InputActionSetupExtensions"/>, the extension methods in here are meant to be 34 /// called during normal game operation, i.e. as part of screens whether the user can rebind 35 /// controls. 36 /// 37 /// The two primary duties of these extensions are to apply binding overrides that non-destructively 38 /// redirect existing bindings and to facilitate user-controlled rebinding by listening for controls 39 /// actuated by the user. 40 /// 41 /// To implement user-controlled rebinding, create a UI with a button to trigger rebinding. 42 /// If the user clicks the button to bind a control to an action, use `InputAction.PerformInteractiveRebinding` 43 /// to handle the rebinding, as in the following example: 44 /// <example> 45 /// <code> 46 /// void RemapButtonClicked(InputAction actionToRebind) 47 /// { 48 /// var rebindOperation = actionToRebind.PerformInteractiveRebinding() 49 /// // To avoid accidental input from mouse motion 50 /// .WithControlsExcluding("Mouse") 51 /// .OnMatchWaitForAnother(0.1f) 52 /// .Start(); 53 /// } 54 /// </code> 55 /// </example> 56 /// You can install the Tanks Demo sample from the Input System package using the Package Manager window, which has an example of an interactive rebinding UI. 57 /// </remarks> 58 /// <seealso cref="InputActionSetupExtensions"/> 59 /// <seealso cref="InputBinding"/> 60 /// <seealso cref="InputAction.bindings"/> 61 public static partial class InputActionRebindingExtensions 62 { 63 /// <summary> 64 /// Get the index of the first binding in <see cref="InputAction.bindings"/> on <paramref name="action"/> 65 /// that matches the given binding mask. 66 /// </summary> 67 /// <param name="action">An input action.</param> 68 /// <param name="bindingMask">Binding mask to match (see <see cref="InputBinding.Matches"/>).</param> 69 /// <returns>The first binding on the action matching <paramref name="bindingMask"/> or -1 if no binding 70 /// on the action matches the mask.</returns> 71 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception> 72 /// <seealso cref="InputBinding.Matches"/> 73 public static int GetBindingIndex(this InputAction action, InputBinding bindingMask) 74 { 75 if (action == null) 76 throw new ArgumentNullException(nameof(action)); 77 78 var bindings = action.bindings; 79 for (var i = 0; i < bindings.Count; ++i) 80 if (bindingMask.Matches(bindings[i])) 81 return i; 82 83 return -1; 84 } 85 86 /// <summary> 87 /// Get the index of the first binding in <see cref="InputActionMap.bindings"/> on <paramref name="actionMap"/> 88 /// that matches the given binding mask. 89 /// </summary> 90 /// <param name="actionMap">An input action map.</param> 91 /// <param name="bindingMask">Binding mask to match (see <see cref="InputBinding.Matches"/>).</param> 92 /// <returns>The first binding on the action matching <paramref name="bindingMask"/> or -1 if no binding 93 /// on the action matches the mask.</returns> 94 /// <exception cref="ArgumentNullException"><paramref name="actionMap"/> is <c>null</c>.</exception> 95 /// <seealso cref="InputBinding.Matches"/> 96 public static int GetBindingIndex(this InputActionMap actionMap, InputBinding bindingMask) 97 { 98 if (actionMap == null) 99 throw new ArgumentNullException(nameof(actionMap)); 100 101 var bindings = actionMap.bindings; 102 for (var i = 0; i < bindings.Count; ++i) 103 if (bindingMask.Matches(bindings[i])) 104 return i; 105 106 return -1; 107 } 108 109 /// <summary> 110 /// Get the index of the first binding in <see cref="InputAction.bindings"/> on <paramref name="action"/> 111 /// that matches the given binding group and/or path. 112 /// </summary> 113 /// <param name="action">An input action.</param> 114 /// <param name="group">Binding group to match (see <see cref="InputBinding.groups"/>).</param> 115 /// <param name="path">Binding path to match (see <see cref="InputBinding.path"/>).</param> 116 /// <returns>The first binding on the action matching the given group and/or path or -1 if no binding 117 /// on the action matches.</returns> 118 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception> 119 /// <seealso cref="InputBinding.Matches"/> 120 public static int GetBindingIndex(this InputAction action, string group = default, string path = default) 121 { 122 if (action == null) 123 throw new ArgumentNullException(nameof(action)); 124 return action.GetBindingIndex(new InputBinding(groups: group, path: path)); 125 } 126 127 /// <summary> 128 /// Return the binding that the given control resolved from. 129 /// </summary> 130 /// <param name="action">An input action that may be using the given control.</param> 131 /// <param name="control">Control to look for a binding for.</param> 132 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c> -or- <paramref name="control"/> 133 /// is <c>null</c>.</exception> 134 /// <returns>The binding from which <paramref name="control"/> has been resolved or <c>null</c> if no such binding 135 /// could be found on <paramref name="action"/>.</returns> 136 public static InputBinding? GetBindingForControl(this InputAction action, InputControl control) 137 { 138 if (action == null) 139 throw new ArgumentNullException(nameof(action)); 140 if (control == null) 141 throw new ArgumentNullException(nameof(control)); 142 143 var bindingIndex = GetBindingIndexForControl(action, control); 144 if (bindingIndex == -1) 145 return null; 146 return action.bindings[bindingIndex]; 147 } 148 149 /// <summary> 150 /// Return the index into <paramref name="action"/>'s <see cref="InputAction.bindings"/> that corresponds 151 /// to <paramref name="control"/> bound to the action. 152 /// </summary> 153 /// <param name="action">The input action whose bindings to use.</param> 154 /// <param name="control">An input control for which to look for a binding.</param> 155 /// <returns>The index into the action's binding array for the binding that <paramref name="control"/> was 156 /// resolved from or -1 if the control is not currently bound to the action.</returns> 157 /// <remarks> 158 /// Note that this method will only take currently active bindings into consideration. This means that if 159 /// the given control <em>could</em> come from one of the bindings on the action but does not currently 160 /// do so, the method still returns -1. 161 /// 162 /// In case you want to manually find out which of the bindings on the action could match the given control, 163 /// you can do so using <see cref="InputControlPath.Matches"/>: 164 /// 165 /// <example> 166 /// <code> 167 /// // Find the binding on 'action' that matches the given 'control'. 168 /// foreach (var binding in action.bindings) 169 /// if (InputControlPath.Matches(binding.effectivePath, control)) 170 /// Debug.Log($"Binding for {control}: {binding}"); 171 /// </code> 172 /// </example> 173 /// </remarks> 174 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c> -or- <paramref name="control"/> 175 /// is <c>null</c>.</exception> 176 public static unsafe int GetBindingIndexForControl(this InputAction action, InputControl control) 177 { 178 if (action == null) 179 throw new ArgumentNullException(nameof(action)); 180 if (control == null) 181 throw new ArgumentNullException(nameof(control)); 182 183 var actionMap = action.GetOrCreateActionMap(); 184 actionMap.ResolveBindingsIfNecessary(); 185 186 var state = actionMap.m_State; 187 Debug.Assert(state != null, "Bindings are expected to have been resolved at this point"); 188 189 var controls = state.controls; 190 var controlCount = state.totalControlCount; 191 var bindingStates = state.bindingStates; 192 var controlIndexToBindingIndex = state.controlIndexToBindingIndex; 193 var actionIndex = action.m_ActionIndexInState; 194 195 // Go through all controls in the state until we find our control. 196 for (var i = 0; i < controlCount; ++i) 197 { 198 if (controls[i] != control) 199 continue; 200 201 // The control may be the same one we're looking for but may be bound to a completely 202 // different action. Skip anything that isn't related to our action. 203 var bindingIndexInState = controlIndexToBindingIndex[i]; 204 if (bindingStates[bindingIndexInState].actionIndex != actionIndex) 205 continue; 206 207 // Got it. 208 var bindingIndexInMap = state.GetBindingIndexInMap(bindingIndexInState); 209 return action.BindingIndexOnMapToBindingIndexOnAction(bindingIndexInMap); 210 } 211 212 return -1; 213 } 214 215 ////TODO: add option to make it *not* take bound controls into account when creating display strings 216 217 /// <summary> 218 /// Return a string suitable for display in UIs that shows what the given action is currently bound to. 219 /// </summary> 220 /// <param name="action">Action to create a display string for.</param> 221 /// <param name="options">Optional set of formatting flags.</param> 222 /// <param name="group">Optional binding group to restrict the operation to. If this is supplied, it effectively 223 /// becomes the binding mask (see <see cref="InputBinding.Matches(InputBinding)"/>) to supply to <see 224 /// cref="GetBindingDisplayString(InputAction,InputBinding,InputBinding.DisplayStringOptions)"/>.</param> 225 /// <returns>A string suitable for display in rebinding UIs.</returns> 226 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception> 227 /// <remarks> 228 /// This method will take into account any binding masks (such as from control schemes) in effect on the action 229 /// (such as <see cref="InputAction.bindingMask"/> on the action itself, the <see cref="InputActionMap.bindingMask"/> 230 /// on its action map, or the <see cref="InputActionAsset.bindingMask"/> on its asset) as well as the actual controls 231 /// that the action is currently bound to (see <see cref="InputAction.controls"/>). 232 /// 233 /// <example> 234 /// <code> 235 /// var action = new InputAction(); 236 /// 237 /// action.AddBinding("&lt;Gamepad&gt;/buttonSouth", groups: "Gamepad"); 238 /// action.AddBinding("&lt;Mouse&gt;/leftButton", groups: "KeyboardMouse"); 239 /// 240 /// // Prints "A | LMB". 241 /// Debug.Log(action.GetBindingDisplayString()); 242 /// 243 /// // Prints "A". 244 /// Debug.Log(action.GetBindingDisplayString(group: "Gamepad"); 245 /// 246 /// // Prints "LMB". 247 /// Debug.Log(action.GetBindingDisplayString(group: "KeyboardMouse"); 248 /// </code> 249 /// </example> 250 /// </remarks> 251 /// <seealso cref="InputBinding.ToDisplayString(InputBinding.DisplayStringOptions,InputControl)"/> 252 /// <seealso cref="InputControlPath.ToHumanReadableString(string,InputControlPath.HumanReadableStringOptions,InputControl)"/> 253 public static string GetBindingDisplayString(this InputAction action, InputBinding.DisplayStringOptions options = default, 254 string group = default) 255 { 256 if (action == null) 257 throw new ArgumentNullException(nameof(action)); 258 259 // Default binding mask to the one found on the action or any of its 260 // containers. 261 InputBinding bindingMask; 262 if (!string.IsNullOrEmpty(group)) 263 { 264 bindingMask = InputBinding.MaskByGroup(group); 265 } 266 else 267 { 268 var mask = action.FindEffectiveBindingMask(); 269 if (mask.HasValue) 270 bindingMask = mask.Value; 271 else 272 bindingMask = default; 273 } 274 275 return GetBindingDisplayString(action, bindingMask, options); 276 } 277 278 /// <summary> 279 /// Return a string suitable for display in UIs that shows what the given action is currently bound to. 280 /// </summary> 281 /// <param name="action">Action to create a display string for.</param> 282 /// <param name="bindingMask">Mask for bindings to take into account. Any binding on the action not 283 /// matching (see <see cref="InputBinding.Matches(InputBinding)"/>) the mask is ignored and not included 284 /// in the resulting string.</param> 285 /// <param name="options">Optional set of formatting flags.</param> 286 /// <returns>A string suitable for display in rebinding UIs.</returns> 287 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception> 288 /// <remarks> 289 /// This method will take into account any binding masks (such as from control schemes) in effect on the action 290 /// (such as <see cref="InputAction.bindingMask"/> on the action itself, the <see cref="InputActionMap.bindingMask"/> 291 /// on its action map, or the <see cref="InputActionAsset.bindingMask"/> on its asset) as well as the actual controls 292 /// that the action is currently bound to (see <see cref="InputAction.controls"/>). 293 /// 294 /// <example> 295 /// <code> 296 /// var action = new InputAction(); 297 /// 298 /// action.AddBinding("&lt;Gamepad&gt;/buttonSouth", groups: "Gamepad"); 299 /// action.AddBinding("&lt;Mouse&gt;/leftButton", groups: "KeyboardMouse"); 300 /// 301 /// // Prints "A". 302 /// Debug.Log(action.GetBindingDisplayString(InputBinding.MaskByGroup("Gamepad")); 303 /// 304 /// // Prints "LMB". 305 /// Debug.Log(action.GetBindingDisplayString(InputBinding.MaskByGroup("KeyboardMouse")); 306 /// </code> 307 /// </example> 308 /// </remarks> 309 /// <seealso cref="InputBinding.ToDisplayString(InputBinding.DisplayStringOptions,InputControl)"/> 310 /// <seealso cref="InputControlPath.ToHumanReadableString(string,InputControlPath.HumanReadableStringOptions,InputControl)"/> 311 public static string GetBindingDisplayString(this InputAction action, InputBinding bindingMask, 312 InputBinding.DisplayStringOptions options = default) 313 { 314 if (action == null) 315 throw new ArgumentNullException(nameof(action)); 316 317 var result = string.Empty; 318 var bindings = action.bindings; 319 for (var i = 0; i < bindings.Count; ++i) 320 { 321 if (bindings[i].isPartOfComposite) 322 continue; 323 if (!bindingMask.Matches(bindings[i])) 324 continue; 325 326 ////REVIEW: should this filter out bindings that are not resolving to any controls? 327 328 var text = action.GetBindingDisplayString(i, options); 329 if (result != "") 330 result = $"{result} | {text}"; 331 else 332 result = text; 333 } 334 335 return result; 336 } 337 338 /// <summary> 339 /// Return a string suitable for display in UIs that shows what the given action is currently bound to. 340 /// </summary> 341 /// <param name="action">Action to create a display string for.</param> 342 /// <param name="bindingIndex">Index of the binding in the <see cref="InputAction.bindings"/> array of 343 /// <paramref name="action"/> for which to get a display string.</param> 344 /// <param name="options">Optional set of formatting flags.</param> 345 /// <returns>A string suitable for display in rebinding UIs.</returns> 346 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception> 347 /// <remarks> 348 /// This method will ignore active binding masks and return the display string for the given binding whether it 349 /// is masked out (disabled) or not. 350 /// 351 /// <example> 352 /// <code> 353 /// var action = new InputAction(); 354 /// 355 /// action.AddBinding("&lt;Gamepad&gt;/buttonSouth", groups: "Gamepad"); 356 /// action.AddBinding("&lt;Mouse&gt;/leftButton", groups: "KeyboardMouse"); 357 /// 358 /// // Prints "A". 359 /// Debug.Log(action.GetBindingDisplayString(0)); 360 /// 361 /// // Prints "LMB". 362 /// Debug.Log(action.GetBindingDisplayString(1)); 363 /// </code> 364 /// </example> 365 /// </remarks> 366 /// <seealso cref="InputBinding.ToDisplayString(InputBinding.DisplayStringOptions,InputControl)"/> 367 /// <seealso cref="InputControlPath.ToHumanReadableString(string,InputControlPath.HumanReadableStringOptions,InputControl)"/> 368 public static string GetBindingDisplayString(this InputAction action, int bindingIndex, InputBinding.DisplayStringOptions options = default) 369 { 370 if (action == null) 371 throw new ArgumentNullException(nameof(action)); 372 373 return action.GetBindingDisplayString(bindingIndex, out var _, out var _, options); 374 } 375 376 /// <summary> 377 /// Return a string suitable for display in UIs that shows what the given action is currently bound to. 378 /// </summary> 379 /// <param name="action">Action to create a display string for.</param> 380 /// <param name="bindingIndex">Index of the binding in the <see cref="InputAction.bindings"/> array of 381 /// <paramref name="action"/> for which to get a display string.</param> 382 /// <param name="deviceLayoutName">Receives the name of the <see cref="InputControlLayout"/> used for the 383 /// device in the given binding, if applicable. Otherwise is set to <c>null</c>. If, for example, the binding 384 /// is <c>"&lt;Gamepad&gt;/buttonSouth"</c>, the resulting value is <c>"Gamepad</c>.</param> 385 /// <param name="controlPath">Receives the path to the control on the device referenced in the given binding, 386 /// if applicable. Otherwise is set to <c>null</c>. If, for example, the binding is <c>"&lt;Gamepad&gt;/leftStick/x"</c>, 387 /// the resulting value is <c>"leftStick/x"</c>.</param> 388 /// <param name="options">Optional set of formatting flags.</param> 389 /// <returns>A string suitable for display in rebinding UIs.</returns> 390 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception> 391 /// <remarks> 392 /// The information returned by <paramref name="deviceLayoutName"/> and <paramref name="controlPath"/> can be used, for example, 393 /// to associate images with controls. Based on knowing which layout is used and which control on the layout is referenced, you 394 /// can look up an image dynamically. For example, if the layout is based on <see cref="DualShock.DualShockGamepad"/> (use 395 /// <see cref="InputSystem.IsFirstLayoutBasedOnSecond"/> to determine inheritance), you can pick a PlayStation-specific image 396 /// for the control as named by <paramref name="controlPath"/>. 397 /// 398 /// <example> 399 /// <code> 400 /// var action = new InputAction(); 401 /// 402 /// action.AddBinding("&lt;Gamepad&gt;/dpad/up", groups: "Gamepad"); 403 /// action.AddBinding("&lt;Mouse&gt;/leftButton", groups: "KeyboardMouse"); 404 /// 405 /// // Prints "A", then "Gamepad", then "dpad/up". 406 /// Debug.Log(action.GetBindingDisplayString(0, out var deviceLayoutNameA, out var controlPathA)); 407 /// Debug.Log(deviceLayoutNameA); 408 /// Debug.Log(controlPathA); 409 /// 410 /// // Prints "LMB", then "Mouse", then "leftButton". 411 /// Debug.Log(action.GetBindingDisplayString(1, out var deviceLayoutNameB, out var controlPathB)); 412 /// Debug.Log(deviceLayoutNameB); 413 /// Debug.Log(controlPathB); 414 /// </code> 415 /// </example> 416 /// </remarks> 417 /// <seealso cref="InputBinding.ToDisplayString(InputBinding.DisplayStringOptions,InputControl)"/> 418 /// <seealso cref="InputControlPath.ToHumanReadableString(string,InputControlPath.HumanReadableStringOptions,InputControl)"/> 419 /// <seealso cref="InputActionRebindingExtensions.GetBindingIndex(InputAction,InputBinding)"/> 420 public static unsafe string GetBindingDisplayString(this InputAction action, int bindingIndex, 421 out string deviceLayoutName, out string controlPath, 422 InputBinding.DisplayStringOptions options = default) 423 { 424 if (action == null) 425 throw new ArgumentNullException(nameof(action)); 426 427 deviceLayoutName = null; 428 controlPath = null; 429 430 var bindings = action.bindings; 431 var bindingCount = bindings.Count; 432 if (bindingIndex < 0 || bindingIndex >= bindingCount) 433 throw new ArgumentOutOfRangeException( 434 $"Binding index {bindingIndex} is out of range on action '{action}' with {bindings.Count} bindings", 435 nameof(bindingIndex)); 436 437 // If the binding is a composite, compose a string using the display format string for 438 // the composite. 439 // NOTE: In this case, there won't be a deviceLayoutName returned from the method. 440 if (bindings[bindingIndex].isComposite) 441 { 442 var compositeName = NameAndParameters.Parse(bindings[bindingIndex].effectivePath).name; 443 444 // Determine what parts we have. 445 var firstPartIndex = bindingIndex + 1; 446 var lastPartIndex = firstPartIndex; 447 while (lastPartIndex < bindingCount && bindings[lastPartIndex].isPartOfComposite) 448 ++lastPartIndex; 449 var partCount = lastPartIndex - firstPartIndex; 450 451 // Get the display string for each part. 452 var partStrings = new string[partCount]; 453 for (var i = 0; i < partCount; ++i) 454 { 455 var partString = action.GetBindingDisplayString(firstPartIndex + i, options); 456 if (string.IsNullOrEmpty(partString)) 457 partString = " "; 458 partStrings[i] = partString; 459 } 460 461 // Put the parts together based on the display format string for 462 // the composite. 463 var displayFormatString = InputBindingComposite.GetDisplayFormatString(compositeName); 464 if (string.IsNullOrEmpty(displayFormatString)) 465 { 466 // No display format string. Simply go and combine all part strings. 467 return StringHelpers.Join("/", partStrings); 468 } 469 470 return StringHelpers.ExpandTemplateString(displayFormatString, 471 fragment => 472 { 473 var result = string.Empty; 474 475 // Go through all parts and look for one with the given name. 476 for (var i = 0; i < partCount; ++i) 477 { 478 if (!string.Equals(bindings[firstPartIndex + i].name, fragment, StringComparison.InvariantCultureIgnoreCase)) 479 continue; 480 481 if (!string.IsNullOrEmpty(result)) 482 result = $"{result}|{partStrings[i]}"; 483 else 484 result = partStrings[i]; 485 } 486 487 if (string.IsNullOrEmpty(result)) 488 result = " "; 489 490 return result; 491 }); 492 } 493 494 // See if the binding maps to controls. 495 InputControl control = null; 496 var actionMap = action.GetOrCreateActionMap(); 497 actionMap.ResolveBindingsIfNecessary(); 498 var actionState = actionMap.m_State; 499 Debug.Assert(actionState != null, "Expecting action state to be in place at this point"); 500 var bindingIndexInMap = action.BindingIndexOnActionToBindingIndexOnMap(bindingIndex); 501 var bindingIndexInState = actionState.GetBindingIndexInState(actionMap.m_MapIndexInState, bindingIndexInMap); 502 Debug.Assert(bindingIndexInState >= 0 && bindingIndexInState < actionState.totalBindingCount, 503 "Computed binding index is out of range"); 504 var bindingStatePtr = &actionState.bindingStates[bindingIndexInState]; 505 if (bindingStatePtr->controlCount > 0) 506 { 507 ////REVIEW: does it make sense to just take a single control here? 508 control = actionState.controls[bindingStatePtr->controlStartIndex]; 509 } 510 511 // Take interactions applied to the action into account (except if explicitly forced off). 512 var binding = bindings[bindingIndex]; 513 if (string.IsNullOrEmpty(binding.effectiveInteractions)) 514 binding.overrideInteractions = action.interactions; 515 else if (!string.IsNullOrEmpty(action.interactions)) 516 binding.overrideInteractions = $"{binding.effectiveInteractions};action.interactions"; 517 518 return binding.ToDisplayString(out deviceLayoutName, out controlPath, options, control: control); 519 } 520 521 /// <summary> 522 /// Put an override on all matching bindings of <paramref name="action"/>. 523 /// </summary> 524 /// <param name="action">Action to apply the override to.</param> 525 /// <param name="newPath">New binding path to take effect. Supply an empty string 526 /// to disable the binding(s). See <see cref="InputControlPath"/> for details on 527 /// the path language.</param> 528 /// <param name="group">Optional list of binding groups to target the override 529 /// to. For example, <c>"Keyboard;Gamepad"</c> will only apply overrides to bindings 530 /// that either have the <c>"Keyboard"</c> or the <c>"Gamepad"</c> binding group 531 /// listed in <see cref="InputBinding.groups"/>.</param> 532 /// <param name="path">Only override bindings that have this exact path.</param> 533 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception> 534 /// <remarks> 535 /// Calling this method is equivalent to calling <see cref="ApplyBindingOverride(InputAction,InputBinding)"/> 536 /// with the properties of the given <see cref="InputBinding"/> initialized accordingly. 537 /// 538 /// <example> 539 /// <code> 540 /// // Override the binding to the gamepad A button with a binding to 541 /// // the Y button. 542 /// fireAction.ApplyBindingOverride("&lt;Gamepad&gt;/buttonNorth", 543 /// path: "&lt;Gamepad&gt;/buttonSouth); 544 /// </code> 545 /// </example> 546 /// </remarks> 547 /// <seealso cref="ApplyBindingOverride(InputAction,InputBinding)"/> 548 /// <seealso cref="InputBinding.effectivePath"/> 549 /// <seealso cref="InputBinding.overridePath"/> 550 /// <seealso cref="InputBinding.Matches"/> 551 public static void ApplyBindingOverride(this InputAction action, string newPath, string group = null, string path = null) 552 { 553 if (action == null) 554 throw new ArgumentNullException(nameof(action)); 555 556 ApplyBindingOverride(action, new InputBinding {overridePath = newPath, groups = group, path = path}); 557 } 558 559 /// <summary> 560 /// Apply overrides to all bindings on <paramref name="action"/> that match <paramref name="bindingOverride"/>. 561 /// The override values are taken from <see cref="InputBinding.overridePath"/>, <see cref="InputBinding.overrideProcessors"/>, 562 /// and <seealso cref="InputBinding.overrideInteractions"/> on <paramref name="bindingOverride"/>. 563 /// </summary> 564 /// <param name="action">Action to override bindings on.</param> 565 /// <param name="bindingOverride">A binding that both acts as a mask (see <see cref="InputBinding.Matches"/>) 566 /// on the bindings to <paramref name="action"/> and as a container for the override values.</param> 567 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception> 568 /// <remarks> 569 /// The method will go through all of the bindings for <paramref name="action"/> (i.e. its <see cref="InputAction.bindings"/>) 570 /// and call <see cref="InputBinding.Matches"/> on them with <paramref name="bindingOverride"/>. 571 /// For every binding that returns <c>true</c> from <c>Matches</c>, the override values from the 572 /// binding (i.e. <see cref="InputBinding.overridePath"/>, <see cref="InputBinding.overrideProcessors"/>, 573 /// and <see cref="InputBinding.overrideInteractions"/>) are copied into the binding. 574 /// 575 /// Binding overrides are non-destructive. They do not change the bindings set up for an action 576 /// but rather apply non-destructive modifications that change the paths of existing bindings. 577 /// However, this also means that for overrides to work, there have to be existing bindings that 578 /// can be modified. 579 /// 580 /// This is achieved by setting <see cref="InputBinding.overridePath"/> which is a non-serialized 581 /// property. When resolving bindings, the system will use <see cref="InputBinding.effectivePath"/> 582 /// which uses <see cref="InputBinding.overridePath"/> if set or <see cref="InputBinding.path"/> 583 /// otherwise. The same applies to <see cref="InputBinding.effectiveProcessors"/> and <see 584 /// cref="InputBinding.effectiveInteractions"/>. 585 /// 586 /// <example> 587 /// <code> 588 /// // Override the binding in the "KeyboardMouse" group on 'fireAction' 589 /// // by setting its override binding path to the space bar on the keyboard. 590 /// fireAction.ApplyBindingOverride(new InputBinding 591 /// { 592 /// groups = "KeyboardMouse", 593 /// overridePath = "&lt;Keyboard&gt;/space" 594 /// }); 595 /// </code> 596 /// </example> 597 /// 598 /// If the given action is enabled when calling this method, the effect will be immediate, 599 /// i.e. binding resolution takes place and <see cref="InputAction.controls"/> are updated. 600 /// If the action is not enabled, binding resolution is deferred to when controls are needed 601 /// next (usually when either <see cref="InputAction.controls"/> is queried or when the 602 /// action is enabled). 603 /// </remarks> 604 /// <seealso cref="InputAction.bindings"/> 605 /// <seealso cref="InputBinding.Matches"/> 606 public static void ApplyBindingOverride(this InputAction action, InputBinding bindingOverride) 607 { 608 if (action == null) 609 throw new ArgumentNullException(nameof(action)); 610 611 var enabled = action.enabled; 612 if (enabled) 613 action.Disable(); 614 615 bindingOverride.action = action.name; 616 var actionMap = action.GetOrCreateActionMap(); 617 ApplyBindingOverride(actionMap, bindingOverride); 618 619 if (enabled) 620 { 621 action.Enable(); 622 action.RequestInitialStateCheckOnEnabledAction(); 623 } 624 } 625 626 /// <summary> 627 /// Apply a binding override to the Nth binding on the given action. 628 /// </summary> 629 /// <param name="action">Action to apply the binding override to.</param> 630 /// <param name="bindingIndex">Index of the binding in <see cref="InputAction.bindings"/> to 631 /// which to apply the override to.</param> 632 /// <param name="bindingOverride">A binding that specifies the overrides to apply. In particular, 633 /// the <see cref="InputBinding.overridePath"/>, <see cref="InputBinding.overrideProcessors"/>, and 634 /// <see cref="InputBinding.overrideInteractions"/> properties will be copied into the binding 635 /// in <see cref="InputAction.bindings"/>. The remaining fields will be ignored by this method.</param> 636 /// <exception cref="ArgumentNullException"><paramref name="action"/> is null.</exception> 637 /// <exception cref="ArgumentOutOfRangeException"><paramref name="bindingIndex"/> is out of range.</exception> 638 /// <remarks> 639 /// Unlike <see cref="ApplyBindingOverride(InputAction,InputBinding)"/> this method will 640 /// not use <see cref="InputBinding.Matches"/> to determine which binding to apply the 641 /// override to. Instead, it will apply the override to the binding at the given index 642 /// and to that binding alone. 643 /// 644 /// The remaining details of applying overrides are identical to <see 645 /// cref="ApplyBindingOverride(InputAction,InputBinding)"/>. 646 /// 647 /// Note that calling this method with an empty (default-constructed) <paramref name="bindingOverride"/> 648 /// is equivalent to resetting all overrides on the given binding. 649 /// 650 /// <example> 651 /// <code> 652 /// // Reset the overrides on the second binding on 'fireAction'. 653 /// fireAction.ApplyBindingOverride(1, default); 654 /// </code> 655 /// </example> 656 /// </remarks> 657 /// <seealso cref="ApplyBindingOverride(InputAction,InputBinding)"/> 658 public static void ApplyBindingOverride(this InputAction action, int bindingIndex, InputBinding bindingOverride) 659 { 660 if (action == null) 661 throw new ArgumentNullException(nameof(action)); 662 663 var indexOnMap = action.BindingIndexOnActionToBindingIndexOnMap(bindingIndex); 664 bindingOverride.action = action.name; 665 ApplyBindingOverride(action.GetOrCreateActionMap(), indexOnMap, bindingOverride); 666 } 667 668 /// <summary> 669 /// Apply a binding override to the Nth binding on the given action. 670 /// </summary> 671 /// <param name="action">Action to apply the binding override to.</param> 672 /// <param name="bindingIndex">Index of the binding in <see cref="InputAction.bindings"/> to 673 /// which to apply the override to.</param> 674 /// <param name="path">Override path (<see cref="InputBinding.overridePath"/>) to set on 675 /// the given binding in <see cref="InputAction.bindings"/>.</param> 676 /// <exception cref="ArgumentNullException"><paramref name="action"/> is null.</exception> 677 /// <exception cref="ArgumentOutOfRangeException"><paramref name="bindingIndex"/> is out of range.</exception> 678 /// <remarks> 679 /// Calling this method is equivalent to calling <see cref="ApplyBindingOverride(InputAction,int,InputBinding)"/> 680 /// like so: 681 /// 682 /// <example> 683 /// <code> 684 /// action.ApplyBindingOverride(new InputBinding { overridePath = path }); 685 /// </code> 686 /// </example> 687 /// </remarks> 688 /// <seealso cref="ApplyBindingOverride(InputAction,int,InputBinding)"/> 689 public static void ApplyBindingOverride(this InputAction action, int bindingIndex, string path) 690 { 691 if (path == null) 692 throw new ArgumentException("Binding path cannot be null", nameof(path)); 693 ApplyBindingOverride(action, bindingIndex, new InputBinding {overridePath = path}); 694 } 695 696 /// <summary> 697 /// Apply the given binding override to all bindings in the map that are matched by the override. 698 /// </summary> 699 /// <param name="actionMap">An action map. Overrides will be applied to its <see cref="InputActionMap.bindings"/>.</param> 700 /// <param name="bindingOverride">Binding that is matched (see <see cref="InputBinding.Matches"/>) against 701 /// the <see cref="InputActionMap.bindings"/> of <paramref name="actionMap"/>. The binding's 702 /// <see cref="InputBinding.overridePath"/>, <see cref="InputBinding.overrideInteractions"/>, and 703 /// <see cref="InputBinding.overrideProcessors"/> properties will be copied over to any matching binding.</param> 704 /// <returns>The number of bindings overridden in the given map.</returns> 705 /// <exception cref="ArgumentNullException"><paramref name="actionMap"/> is <c>null</c>.</exception> 706 /// <seealso cref="InputActionMap.bindings"/> 707 /// <seealso cref="InputBinding.overridePath"/> 708 /// <seealso cref="InputBinding.overrideInteractions"/> 709 /// <seealso cref="InputBinding.overrideProcessors"/> 710 public static int ApplyBindingOverride(this InputActionMap actionMap, InputBinding bindingOverride) 711 { 712 if (actionMap == null) 713 throw new ArgumentNullException(nameof(actionMap)); 714 715 var bindings = actionMap.m_Bindings; 716 if (bindings == null) 717 return 0; 718 719 // Go through all bindings in the map and match them to the override. 720 var bindingCount = bindings.Length; 721 var matchCount = 0; 722 for (var i = 0; i < bindingCount; ++i) 723 { 724 if (!bindingOverride.Matches(ref bindings[i])) 725 continue; 726 727 // Set overrides on binding. 728 bindings[i].overridePath = bindingOverride.overridePath; 729 bindings[i].overrideInteractions = bindingOverride.overrideInteractions; 730 bindings[i].overrideProcessors = bindingOverride.overrideProcessors; 731 ++matchCount; 732 } 733 734 if (matchCount > 0) 735 actionMap.OnBindingModified(); 736 737 return matchCount; 738 } 739 740 /// <summary> 741 /// Copy the override properties (<see cref="InputBinding.overridePath"/>, <see cref="InputBinding.overrideProcessors"/>, 742 /// and <see cref="InputBinding.overrideInteractions"/>) from <paramref name="bindingOverride"/> over to the 743 /// binding at index <paramref name="bindingIndex"/> in <see cref="InputActionMap.bindings"/> of <paramref name="actionMap"/>. 744 /// </summary> 745 /// <param name="actionMap">Action map whose bindings to modify.</param> 746 /// <param name="bindingIndex">Index of the binding to modify in <see cref="InputActionMap.bindings"/> of 747 /// <paramref name="actionMap"/>.</param> 748 /// <param name="bindingOverride">Binding whose override properties (<see cref="InputBinding.overridePath"/>, 749 /// <see cref="InputBinding.overrideProcessors"/>, and <see cref="InputBinding.overrideInteractions"/>) to copy.</param> 750 /// <exception cref="ArgumentNullException"><paramref name="actionMap"/> is <c>null</c>.</exception> 751 /// <exception cref="ArgumentOutOfRangeException"><paramref name="bindingIndex"/> is not a valid index for 752 /// <see cref="InputActionMap.bindings"/> of <paramref name="actionMap"/>.</exception> 753 public static void ApplyBindingOverride(this InputActionMap actionMap, int bindingIndex, InputBinding bindingOverride) 754 { 755 if (actionMap == null) 756 throw new ArgumentNullException(nameof(actionMap)); 757 758 var bindingsCount = actionMap.m_Bindings?.Length ?? 0; 759 if (bindingIndex < 0 || bindingIndex >= bindingsCount) 760 throw new ArgumentOutOfRangeException(nameof(bindingIndex), 761 $"Cannot apply override to binding at index {bindingIndex} in map '{actionMap}' with only {bindingsCount} bindings"); 762 763 actionMap.m_Bindings[bindingIndex].overridePath = bindingOverride.overridePath; 764 actionMap.m_Bindings[bindingIndex].overrideInteractions = bindingOverride.overrideInteractions; 765 actionMap.m_Bindings[bindingIndex].overrideProcessors = bindingOverride.overrideProcessors; 766 767 actionMap.OnBindingModified(); 768 } 769 770 /// <summary> 771 /// Remove any overrides from the binding on <paramref name="action"/> with the given index. 772 /// </summary> 773 /// <param name="action">Action whose bindings to modify.</param> 774 /// <param name="bindingIndex">Index of the binding within <paramref name="action"/>'s <see cref="InputAction.bindings"/>.</param> 775 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception> 776 /// <exception cref="ArgumentOutOfRangeException"><paramref name="bindingIndex"/> is invalid.</exception> 777 public static void RemoveBindingOverride(this InputAction action, int bindingIndex) 778 { 779 if (action == null) 780 throw new ArgumentNullException(nameof(action)); 781 782 action.ApplyBindingOverride(bindingIndex, default(InputBinding)); 783 } 784 785 /// <summary> 786 /// Remove any overrides from the binding on <paramref name="action"/> matching the given binding mask. 787 /// </summary> 788 /// <param name="action">Action whose bindings to modify.</param> 789 /// <param name="bindingMask">Mask that will be matched against the bindings on <paramref name="action"/>. All bindings 790 /// that match the mask (see <see cref="InputBinding.Matches"/>) will have their overrides removed. If none of the 791 /// bindings on the action match the mask, no bindings will be modified.</param> 792 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception> 793 /// <remarks> 794 /// <example> 795 /// <code> 796 /// // Remove all binding overrides from bindings associated with the "Gamepad" binding group. 797 /// myAction.RemoveBindingOverride(InputBinding.MaskByGroup("Gamepad")); 798 /// </code> 799 /// </example> 800 /// </remarks> 801 public static void RemoveBindingOverride(this InputAction action, InputBinding bindingMask) 802 { 803 if (action == null) 804 throw new ArgumentNullException(nameof(action)); 805 806 bindingMask.overridePath = null; 807 bindingMask.overrideInteractions = null; 808 bindingMask.overrideProcessors = null; 809 810 // Simply apply but with a null binding. 811 ApplyBindingOverride(action, bindingMask); 812 } 813 814 private static void RemoveBindingOverride(this InputActionMap actionMap, InputBinding bindingMask) 815 { 816 if (actionMap == null) 817 throw new ArgumentNullException(nameof(actionMap)); 818 819 bindingMask.overridePath = null; 820 bindingMask.overrideInteractions = null; 821 bindingMask.overrideProcessors = null; 822 823 // Simply apply but with a null binding. 824 ApplyBindingOverride(actionMap, bindingMask); 825 } 826 827 /// <summary> 828 /// Restore all bindings in the map to their defaults. 829 /// </summary> 830 /// <param name="actions">Collection of actions to remove overrides from.</param> 831 /// <exception cref="ArgumentNullException"><paramref name="actions"/> is <c>null</c>.</exception> 832 /// <seealso cref="ApplyBindingOverride(InputAction,int,InputBinding)"/> 833 /// <seealso cref="InputBinding.overridePath"/> 834 /// <seealso cref="InputBinding.overrideInteractions"/> 835 /// <seealso cref="InputBinding.overrideProcessors"/> 836 public static void RemoveAllBindingOverrides(this IInputActionCollection2 actions) 837 { 838 if (actions == null) 839 throw new ArgumentNullException(nameof(actions)); 840 841 using (DeferBindingResolution()) 842 { 843 // Go through all actions and then through the bindings in their action maps 844 // and reset the bindings for those actions. Bit of a roundabout and inefficient 845 // way but should be okay. Problem is that IInputActionCollection2 doesn't give 846 // us quite the same level of access as InputActionMap and InputActionAsset do. 847 foreach (var action in actions) 848 { 849 var actionMap = action.GetOrCreateActionMap(); 850 var bindings = actionMap.m_Bindings; 851 var numBindings = bindings.LengthSafe(); 852 853 for (var i = 0; i < numBindings; ++i) 854 { 855 ref var binding = ref bindings[i]; 856 if (!binding.TriggersAction(action)) 857 continue; 858 binding.RemoveOverrides(); 859 } 860 861 actionMap.OnBindingModified(); 862 } 863 } 864 } 865 866 /// <summary> 867 /// Remove all binding overrides on <paramref name="action"/>, i.e. clear all <see cref="InputBinding.overridePath"/>, 868 /// <see cref="InputBinding.overrideProcessors"/>, and <see cref="InputBinding.overrideInteractions"/> set on bindings 869 /// for the given action. 870 /// </summary> 871 /// <param name="action">Action to remove overrides from.</param> 872 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception> 873 /// <seealso cref="ApplyBindingOverride(InputAction,int,InputBinding)"/> 874 /// <seealso cref="InputBinding.overridePath"/> 875 /// <seealso cref="InputBinding.overrideInteractions"/> 876 /// <seealso cref="InputBinding.overrideProcessors"/> 877 public static void RemoveAllBindingOverrides(this InputAction action) 878 { 879 if (action == null) 880 throw new ArgumentNullException(nameof(action)); 881 882 var actionName = action.name; 883 var actionMap = action.GetOrCreateActionMap(); 884 var bindings = actionMap.m_Bindings; 885 if (bindings == null) 886 return; 887 888 var bindingCount = bindings.Length; 889 for (var i = 0; i < bindingCount; ++i) 890 { 891 if (string.Compare(bindings[i].action, actionName, StringComparison.InvariantCultureIgnoreCase) != 0) 892 continue; 893 894 bindings[i].overridePath = null; 895 bindings[i].overrideInteractions = null; 896 bindings[i].overrideProcessors = null; 897 } 898 899 actionMap.OnBindingModified(); 900 } 901 902 ////REVIEW: are the IEnumerable variations worth having? 903 904 public static void ApplyBindingOverrides(this InputActionMap actionMap, IEnumerable<InputBinding> overrides) 905 { 906 if (actionMap == null) 907 throw new ArgumentNullException(nameof(actionMap)); 908 if (overrides == null) 909 throw new ArgumentNullException(nameof(overrides)); 910 911 foreach (var binding in overrides) 912 ApplyBindingOverride(actionMap, binding); 913 } 914 915 public static void RemoveBindingOverrides(this InputActionMap actionMap, IEnumerable<InputBinding> overrides) 916 { 917 if (actionMap == null) 918 throw new ArgumentNullException(nameof(actionMap)); 919 if (overrides == null) 920 throw new ArgumentNullException(nameof(overrides)); 921 922 foreach (var binding in overrides) 923 RemoveBindingOverride(actionMap, binding); 924 } 925 926 ////TODO: add option to suppress any non-matching binding by setting its override to an empty path 927 ////TODO: need ability to do this with a list of controls 928 929 /// <summary> 930 /// For all bindings in the <paramref name="action"/>, if a binding matches a control in the given control 931 /// hierarchy, set an override on the binding to refer specifically to that control. 932 /// </summary> 933 /// <param name="action">An action whose bindings to modify.</param> 934 /// <param name="control">A control hierarchy or an entire <see cref="InputDevice"/>.</param> 935 /// <returns>The number of binding overrides that have been applied to the given action.</returns> 936 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c> -or- <paramref name="control"/> 937 /// is <c>null</c>.</exception> 938 /// <remarks> 939 /// This method can be used to restrict bindings that otherwise apply to a wide set of possible 940 /// controls. 941 /// 942 /// <example> 943 /// <code> 944 /// // Create two gamepads. 945 /// var gamepad1 = InputSystem.AddDevice&lt;Gamepad&gt;(); 946 /// var gamepad2 = InputSystem.AddDevice&lt;Gamepad&gt;(); 947 /// 948 /// // Create an action that binds to the A button on gamepads. 949 /// var action = new InputAction(); 950 /// action.AddBinding("&lt;Gamepad&gt;/buttonSouth"); 951 /// 952 /// // When we enable the action now, it will bind to both 953 /// // gamepad1.buttonSouth and gamepad2.buttonSouth. 954 /// action.Enable(); 955 /// 956 /// // But let's say we want the action to specifically work 957 /// // only with the first gamepad. One way to do it is like 958 /// // this: 959 /// action.ApplyBindingOverridesOnMatchingControls(gamepad1); 960 /// 961 /// // As "&lt;Gamepad&gt;/buttonSouth" matches the gamepad1.buttonSouth 962 /// // control, an override will automatically be applied such that 963 /// // the binding specifically refers to that button on that gamepad. 964 /// </code> 965 /// </example> 966 /// 967 /// Note that for actions that are part of <see cref="InputActionMap"/>s and/or 968 /// <see cref="InputActionAsset"/>s, it is possible to restrict actions to 969 /// specific device without having to set overrides. See <see cref="InputActionMap.bindingMask"/> 970 /// and <see cref="InputActionAsset.bindingMask"/>. 971 /// </remarks> 972 /// <seealso cref="InputActionMap.devices"/> 973 /// <seealso cref="InputActionAsset.devices"/> 974 public static int ApplyBindingOverridesOnMatchingControls(this InputAction action, InputControl control) 975 { 976 if (action == null) 977 throw new ArgumentNullException(nameof(action)); 978 if (control == null) 979 throw new ArgumentNullException(nameof(control)); 980 981 var bindings = action.bindings; 982 var bindingsCount = bindings.Count; 983 var numMatchingControls = 0; 984 985 for (var i = 0; i < bindingsCount; ++i) 986 { 987 var matchingControl = InputControlPath.TryFindControl(control, bindings[i].path); 988 if (matchingControl == null) 989 continue; 990 991 action.ApplyBindingOverride(i, matchingControl.path); 992 ++numMatchingControls; 993 } 994 995 return numMatchingControls; 996 } 997 998 /// <summary> 999 /// For all bindings in the <paramref name="actionMap"/>, if a binding matches a control in the given control 1000 /// hierarchy, set an override on the binding to refer specifically to that control. 1001 /// </summary> 1002 /// <param name="actionMap">An action map whose bindings to modify.</param> 1003 /// <param name="control">A control hierarchy or an entire <see cref="InputDevice"/>.</param> 1004 /// <returns>The number of binding overrides that have been applied to the given action.</returns> 1005 /// <exception cref="ArgumentNullException"><paramref name="actionMap"/> is <c>null</c> -or- <paramref name="control"/> 1006 /// is <c>null</c>.</exception> 1007 /// <remarks> 1008 /// This method can be used to restrict bindings that otherwise apply to a wide set of possible 1009 /// controls. It will go through <see cref="InputActionMap.bindings"/> and apply overrides to 1010 /// <example> 1011 /// <code> 1012 /// // Create two gamepads. 1013 /// var gamepad1 = InputSystem.AddDevice&lt;Gamepad&gt;(); 1014 /// var gamepad2 = InputSystem.AddDevice&lt;Gamepad&gt;(); 1015 /// 1016 /// // Create an action map with an action for the A and B buttons 1017 /// // on gamepads. 1018 /// var actionMap = new InputActionMap(); 1019 /// var aButtonAction = actionMap.AddAction("a", binding: "&lt;Gamepad&gt;/buttonSouth"); 1020 /// var bButtonAction = actionMap.AddAction("b", binding: "&lt;Gamepad&gt;/buttonEast"); 1021 /// 1022 /// // When we enable the action map now, the actions will bind 1023 /// // to the buttons on both gamepads. 1024 /// actionMap.Enable(); 1025 /// 1026 /// // But let's say we want the actions to specifically work 1027 /// // only with the first gamepad. One way to do it is like 1028 /// // this: 1029 /// actionMap.ApplyBindingOverridesOnMatchingControls(gamepad1); 1030 /// 1031 /// // Now binding overrides on the actions will be set to specifically refer 1032 /// // to the controls on the first gamepad. 1033 /// </code> 1034 /// </example> 1035 /// 1036 /// Note that for actions that are part of <see cref="InputActionMap"/>s and/or 1037 /// <see cref="InputActionAsset"/>s, it is possible to restrict actions to 1038 /// specific device without having to set overrides. See <see cref="InputActionMap.bindingMask"/> 1039 /// and <see cref="InputActionAsset.bindingMask"/>. 1040 /// 1041 /// <example> 1042 /// <code> 1043 /// // For an InputActionMap, we could alternatively just do: 1044 /// actionMap.devices = new InputDevice[] { gamepad1 }; 1045 /// </code> 1046 /// </example> 1047 /// </remarks> 1048 /// <seealso cref="InputActionMap.devices"/> 1049 /// <seealso cref="InputActionAsset.devices"/> 1050 public static int ApplyBindingOverridesOnMatchingControls(this InputActionMap actionMap, InputControl control) 1051 { 1052 if (actionMap == null) 1053 throw new ArgumentNullException(nameof(actionMap)); 1054 if (control == null) 1055 throw new ArgumentNullException(nameof(control)); 1056 1057 var actions = actionMap.actions; 1058 var actionCount = actions.Count; 1059 var numMatchingControls = 0; 1060 1061 for (var i = 0; i < actionCount; ++i) 1062 { 1063 var action = actions[i]; 1064 numMatchingControls = action.ApplyBindingOverridesOnMatchingControls(control); 1065 } 1066 1067 return numMatchingControls; 1068 } 1069 1070 /// <summary> 1071 /// Return a JSON string containing all overrides applied to bindings in the given set of <paramref name="actions"/>. 1072 /// </summary> 1073 /// <param name="actions">A collection of <see cref="InputAction"/>s such as an <see cref="InputActionAsset"/> or 1074 /// an <see cref="InputActionMap"/>.</param> 1075 /// <returns>A JSON string containing a serialized version of the overrides applied to bindings in the given set of actions.</returns> 1076 /// <remarks> 1077 /// This method can be used to serialize the overrides, i.e. <see cref="InputBinding.overridePath"/>, 1078 /// <see cref="InputBinding.overrideProcessors"/>, and <see cref="InputBinding.overrideInteractions"/>, applied to 1079 /// bindings in the set of actions. Only overrides will be saved. 1080 /// 1081 /// <example> 1082 /// <code> 1083 /// void SaveUserRebinds(PlayerInput player) 1084 /// { 1085 /// var rebinds = player.actions.SaveBindingOverridesAsJson(); 1086 /// PlayerPrefs.SetString("rebinds", rebinds); 1087 /// } 1088 /// 1089 /// void LoadUserRebinds(PlayerInput player) 1090 /// { 1091 /// var rebinds = PlayerPrefs.GetString("rebinds"); 1092 /// player.actions.LoadBindingOverridesFromJson(rebinds); 1093 /// } 1094 /// </code> 1095 /// </example> 1096 /// 1097 /// Note that this method can also be used with C# wrapper classes generated from .inputactions assets. 1098 /// </remarks> 1099 /// <exception cref="ArgumentNullException"><paramref name="actions"/> is <c>null</c>.</exception> 1100 /// <seealso cref="LoadBindingOverridesFromJson(IInputActionCollection2,string,bool)"/> 1101 public static string SaveBindingOverridesAsJson(this IInputActionCollection2 actions) 1102 { 1103 if (actions == null) 1104 throw new ArgumentNullException(nameof(actions)); 1105 1106 var overrides = new List<InputActionMap.BindingOverrideJson>(); 1107 foreach (var binding in actions.bindings) 1108 actions.AddBindingOverrideJsonTo(binding, overrides); 1109 1110 if (overrides.Count == 0) 1111 return string.Empty; 1112 1113 return JsonUtility.ToJson(new InputActionMap.BindingOverrideListJson {bindings = overrides}); 1114 } 1115 1116 /// <summary> 1117 /// Return a string in JSON format that contains all overrides applied <see cref="InputAction.bindings"/> 1118 /// of <paramref name="action"/>. 1119 /// </summary> 1120 /// <param name="action">An action for which to extract binding overrides.</param> 1121 /// <returns>A string in JSON format containing binding overrides for <paramref name="action"/>.</returns> 1122 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception> 1123 /// <remarks> 1124 /// This overrides can be restored using <seealso cref="LoadBindingOverridesFromJson(InputAction,string,bool)"/>. 1125 /// </remarks> 1126 public static string SaveBindingOverridesAsJson(this InputAction action) 1127 { 1128 if (action == null) 1129 throw new ArgumentNullException(nameof(action)); 1130 1131 var isSingletonAction = action.isSingletonAction; 1132 var actionMap = action.GetOrCreateActionMap(); 1133 var list = new List<InputActionMap.BindingOverrideJson>(); 1134 1135 foreach (var binding in action.bindings) 1136 { 1137 // If we're not looking at a singleton action, the bindings in the map may be 1138 // for other actions. Skip all that are. 1139 if (!isSingletonAction && !binding.TriggersAction(action)) 1140 continue; 1141 1142 actionMap.AddBindingOverrideJsonTo(binding, list, isSingletonAction ? action : null); 1143 } 1144 1145 if (list.Count == 0) 1146 return string.Empty; 1147 1148 return JsonUtility.ToJson(new InputActionMap.BindingOverrideListJson {bindings = list}); 1149 } 1150 1151 private static void AddBindingOverrideJsonTo(this IInputActionCollection2 actions, InputBinding binding, 1152 List<InputActionMap.BindingOverrideJson> list, InputAction action = null) 1153 { 1154 if (!binding.hasOverrides) 1155 return; 1156 1157 ////REVIEW: should this throw if there's no existing GUID on the binding? or should we rather have 1158 //// move avenues for locating a binding on an action? 1159 1160 if (action == null) 1161 action = actions.FindAction(binding.action); 1162 1163 string actionName = action != null && !action.isSingletonAction ? $"{action.actionMap.name}/{action.name}" : ""; 1164 var @override = InputActionMap.BindingOverrideJson.FromBinding(binding, actionName); 1165 1166 list.Add(@override); 1167 } 1168 1169 /// <summary> 1170 /// Restore all binding overrides stored in the given JSON string to the bindings in <paramref name="actions"/>. 1171 /// </summary> 1172 /// <param name="actions">A set of actions and their bindings, such as an <see cref="InputActionMap"/>, an 1173 /// <see cref="InputActionAsset"/>, or a C# wrapper class generated from an .inputactions asset.</param> 1174 /// <param name="json">A string persisting binding overrides in JSON format. See 1175 /// <see cref="SaveBindingOverridesAsJson(IInputActionCollection2)"/>.</param> 1176 /// <param name="removeExisting">If true (default), all existing overrides present on the bindings 1177 /// of <paramref name="actions"/> will be removed first. If false, existing binding overrides will be left 1178 /// in place but may be overwritten by overrides present in <paramref name="json"/>.</param> 1179 /// <remarks> 1180 /// <example> 1181 /// <code> 1182 /// void SaveUserRebinds(PlayerInput player) 1183 /// { 1184 /// var rebinds = player.actions.SaveBindingOverridesAsJson(); 1185 /// PlayerPrefs.SetString("rebinds", rebinds); 1186 /// } 1187 /// 1188 /// void LoadUserRebinds(PlayerInput player) 1189 /// { 1190 /// var rebinds = PlayerPrefs.GetString("rebinds"); 1191 /// player.actions.LoadBindingOverridesFromJson(rebinds); 1192 /// } 1193 /// </code> 1194 /// </example> 1195 /// 1196 /// Note that this method can also be used with C# wrapper classes generated from .inputactions assets. 1197 /// </remarks> 1198 /// <exception cref="ArgumentNullException"><paramref name="actions"/> is <c>null</c>.</exception> 1199 /// <seealso cref="SaveBindingOverridesAsJson(IInputActionCollection2)"/> 1200 /// <seealso cref="InputBinding.overridePath"/> 1201 public static void LoadBindingOverridesFromJson(this IInputActionCollection2 actions, string json, bool removeExisting = true) 1202 { 1203 if (actions == null) 1204 throw new ArgumentNullException(nameof(actions)); 1205 1206 using (DeferBindingResolution()) 1207 { 1208 if (removeExisting) 1209 actions.RemoveAllBindingOverrides(); 1210 1211 actions.LoadBindingOverridesFromJsonInternal(json); 1212 } 1213 } 1214 1215 /// <summary> 1216 /// Restore all binding overrides stored in the given JSON string to the bindings of <paramref name="action"/>. 1217 /// </summary> 1218 /// <param name="action">Action to restore bindings on.</param> 1219 /// <param name="json">A string persisting binding overrides in JSON format. See 1220 /// <see cref="SaveBindingOverridesAsJson(InputAction)"/>.</param> 1221 /// <param name="removeExisting">If true (default), all existing overrides present on the bindings 1222 /// of <paramref name="action"/> will be removed first. If false, existing binding overrides will be left 1223 /// in place but may be overwritten by overrides present in <paramref name="json"/>.</param> 1224 /// <remarks> 1225 /// <example> 1226 /// <code> 1227 /// void SaveUserRebinds(PlayerInput player) 1228 /// { 1229 /// var rebinds = player.actions.SaveBindingOverridesAsJson(); 1230 /// PlayerPrefs.SetString("rebinds", rebinds); 1231 /// } 1232 /// 1233 /// void LoadUserRebinds(PlayerInput player) 1234 /// { 1235 /// var rebinds = PlayerPrefs.GetString("rebinds"); 1236 /// player.actions.LoadBindingOverridesFromJson(rebinds); 1237 /// } 1238 /// </code> 1239 /// </example> 1240 /// 1241 /// Note that this method can also be used with C# wrapper classes generated from .inputactions assets. 1242 /// </remarks> 1243 /// <exception cref="ArgumentNullException"><paramref name="actions"/> is <c>null</c>.</exception> 1244 /// <seealso cref="SaveBindingOverridesAsJson(IInputActionCollection2)"/> 1245 /// <seealso cref="InputBinding.overridePath"/> 1246 public static void LoadBindingOverridesFromJson(this InputAction action, string json, bool removeExisting = true) 1247 { 1248 if (action == null) 1249 throw new ArgumentNullException(nameof(action)); 1250 1251 using (DeferBindingResolution()) 1252 { 1253 if (removeExisting) 1254 action.RemoveAllBindingOverrides(); 1255 1256 action.GetOrCreateActionMap().LoadBindingOverridesFromJsonInternal(json); 1257 } 1258 } 1259 1260 private static void LoadBindingOverridesFromJsonInternal(this IInputActionCollection2 actions, string json) 1261 { 1262 if (string.IsNullOrEmpty(json)) 1263 return; 1264 1265 var overrides = JsonUtility.FromJson<InputActionMap.BindingOverrideListJson>(json); 1266 foreach (var entry in overrides.bindings) 1267 { 1268 // Try to find the binding by ID. 1269 if (!string.IsNullOrEmpty(entry.id)) 1270 { 1271 var bindingIndex = actions.FindBinding(new InputBinding { m_Id = entry.id }, out var action); 1272 if (bindingIndex != -1) 1273 { 1274 action.ApplyBindingOverride(bindingIndex, InputActionMap.BindingOverrideJson.ToBinding(entry)); 1275 continue; 1276 } 1277 } 1278 Debug.LogWarning("Could not override binding as no existing binding was found with the id: " + entry.id); 1279 } 1280 } 1281 1282 ////TODO: allow overwriting magnitude with custom values; maybe turn more into an overall "score" for a control 1283 1284 /// <summary> 1285 /// An ongoing rebinding operation. 1286 /// </summary> 1287 /// <remarks> 1288 /// <example> 1289 /// An example for how to use this class comes with the Input System package in the form of the "Rebinding UI" sample 1290 /// that can be installed from the Package Manager UI in the Unity editor. The sample comes with a reusable <c>RebindActionUI</c> 1291 /// component that also has a dedicated custom inspector. 1292 /// </example> 1293 /// 1294 /// The most convenient way to use this class is by using <see cref="InputActionRebindingExtensions.PerformInteractiveRebinding"/>. 1295 /// This method sets up many default behaviors based on the information found in the given action. 1296 /// 1297 /// Note that instances of this class <em>must</em> be disposed of to not leak memory on the unmanaged heap. 1298 /// 1299 /// <example> 1300 /// <code> 1301 /// using TMPro; 1302 /// using UnityEngine; 1303 /// using UnityEngine.InputSystem; 1304 /// 1305 /// public class RebindButton : MonoBehaviour 1306 /// { 1307 /// 1308 /// // A MonoBehaviour that can be hooked up to a UI.Button control. 1309 /// // This example requires you to set up a Text Mesh Pro text field, 1310 /// // And a UI button which calls the OnClick method in this script. 1311 /// 1312 /// public InputActionReference actionReference; // Reference to an action to rebind. 1313 /// public int bindingIndex; // Index into m_Action.bindings for binding to rebind. 1314 /// public TextMeshProUGUI displayText; // Text in UI that receives the binding display string. 1315 /// private InputActionRebindingExtensions.RebindingOperation rebind; 1316 /// 1317 /// public void OnEnable() 1318 /// { 1319 /// UpdateDisplayText(); 1320 /// } 1321 /// 1322 /// public void OnDisable() 1323 /// { 1324 /// rebind?.Dispose(); 1325 /// } 1326 /// 1327 /// public void OnClick() 1328 /// { 1329 /// var rebind = actionReference.action.PerformInteractiveRebinding().WithTargetBinding(bindingIndex).OnComplete(_ => UpdateDisplayText()); 1330 /// rebind.Start(); 1331 /// } 1332 /// 1333 /// private void UpdateDisplayText() 1334 /// { 1335 /// displayText.text = actionReference.action.GetBindingDisplayString(bindingIndex); 1336 /// } 1337 /// } 1338 /// </code> 1339 /// </example> 1340 /// 1341 /// The goal of a rebind is always to generate a control path (see <see cref="InputControlPath"/>) usable 1342 /// with a binding. By default, the generated path will be installed in <see cref="InputBinding.overridePath"/>. 1343 /// This is non-destructive as the original path is left intact in the form of <see cref="InputBinding.path"/>. 1344 /// 1345 /// This class acts as both a configuration interface for rebinds as well as a controller while 1346 /// the rebind is ongoing. An instance can be reused arbitrary many times. Doing so can avoid allocating 1347 /// additional GC memory (the class internally retains state that it can reuse for multiple rebinds). 1348 /// 1349 /// Note, however, that during rebinding it can be necessary to look at the <see cref="InputControlLayout"/> 1350 /// information registered in the system which means that layouts may have to be loaded. These will be 1351 /// cached for as long as the rebind operation is not disposed of. 1352 /// 1353 /// To reset the configuration of a rebind operation without releasing its memory, call <see cref="Reset"/>. 1354 /// Note that changing configuration while a rebind is in progress in not allowed and will throw 1355 /// <see cref="InvalidOperationException"/>. 1356 /// 1357 /// Note that it is also possible to use this class for selecting controls interactively without also 1358 /// having an <see cref="InputAction"/> or even associated <see cref="InputBinding"/>s. To set this up, 1359 /// configure the rebind accordingly with the respective methods (such as <see cref="WithExpectedControlType{Type}"/>) 1360 /// and use <see cref="OnApplyBinding"/> to intercept the binding override process and instead use custom 1361 /// logic to do something with the resulting path (or to even just use the control list found in <see cref="candidates"/>). 1362 /// </remarks> 1363 /// <seealso cref="InputActionRebindingExtensions.PerformInteractiveRebinding"/> 1364 public sealed class RebindingOperation : IDisposable 1365 { 1366 public const float kDefaultMagnitudeThreshold = 0.2f; 1367 1368 /// <summary> 1369 /// The action that rebinding is being performed on. 1370 /// </summary> 1371 /// <seealso cref="WithAction"/> 1372 public InputAction action => m_ActionToRebind; 1373 1374 /// <summary> 1375 /// Optional mask to determine which bindings to apply overrides to. 1376 /// </summary> 1377 /// <remarks> 1378 /// If this is not null, all bindings that match this mask will have overrides applied to them. 1379 /// </remarks> 1380 public InputBinding? bindingMask => m_BindingMask; 1381 1382 ////REVIEW: exposing this as InputControlList is very misleading as users will not get an error when modifying the list; 1383 //// however, exposing through an interface will lead to boxing... 1384 /// <summary> 1385 /// Controls that had input and were deemed potential matches to rebind to. 1386 /// </summary> 1387 /// <remarks> 1388 /// Controls in the list should be ordered by priority with the first element in the list being 1389 /// considered the best match. 1390 /// </remarks> 1391 /// <seealso cref="AddCandidate"/> 1392 /// <seealso cref="RemoveCandidate"/> 1393 /// <seealso cref="scores"/> 1394 /// <seealso cref="magnitudes"/> 1395 public InputControlList<InputControl> candidates => m_Candidates; 1396 1397 /// <summary> 1398 /// The matching score for each control in <see cref="candidates"/>. 1399 /// </summary> 1400 /// <value>A relative floating-point score for each control in <see cref="candidates"/>.</value> 1401 /// <remarks> 1402 /// Candidates are ranked and sorted by their score. By default, a score is computed for each candidate 1403 /// control automatically. However, this can be overridden using <see cref="OnComputeScore"/>. 1404 /// 1405 /// Default scores are directly based on magnitudes (see <see cref="InputControl.EvaluateMagnitude()"/>). 1406 /// The greater the magnitude of actuation, the greater the score associated with the control. This means, 1407 /// for example, that if both X and Y are actuated on a gamepad stick, the axis with the greater amount 1408 /// of actuation will get scored higher and thus be more likely to get picked. 1409 /// 1410 /// In addition, 1 is added to each default score if the respective control is non-synthetic (see <see 1411 /// cref="InputControl.synthetic"/>). This will give controls that correspond to actual controls present 1412 /// on the device precedence over those added internally. For example, if both are actuated, the synthetic 1413 /// <see cref="Controls.StickControl.up"/> button on stick controls will be ranked lower than the <see 1414 /// cref="Gamepad.buttonSouth"/> which is an actual button on the device. 1415 /// </remarks> 1416 /// <seealso cref="OnComputeScore"/> 1417 /// <seealso cref="candidates"/> 1418 /// <seealso cref="magnitudes"/> 1419 public ReadOnlyArray<float> scores => new ReadOnlyArray<float>(m_Scores, 0, m_Candidates.Count); 1420 1421 /// <summary> 1422 /// The matching control actuation level (see <see cref="InputControl.EvaluateMagnitude()"/> for each control in <see cref="candidates"/>. 1423 /// </summary> 1424 /// <value><see cref="InputControl.EvaluateMagnitude()"/> result for each <see cref="InputControl"/> in <see cref="candidates"/>.</value> 1425 /// <remarks> 1426 /// This array mirrors <see cref="candidates"/>, i.e. each entry corresponds to the entry in <see cref="candidates"/> at 1427 /// the same index. 1428 /// </remarks> 1429 /// <seealso cref="InputControl.EvaluateMagnitude()"/> 1430 /// <seealso cref="candidates"/> 1431 /// <seealso cref="scores"/> 1432 public ReadOnlyArray<float> magnitudes => new ReadOnlyArray<float>(m_Magnitudes, 0, m_Candidates.Count); 1433 1434 /// <summary> 1435 /// The control currently deemed the best candidate. 1436 /// </summary> 1437 /// <value>Primary candidate control at this point.</value> 1438 /// <remarks> 1439 /// If there are no candidates yet, this returns <c>null</c>. If there are candidates, 1440 /// it returns the first element of <see cref="candidates"/> which is always the control 1441 /// with the highest matching score. 1442 /// </remarks> 1443 public InputControl selectedControl 1444 { 1445 get 1446 { 1447 if (m_Candidates.Count == 0) 1448 return null; 1449 1450 return m_Candidates[0]; 1451 } 1452 1453 ////TODO: allow setting this directly from a callback 1454 } 1455 1456 /// <summary> 1457 /// Whether the rebind is currently in progress. 1458 /// </summary> 1459 /// <value>Whether rebind is in progress.</value> 1460 /// <remarks> 1461 /// This is true after calling <see cref="Start"/> and set to false when 1462 /// <see cref="OnComplete"/> or <see cref="OnCancel"/> is called. 1463 /// </remarks> 1464 /// <seealso cref="Start"/> 1465 /// <seealso cref="completed"/> 1466 /// <seealso cref="canceled"/> 1467 public bool started => (m_Flags & Flags.Started) != 0; 1468 1469 /// <summary> 1470 /// Whether the rebind has been completed. 1471 /// </summary> 1472 /// <value>True if the rebind has been completed.</value> 1473 /// <seealso cref="OnComplete(Action{RebindingOperation})"/> 1474 /// <seealso cref="OnComplete"/> 1475 public bool completed => (m_Flags & Flags.Completed) != 0; 1476 1477 /// <summary> 1478 /// Whether the rebind has been cancelled. 1479 /// </summary> 1480 /// <seealso cref="OnCancel"/> 1481 public bool canceled => (m_Flags & Flags.Canceled) != 0; 1482 1483 public double startTime => m_StartTime; 1484 1485 public float timeout => m_Timeout; 1486 1487 /// <summary> 1488 /// Name of the control layout that the rebind is looking for. 1489 /// </summary> 1490 /// <remarks> 1491 /// This is optional but in general, rebinds will be more successful when the operation knows 1492 /// what kind of input it is looking for. 1493 /// 1494 /// If an action is supplied with <see cref="WithAction"/> (automatically done by <see cref="InputActionRebindingExtensions.PerformInteractiveRebinding"/>), 1495 /// the expected control type is automatically set to <see cref="InputAction.expectedControlType"/> or, if that is 1496 /// not set, to <c>"Button"</c> in case the action has type <see cref="InputActionType.Button"/>. 1497 /// 1498 /// If a binding is supplied with <see cref="WithTargetBinding"/> and the binding is a part binding (see <see cref="InputBinding.isPartOfComposite"/>), 1499 /// the expected control type is automatically set to that expected by the respective part of the composite. 1500 /// 1501 /// If this is set, any input on controls that are not of the expected type is ignored. If this is not set, 1502 /// any control that matches all of the other criteria is considered for rebinding. 1503 /// </remarks> 1504 /// <seealso cref="InputControl.layout"/> 1505 /// <seealso cref="InputAction.expectedControlType"/> 1506 public string expectedControlType => m_ExpectedLayout; 1507 1508 /// <summary> 1509 /// Perform rebinding on the bindings of the given action. 1510 /// </summary> 1511 /// <param name="action">Action to perform rebinding on.</param> 1512 /// <returns>The same RebindingOperation instance.</returns> 1513 /// <remarks> 1514 /// Note that by default, a rebind does not have a binding mask or any other setting 1515 /// that constrains which binding the rebind is applied to. This means that if the action 1516 /// has multiple bindings, all of them will have overrides applied to them. 1517 /// 1518 /// To target specific bindings, either set a binding index with <see cref="WithTargetBinding"/>, 1519 /// or set a binding mask with <see cref="WithBindingMask"/> or <see cref="WithBindingGroup"/>. 1520 /// 1521 /// If the action has an associated <see cref="InputAction.expectedControlType"/> set, 1522 /// it will automatically be passed to <see cref="WithExpectedControlType(string)"/>. 1523 /// </remarks> 1524 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception> 1525 /// <exception cref="InvalidOperationException"><paramref name="action"/> is currently enabled.</exception> 1526 /// <seealso cref="PerformInteractiveRebinding"/> 1527 public RebindingOperation WithAction(InputAction action) 1528 { 1529 ThrowIfRebindInProgress(); 1530 1531 if (action == null) 1532 throw new ArgumentNullException(nameof(action)); 1533 if (action.enabled) 1534 throw new InvalidOperationException($"Cannot rebind action '{action}' while it is enabled"); 1535 1536 m_ActionToRebind = action; 1537 1538 // If the action has an associated expected layout, constrain ourselves by it. 1539 // NOTE: We do *NOT* translate this to a control type and constrain by that as a whole chain 1540 // of derived layouts may share the same control type. 1541 if (!string.IsNullOrEmpty(action.expectedControlType)) 1542 WithExpectedControlType(action.expectedControlType); 1543 else if (action.type == InputActionType.Button) 1544 WithExpectedControlType("Button"); 1545 1546 return this; 1547 } 1548 1549 /// <summary> 1550 /// Prevent all input events that have input matching the rebind operation's configuration from reaching 1551 /// its targeted <see cref="InputDevice"/>s and thus taking effect. 1552 /// </summary> 1553 /// <returns>The same RebindingOperation instance.</returns> 1554 /// <remarks> 1555 /// While rebinding interactively, it is usually for the most part undesirable for input to actually have an effect. 1556 /// For example, when rebind gamepad input, pressing the "A" button should not lead to a "submit" action in the UI. 1557 /// For this reason, a rebind can be configured to automatically swallow any input event except the ones having 1558 /// input on controls matching <see cref="WithControlsExcluding"/>. 1559 /// 1560 /// Not at all input necessarily should be suppressed. For example, it can be desirable to have UI that 1561 /// allows the user to cancel an ongoing rebind by clicking with the mouse. This means that mouse position and 1562 /// click input should come through. For this reason, input from controls matching <see cref="WithControlsExcluding"/> 1563 /// is still let through. 1564 /// </remarks> 1565 public RebindingOperation WithMatchingEventsBeingSuppressed(bool value = true) 1566 { 1567 ThrowIfRebindInProgress(); 1568 if (value) 1569 m_Flags |= Flags.SuppressMatchingEvents; 1570 else 1571 m_Flags &= ~Flags.SuppressMatchingEvents; 1572 return this; 1573 } 1574 1575 /// <summary> 1576 /// Set the control path that is matched against actuated controls. 1577 /// </summary> 1578 /// <param name="binding">A control path (see <see cref="InputControlPath"/>) such as <c>"&lt;Keyboard&gt;/escape"</c>.</param> 1579 /// <returns>The same RebindingOperation instance.</returns> 1580 /// <remarks> 1581 /// Note that every rebind operation has only one such path. Calling this method repeatedly will overwrite 1582 /// the path set from prior calls. 1583 /// 1584 /// <code> 1585 /// var rebind = new RebindingOperation(); 1586 /// 1587 /// // Cancel from keyboard escape key. 1588 /// rebind 1589 /// .WithCancelingThrough("&lt;Keyboard&gt;/escape"); 1590 /// 1591 /// // Cancel from any control with "Cancel" usage. 1592 /// // NOTE: This can be dangerous. The control that the wants to bind to may have the "Cancel" 1593 /// // usage assigned to it, thus making it impossible for the user to bind to the control. 1594 /// rebind 1595 /// .WithCancelingThrough("*/{Cancel}"); 1596 /// </code> 1597 /// </remarks> 1598 public RebindingOperation WithCancelingThrough(string binding) 1599 { 1600 ThrowIfRebindInProgress(); 1601 m_CancelBinding = binding; 1602 return this; 1603 } 1604 1605 public RebindingOperation WithCancelingThrough(InputControl control) 1606 { 1607 ThrowIfRebindInProgress(); 1608 if (control == null) 1609 throw new ArgumentNullException(nameof(control)); 1610 return WithCancelingThrough(control.path); 1611 } 1612 1613 public RebindingOperation WithExpectedControlType(string layoutName) 1614 { 1615 ThrowIfRebindInProgress(); 1616 m_ExpectedLayout = new InternedString(layoutName); 1617 return this; 1618 } 1619 1620 public RebindingOperation WithExpectedControlType(Type type) 1621 { 1622 ThrowIfRebindInProgress(); 1623 if (type != null && !typeof(InputControl).IsAssignableFrom(type)) 1624 throw new ArgumentException($"Type '{type.Name}' is not an InputControl", "type"); 1625 m_ControlType = type; 1626 return this; 1627 } 1628 1629 public RebindingOperation WithExpectedControlType<TControl>() 1630 where TControl : InputControl 1631 { 1632 ThrowIfRebindInProgress(); 1633 return WithExpectedControlType(typeof(TControl)); 1634 } 1635 1636 ////TODO: allow targeting bindings by name (i.e. be able to say WithTargetBinding("Left")) 1637 /// <summary> 1638 /// Rebinding a specific <see cref="InputBinding"/> on an <see cref="InputAction"/> as identified 1639 /// by the given index into <see cref="InputAction.bindings"/>. 1640 /// </summary> 1641 /// <param name="bindingIndex">Index into <see cref="InputAction.bindings"/> of the action supplied 1642 /// by <see cref="WithAction"/>.</param> 1643 /// <returns>The same RebindingOperation instance.</returns> 1644 /// <remarks> 1645 /// Note that if the given binding is a part binding of a composite (see <see cref="InputBinding.isPartOfComposite"/>), 1646 /// then the expected control type (see <see cref="WithExpectedControlType(string)"/>) is implicitly changed to 1647 /// match the type of control expected by the given part. If, for example, the composite the part belongs to 1648 /// is a <see cref="Composites.Vector2Composite"/>, then the expected control type is implicitly changed to 1649 /// <see cref="Controls.ButtonControl"/>. 1650 /// 1651 /// <example> 1652 /// <code> 1653 /// // Create an action with a WASD setup. 1654 /// var moveAction = new InputAction(expectedControlType: "Vector2"); 1655 /// moveAction.AddCompositeBinding("2DVector") 1656 /// .With("Up", "&lt;Keyboard&gt;/w") 1657 /// .With("Down", "&lt;Keyboard&gt;/s") 1658 /// .With("Left", "&lt;Keyboard&gt;/a") 1659 /// .With("Right", "&lt;Keyboard&gt;/d"); 1660 /// 1661 /// // Start a rebind of the "Up" binding. 1662 /// moveAction.PerformInteractiveRebinding() 1663 /// .WithTargetBinding(1) 1664 /// .Start(); 1665 /// </code> 1666 /// </example> 1667 /// </remarks> 1668 /// <exception cref="ArgumentOutOfRangeException"><paramref name="bindingIndex"/> is negative.</exception> 1669 /// <seealso cref="WithAction"/> 1670 /// <seealso cref="InputAction.bindings"/> 1671 /// <seealso cref="WithBindingMask"/> 1672 /// <seealso cref="WithBindingGroup"/> 1673 public RebindingOperation WithTargetBinding(int bindingIndex) 1674 { 1675 if (bindingIndex < 0) 1676 throw new ArgumentOutOfRangeException(nameof(bindingIndex)); 1677 1678 m_TargetBindingIndex = bindingIndex; 1679 1680 ////REVIEW: This works nicely with this method but doesn't work as nicely with other means of selecting bindings (by group or mask). 1681 1682 if (m_ActionToRebind != null && bindingIndex < m_ActionToRebind.bindings.Count) 1683 { 1684 var binding = m_ActionToRebind.bindings[bindingIndex]; 1685 1686 // If it's a composite, this also changes the type of the control we're looking for. 1687 if (binding.isPartOfComposite) 1688 { 1689 var composite = m_ActionToRebind.ChangeBinding(bindingIndex).PreviousCompositeBinding().binding.GetNameOfComposite(); 1690 var partName = binding.name; 1691 var expectedLayout = InputBindingComposite.GetExpectedControlLayoutName(composite, partName); 1692 if (!string.IsNullOrEmpty(expectedLayout)) 1693 WithExpectedControlType(expectedLayout); 1694 } 1695 1696 // If the binding is part of a control scheme, only accept controls 1697 // that also match device requirements. 1698 var asset = action.actionMap?.asset; 1699 if (asset != null && !string.IsNullOrEmpty(binding.groups)) 1700 { 1701 foreach (var group in binding.groups.Split(InputBinding.Separator)) 1702 { 1703 var controlSchemeIndex = 1704 asset.controlSchemes.IndexOf(x => group.Equals(x.bindingGroup, StringComparison.InvariantCultureIgnoreCase)); 1705 if (controlSchemeIndex == -1) 1706 continue; 1707 1708 ////TODO: make this deal with and/or requirements 1709 1710 var controlScheme = asset.controlSchemes[controlSchemeIndex]; 1711 foreach (var requirement in controlScheme.deviceRequirements) 1712 WithControlsHavingToMatchPath(requirement.controlPath); 1713 } 1714 } 1715 } 1716 1717 return this; 1718 } 1719 1720 /// <summary> 1721 /// Apply the rebinding to all <see cref="InputAction.bindings"/> of the action given by <see cref="WithAction"/> 1722 /// which are match the given binding mask (see <see cref="InputBinding.Matches"/>). 1723 /// </summary> 1724 /// <param name="bindingMask">A binding mask. See <see cref="InputBinding.Matches"/>.</param> 1725 /// <returns>The same RebindingOperation instance.</returns> 1726 /// <seealso cref="WithBindingGroup"/> 1727 /// <seealso cref="WithTargetBinding"/> 1728 public RebindingOperation WithBindingMask(InputBinding? bindingMask) 1729 { 1730 m_BindingMask = bindingMask; 1731 return this; 1732 } 1733 1734 /// <summary> 1735 /// Apply the rebinding to all <see cref="InputAction.bindings"/> of the action given by <see cref="WithAction"/> 1736 /// which are associated with the given binding group (see <see cref="InputBinding.groups"/>). 1737 /// </summary> 1738 /// <param name="group">A binding group. See <see cref="InputBinding.groups"/>. A binding matches if any of its 1739 /// group associates matches.</param> 1740 /// <returns>The same RebindingOperation instance.</returns> 1741 /// <seealso cref="WithBindingMask"/> 1742 /// <seealso cref="WithTargetBinding"/> 1743 public RebindingOperation WithBindingGroup(string group) 1744 { 1745 return WithBindingMask(new InputBinding {groups = group}); 1746 } 1747 1748 /// <summary> 1749 /// Disable the default behavior of automatically generalizing the path of a selected control. 1750 /// </summary> 1751 /// <returns>The same RebindingOperation instance.</returns> 1752 /// <remarks> 1753 /// At runtime, every <see cref="InputControl"/> has a unique path in the system (<see cref="InputControl.path"/>). 1754 /// However, when performing rebinds, we are not generally interested in the specific runtime path of the 1755 /// control -- which may depend on the number and types of devices present. In fact, most of the time we are not 1756 /// even interested in what particular brand of device the user is rebinding to but rather want to just bind based 1757 /// on the device's broad category. 1758 /// 1759 /// For example, if the user has a DualShock controller and performs an interactive rebind, we usually do not want 1760 /// to generate override paths that reflects that the input specifically came from a DualShock controller. Rather, 1761 /// we're usually interested in the fact that it came from a gamepad. 1762 /// </remarks> 1763 /// <seealso cref="InputBinding.overridePath"/> 1764 /// <seealso cref="OnGeneratePath"/> 1765 public RebindingOperation WithoutGeneralizingPathOfSelectedControl() 1766 { 1767 m_Flags |= Flags.DontGeneralizePathOfSelectedControl; 1768 return this; 1769 } 1770 1771 /// <summary> 1772 /// Instead of applying the generated path as an <see cref="InputBinding.overridePath"/>, 1773 /// create a new binding on the given action (see <see cref="WithAction"/>). 1774 /// </summary> 1775 /// <param name="group">Binding group (see <see cref="InputBinding.groups"/>) to apply to the new binding. 1776 /// This determines, for example, which control scheme (if any) the binding is associated with.</param> 1777 /// <returns></returns> 1778 /// <seealso cref="OnApplyBinding"/> 1779 public RebindingOperation WithRebindAddingNewBinding(string group = null) 1780 { 1781 m_Flags |= Flags.AddNewBinding; 1782 m_BindingGroupForNewBinding = group; 1783 return this; 1784 } 1785 1786 /// <summary> 1787 /// Require actuation of controls to exceed a certain level. 1788 /// </summary> 1789 /// <param name="magnitude">Minimum magnitude threshold that has to be reached on a control 1790 /// for it to be considered a candidate. See <see cref="InputControl.EvaluateMagnitude()"/> for 1791 /// details about magnitude evaluations.</param> 1792 /// <returns>The same RebindingOperation instance.</returns> 1793 /// <exception cref="ArgumentException"><paramref name="magnitude"/> is negative.</exception> 1794 /// <remarks> 1795 /// Rebind operations use a default threshold of 0.2. This means that the actuation level 1796 /// of any control as returned by <see cref="InputControl.EvaluateMagnitude()"/> must be equal 1797 /// or greater than 0.2 for it to be considered a potential candidate. This helps filter out 1798 /// controls that are actuated incidentally as part of actuating other controls. 1799 /// 1800 /// For example, if the player wants to bind an action to the X axis of the gamepad's right 1801 /// stick, the player will almost unavoidably also actuate the Y axis to a certain degree. 1802 /// However, if actuation of the Y axis stays under 2.0, it will automatically get filtered out. 1803 /// 1804 /// Note that the magnitude threshold is not the only mechanism that helps trying to find 1805 /// the most actuated control. In fact, all controls will eventually be sorted by magnitude 1806 /// of actuation so even if both X and Y of a stick make it into the candidate list, if X 1807 /// is actuated more strongly than Y, it will be favored. 1808 /// 1809 /// Note that you can also use this method to <em>lower</em> the default threshold of 0.2 1810 /// in case you want more controls to make it through the matching process. 1811 /// </remarks> 1812 /// <seealso cref="magnitudes"/> 1813 /// <seealso cref="InputControl.EvaluateMagnitude()"/> 1814 public RebindingOperation WithMagnitudeHavingToBeGreaterThan(float magnitude) 1815 { 1816 ThrowIfRebindInProgress(); 1817 if (magnitude < 0) 1818 throw new ArgumentException($"Magnitude has to be positive but was {magnitude}", 1819 nameof(magnitude)); 1820 m_MagnitudeThreshold = magnitude; 1821 return this; 1822 } 1823 1824 /// <summary> 1825 /// Do not ignore input from noisy controls. 1826 /// </summary> 1827 /// <returns>The same RebindingOperation instance.</returns> 1828 /// <remarks> 1829 /// By default, noisy controls are ignored for rebinds. This means that, for example, a gyro 1830 /// inside a gamepad will not be considered as a potential candidate control as it is hard 1831 /// to tell valid user interaction on the control apart from random jittering that occurs 1832 /// on noisy controls. 1833 /// 1834 /// By calling this method, this behavior can be disabled. This is usually only useful when 1835 /// implementing custom candidate selection through <see cref="OnPotentialMatch"/>. 1836 /// </remarks> 1837 /// <seealso cref="InputControl.noisy"/> 1838 public RebindingOperation WithoutIgnoringNoisyControls() 1839 { 1840 ThrowIfRebindInProgress(); 1841 m_Flags |= Flags.DontIgnoreNoisyControls; 1842 return this; 1843 } 1844 1845 /// <summary> 1846 /// Restrict candidate controls using a control path (see <see cref="InputControlPath"/>). 1847 /// </summary> 1848 /// <param name="path">A control path. See <see cref="InputControlPath"/>.</param> 1849 /// <returns>The same RebindingOperation instance.</returns> 1850 /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c> or empty.</exception> 1851 /// <remarks> 1852 /// This method is most useful to, for example, restrict controls to specific types of devices. 1853 /// If, say, you want to let the player only bind to gamepads, you can do so using 1854 /// 1855 /// <example> 1856 /// <code> 1857 /// rebind.WithControlsHavingToMatchPath("&lt;Gamepad&gt;"); 1858 /// </code> 1859 /// </example> 1860 /// 1861 /// This method can be called repeatedly to add multiple paths. The effect is that candidates 1862 /// are accepted if <em>any</em> of the given paths matches. To reset the list, call <see 1863 /// cref="Reset"/>. 1864 /// </remarks> 1865 /// <seealso cref="InputControlPath.Matches"/> 1866 public RebindingOperation WithControlsHavingToMatchPath(string path) 1867 { 1868 ThrowIfRebindInProgress(); 1869 if (string.IsNullOrEmpty(path)) 1870 throw new ArgumentNullException(nameof(path)); 1871 for (var i = 0; i < m_IncludePathCount; ++i) 1872 if (string.Compare(m_IncludePaths[i], path, StringComparison.InvariantCultureIgnoreCase) == 0) 1873 return this; 1874 ArrayHelpers.AppendWithCapacity(ref m_IncludePaths, ref m_IncludePathCount, path); 1875 return this; 1876 } 1877 1878 ////REVIEW: This API has been confusing for users who usually will do something like WithControlsExcluding("Mouse"); find a more intuitive way to do this 1879 /// <summary> 1880 /// Prevent specific controls from being considered as candidate controls. 1881 /// </summary> 1882 /// <param name="path">A control path. See <see cref="InputControlPath"/>.</param> 1883 /// <returns>The same RebindingOperation instance.</returns> 1884 /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c> or empty.</exception> 1885 /// <remarks> 1886 /// Some controls can be undesirable to include in the candidate selection process even 1887 /// though they constitute valid, non-noise user input. For example, in a desktop application, 1888 /// the mouse will usually be used to navigate the UI including a rebinding UI that makes 1889 /// use of RebindingOperation. It can thus be advisable to exclude specific pointer controls 1890 /// like so: 1891 /// 1892 /// <example> 1893 /// <code> 1894 /// rebind 1895 /// .WithControlsExcluding("&lt;Pointer&gt;/position") // Don't bind to mouse position 1896 /// .WithControlsExcluding("&lt;Pointer&gt;/delta") // Don't bind to mouse movement deltas 1897 /// .WithControlsExcluding("&lt;Pointer&gt;/{PrimaryAction}") // don't bind to controls such as leftButton and taps. 1898 /// </code> 1899 /// </example> 1900 /// 1901 /// This method can be called repeatedly to add multiple exclusions. To reset the list, 1902 /// call <see cref="Reset"/>. 1903 /// </remarks> 1904 /// <seealso cref="InputControlPath.Matches"/> 1905 public RebindingOperation WithControlsExcluding(string path) 1906 { 1907 ThrowIfRebindInProgress(); 1908 if (string.IsNullOrEmpty(path)) 1909 throw new ArgumentNullException(nameof(path)); 1910 for (var i = 0; i < m_ExcludePathCount; ++i) 1911 if (string.Compare(m_ExcludePaths[i], path, StringComparison.InvariantCultureIgnoreCase) == 0) 1912 return this; 1913 ArrayHelpers.AppendWithCapacity(ref m_ExcludePaths, ref m_ExcludePathCount, path); 1914 return this; 1915 } 1916 1917 /// <summary> 1918 /// If no match materializes with <paramref name="timeInSeconds"/>, cancel the rebind automatically. 1919 /// </summary> 1920 /// <param name="timeInSeconds">Time in seconds to wait for a successful rebind. Disabled if timeout is less than or equal to 0.</param> 1921 /// <returns>The same RebindingOperation instance.</returns> 1922 /// <remarks> 1923 /// Limiting rebinds by time can be useful in situations where a rebind may potentially put the user in a situation where 1924 /// there is no other way to escape the rebind. For example, if <see cref="WithMatchingEventsBeingSuppressed"/> is engaged, 1925 /// input may be consumed by the rebind and thus not reach the UI if <see cref="WithControlsExcluding"/> has not also been 1926 /// configured accordingly. 1927 /// 1928 /// By default, no timeout is set. 1929 /// </remarks> 1930 /// <seealso cref="timeout"/> 1931 public RebindingOperation WithTimeout(float timeInSeconds) 1932 { 1933 m_Timeout = timeInSeconds; 1934 return this; 1935 } 1936 1937 /// <summary> 1938 /// Delegate to invoke when the rebind completes successfully. 1939 /// </summary> 1940 /// <param name="callback">A delegate to invoke when the rebind is <see cref="completed"/>.</param> 1941 /// <returns>The same RebindingOperation instance.</returns> 1942 /// <remarks> 1943 /// Note that by the time this is invoked, the rebind has been fully applied, that is 1944 /// <see cref="OnApplyBinding"/> has been executed. 1945 /// </remarks> 1946 public RebindingOperation OnComplete(Action<RebindingOperation> callback) 1947 { 1948 m_OnComplete = callback; 1949 return this; 1950 } 1951 1952 /// <summary> 1953 /// Delegate to invoke when the rebind is cancelled instead of completing. This happens when either an 1954 /// input is received from a control explicitly set up to trigger cancellation (see <see cref="WithCancelingThrough(string)"/> 1955 /// and <see cref="WithCancelingThrough(InputControl)"/>) or when <see cref="Cancel"/> is called 1956 /// explicitly. 1957 /// </summary> 1958 /// <param name="callback">Delegate to invoke when the rebind is cancelled.</param> 1959 /// <returns></returns> 1960 /// <seealso cref="WithCancelingThrough(string)"/> 1961 /// <seealso cref="Cancel"/> 1962 /// <seealso cref="canceled"/> 1963 public RebindingOperation OnCancel(Action<RebindingOperation> callback) 1964 { 1965 m_OnCancel = callback; 1966 return this; 1967 } 1968 1969 /// <summary> 1970 /// Delegate to invoke when the rebind has found one or more controls that it considers 1971 /// potential matches. This allows modifying priority of matches or adding or removing 1972 /// matches altogether. 1973 /// </summary> 1974 /// <param name="callback">Callback to invoke when one or more suitable controls have been found.</param> 1975 /// <returns>The same RebindingOperation instance.</returns> 1976 /// <remarks> 1977 /// The matches will be contained in <see cref="candidates"/>. In the callback, you can, 1978 /// for example, alter the contents of the list in order to customize the selection process. 1979 /// You can remove candidates with <see cref="AddCandidate"/> and/or remove candidates 1980 /// with <see cref="RemoveCandidate"/>. 1981 /// </remarks> 1982 /// <seealso cref="candidates"/> 1983 public RebindingOperation OnPotentialMatch(Action<RebindingOperation> callback) 1984 { 1985 m_OnPotentialMatch = callback; 1986 return this; 1987 } 1988 1989 /// <summary> 1990 /// Set function to call when generating the final binding path (see <see cref="InputBinding.path"/>) for a control 1991 /// that has been selected. 1992 /// </summary> 1993 /// <param name="callback">Delegate to call for when to generate a binding path.</param> 1994 /// <returns>The same RebindingOperation instance.</returns> 1995 /// <remarks> 1996 /// A rebind will by default create a path that it deems most useful for the purpose of rebinding. However, this 1997 /// logic may be undesirable for your use case. By supplying a custom callback you can bypass this logic and thus replace it. 1998 /// 1999 /// When a matching control is singled out, the default logic will look for the device that introduces the given 2000 /// control. For example, if the A button is pressed on an Xbox gamepad, the resulting path will be <c>"&lt;Gamepad&gt;/buttonSouth"</c> 2001 /// as it is the <see cref="Gamepad"/> device that introduces the south face button on gamepads. Thus, the binding will work 2002 /// with any other gamepad, not just the Xbox controller. 2003 /// 2004 /// If the delegate returns a null or empty string, the default logic will be re-engaged. 2005 /// </remarks> 2006 /// <seealso cref="InputBinding.path"/> 2007 /// <seealso cref="WithoutGeneralizingPathOfSelectedControl"/> 2008 public RebindingOperation OnGeneratePath(Func<InputControl, string> callback) 2009 { 2010 m_OnGeneratePath = callback; 2011 return this; 2012 } 2013 2014 /// <summary> 2015 /// Delegate to invoke for compute the matching score for a candidate control. 2016 /// </summary> 2017 /// <param name="callback">A delegate that computes matching scores.</param> 2018 /// <returns>The same RebindingOperation instance.</returns> 2019 /// <remarks> 2020 /// By default, the actuation level of a control is used as its matching score. For a <see cref="Controls.StickControl"/>, 2021 /// for example, the vector magnitude of the control will be its score. So, a stick that is actuated just a little 2022 /// will have a lower score than a stick that is actuated to maximum extent in one direction. 2023 /// 2024 /// The control with the highest score will be the one appearing at index 0 in <see cref="candidates"/> and thus 2025 /// will be the control picked by the rebind as the top candidate. 2026 /// 2027 /// By installing a custom delegate, it is possible to customize the scoring and apply custom logic to boost 2028 /// or lower scores of controls. 2029 /// 2030 /// The first argument to the delegate is the control that is being added to <see cref="candidates"/> and the 2031 /// second argument is a pointer to the input event that contains an input on the control. 2032 /// </remarks> 2033 /// <seealso cref="scores"/> 2034 /// <seealso cref="candidates"/> 2035 public RebindingOperation OnComputeScore(Func<InputControl, InputEventPtr, float> callback) 2036 { 2037 m_OnComputeScore = callback; 2038 return this; 2039 } 2040 2041 /// <summary> 2042 /// Apply a generated binding <see cref="InputBinding.path"/> as the final step to complete a rebind. 2043 /// </summary> 2044 /// <param name="callback">Delegate to invoke in order to the apply the generated binding path.</param> 2045 /// <returns>The same RebindingOperation instance.</returns> 2046 /// <remarks> 2047 /// Once a binding path has been generated (see <see cref="OnGeneratePath"/>) from a candidate control, 2048 /// the last step is to apply the path. The default logic will take the supplied action (see <see cref="WithAction"/>) 2049 /// and apply the path as an <see cref="InputBinding.overridePath"/> on all bindings that have been selected 2050 /// for rebinding with <see cref="WithTargetBinding"/>, <see cref="WithBindingMask"/>, or <see cref="WithBindingGroup"/>. 2051 /// 2052 /// To customize this process, you can supply a custom delegate via this method. If you do so, the default 2053 /// logic is bypassed and the step left entirely to the delegate. This also makes it possible to use 2054 /// rebind operations without even having an action or even <see cref="InputBinding"/>s. 2055 /// </remarks> 2056 public RebindingOperation OnApplyBinding(Action<RebindingOperation, string> callback) 2057 { 2058 m_OnApplyBinding = callback; 2059 return this; 2060 } 2061 2062 /// <summary> 2063 /// If a successful match has been found, wait for the given time for a better match to appear before 2064 /// committing to the match. 2065 /// </summary> 2066 /// <param name="seconds">Time in seconds to wait until committing to a match.</param> 2067 /// <returns>The same RebindingOperation instance.</returns> 2068 /// <remarks> 2069 /// While this adds a certain amount of lag to the operation, the lag is not really perceptible if the timeout 2070 /// is kept short. 2071 /// 2072 /// What this helps with is controls such as sticks where, when moved out of the deadzone, the initial direction 2073 /// that the user presses may not be the one actually intended. For example, the user may be pressing slightly 2074 /// more in the X direction before finally very clearly going more strongly in the Y direction. If the rebind 2075 /// does not wait for a bit but instead takes the first actuation as is, the rebind may appear overly brittle. 2076 /// 2077 /// An alternative to timeouts is to set higher magnitude thresholds with <see cref="WithMagnitudeHavingToBeGreaterThan"/>. 2078 /// The default threshold is 0.2f. By setting it to 0.6f or even higher, timeouts may be unnecessary. 2079 /// </remarks> 2080 public RebindingOperation OnMatchWaitForAnother(float seconds) 2081 { 2082 m_WaitSecondsAfterMatch = seconds; 2083 return this; 2084 } 2085 2086 /// <summary> 2087 /// Start the rebinding. This should be invoked after the rebind operation has been fully configured. 2088 /// </summary> 2089 /// <returns>The same RebindingOperation instance.</returns> 2090 /// <exception cref="InvalidOperationException">The rebind has been configure incorrectly. For example, no action has 2091 /// been given but no <see cref="OnApplyBinding"/> callback has been installed either.</exception> 2092 /// <seealso cref="Cancel"/> 2093 /// <seealso cref="Dispose"/> 2094 public RebindingOperation Start() 2095 { 2096 // Ignore if already started. 2097 if (started) 2098 return this; 2099 2100 // Make sure our configuration is sound. 2101 if (m_ActionToRebind != null && m_ActionToRebind.bindings.Count == 0 && (m_Flags & Flags.AddNewBinding) == 0) 2102 throw new InvalidOperationException( 2103 $"Action '{action}' must have at least one existing binding or must be used with WithRebindingAddNewBinding()"); 2104 if (m_ActionToRebind == null && m_OnApplyBinding == null) 2105 throw new InvalidOperationException( 2106 "Must either have an action (call WithAction()) to apply binding to or have a custom callback to apply the binding (call OnApplyBinding())"); 2107 2108 m_StartTime = InputState.currentTime; 2109 2110 if (m_WaitSecondsAfterMatch > 0 || m_Timeout > 0) 2111 { 2112 HookOnAfterUpdate(); 2113 m_LastMatchTime = -1; 2114 } 2115 2116 HookOnEvent(); 2117 2118 m_Flags |= Flags.Started; 2119 m_Flags &= ~Flags.Canceled; 2120 m_Flags &= ~Flags.Completed; 2121 2122 return this; 2123 } 2124 2125 /// <summary> 2126 /// Cancel an ongoing rebind. This will invoke the callback supplied by <see cref="OnCancel"/> (if any). 2127 /// </summary> 2128 /// <seealso cref="Start"/> 2129 /// <see cref="started"/> 2130 public void Cancel() 2131 { 2132 if (!started) 2133 return; 2134 2135 OnCancel(); 2136 } 2137 2138 /// <summary> 2139 /// Manually complete the rebinding operation. 2140 /// </summary> 2141 public void Complete() 2142 { 2143 if (!started) 2144 return; 2145 2146 OnComplete(); 2147 } 2148 2149 /// <summary> 2150 /// Add a candidate to <see cref="candidates"/>. This will also add values to <see cref="scores"/> and 2151 /// <see cref="magnitudes"/>. If the control has already been added, it's values are simply updated based 2152 /// on the given arguments. 2153 /// </summary> 2154 /// <param name="control">A control that is meant to be considered as a candidate for the rebind.</param> 2155 /// <param name="score">The score to associate with the control (see <see cref="scores"/>). By default, the control with the highest 2156 /// score will be picked by the rebind.</param> 2157 /// <param name="magnitude">Actuation level of the control to enter into <see cref="magnitudes"/>.</param> 2158 /// <exception cref="ArgumentNullException"><paramref name="control"/> is <c>null</c>.</exception> 2159 /// <seealso cref="RemoveCandidate"/> 2160 public void AddCandidate(InputControl control, float score, float magnitude = -1) 2161 { 2162 if (control == null) 2163 throw new ArgumentNullException(nameof(control)); 2164 2165 // If it's already added, update score. 2166 var index = m_Candidates.IndexOf(control); 2167 if (index != -1) 2168 { 2169 m_Scores[index] = score; 2170 } 2171 else 2172 { 2173 // Otherwise, add it. 2174 var scoreCount = m_Candidates.Count; 2175 var magnitudeCount = m_Candidates.Count; 2176 m_Candidates.Add(control); 2177 ArrayHelpers.AppendWithCapacity(ref m_Scores, ref scoreCount, score); 2178 ArrayHelpers.AppendWithCapacity(ref m_Magnitudes, ref magnitudeCount, magnitude); 2179 } 2180 2181 SortCandidatesByScore(); 2182 } 2183 2184 /// <summary> 2185 /// Remove a control from the list of <see cref="candidates"/>. This also removes its entries from 2186 /// <see cref="scores"/> and <see cref="magnitudes"/>. 2187 /// </summary> 2188 /// <param name="control">Control to remove from <see cref="candidates"/>.</param> 2189 /// <exception cref="ArgumentNullException"><paramref name="control"/> is <c>null</c>.</exception> 2190 /// <seealso cref="AddCandidate"/> 2191 public void RemoveCandidate(InputControl control) 2192 { 2193 if (control == null) 2194 throw new ArgumentNullException(nameof(control)); 2195 2196 var index = m_Candidates.IndexOf(control); 2197 if (index == -1) 2198 return; 2199 2200 var candidateCount = m_Candidates.Count; 2201 m_Candidates.RemoveAt(index); 2202 ArrayHelpers.EraseAtWithCapacity(m_Scores, ref candidateCount, index); 2203 } 2204 2205 /// <summary> 2206 /// Release all memory held by the option, especially unmanaged memory which will not otherwise 2207 /// be freed. 2208 /// </summary> 2209 public void Dispose() 2210 { 2211 UnhookOnEvent(); 2212 UnhookOnAfterUpdate(); 2213 m_Candidates.Dispose(); 2214 m_LayoutCache.Clear(); 2215 } 2216 2217 ~RebindingOperation() 2218 { 2219 Dispose(); 2220 } 2221 2222 /// <summary> 2223 /// Reset the configuration on the rebind. 2224 /// </summary> 2225 /// <returns>The same RebindingOperation instance.</returns> 2226 /// <remarks> 2227 /// Call this method to reset the effects of calling methods such as <see cref="WithAction"/>, 2228 /// <see cref="WithBindingGroup"/>, etc. but retain other data that the rebind operation 2229 /// may have allocated already. If you are reusing the same <c>RebindingOperation</c> 2230 /// multiple times, a good strategy is to reset and reconfigure the operation before starting 2231 /// it again. 2232 /// </remarks> 2233 public RebindingOperation Reset() 2234 { 2235 Cancel(); 2236 m_ActionToRebind = default; 2237 m_BindingMask = default; 2238 m_ControlType = default; 2239 m_ExpectedLayout = default; 2240 m_IncludePathCount = default; 2241 m_ExcludePathCount = default; 2242 m_TargetBindingIndex = -1; 2243 m_BindingGroupForNewBinding = default; 2244 m_CancelBinding = default; 2245 m_MagnitudeThreshold = kDefaultMagnitudeThreshold; 2246 m_Timeout = default; 2247 m_WaitSecondsAfterMatch = default; 2248 m_Flags = default; 2249 m_StartingActuations?.Clear(); 2250 return this; 2251 } 2252 2253 private void HookOnEvent() 2254 { 2255 if ((m_Flags & Flags.OnEventHooked) != 0) 2256 return; 2257 2258 if (m_OnEventDelegate == null) 2259 m_OnEventDelegate = OnEvent; 2260 2261 InputSystem.onEvent += m_OnEventDelegate; 2262 m_Flags |= Flags.OnEventHooked; 2263 } 2264 2265 private void UnhookOnEvent() 2266 { 2267 if ((m_Flags & Flags.OnEventHooked) == 0) 2268 return; 2269 2270 InputSystem.onEvent -= m_OnEventDelegate; 2271 m_Flags &= ~Flags.OnEventHooked; 2272 } 2273 2274 private unsafe void OnEvent(InputEventPtr eventPtr, InputDevice device) 2275 { 2276 // Ignore if not a state event. 2277 var eventType = eventPtr.type; 2278 if (eventType != StateEvent.Type && eventType != DeltaStateEvent.Type) 2279 return; 2280 2281 ////TODO: add callback that shows the candidate *and* the event to the user (this is particularly useful when we are suppressing 2282 //// and thus throwing away events) 2283 2284 // Go through controls in the event and see if there's anything interesting. 2285 // NOTE: We go through quite a few steps and operations here. However, the chief goal here is trying to be as robust 2286 // as we can in isolating the control the user really means to single out. If this code here does its job, that 2287 // control should always pop up as the first entry in the candidates list (if the configuration of the rebind 2288 // operation is otherwise sane). 2289 var haveChangedCandidates = false; 2290 var suppressEvent = false; 2291 var controlEnumerationFlags = 2292 InputControlExtensions.Enumerate.IncludeNonLeafControls 2293 | InputControlExtensions.Enumerate.IncludeSyntheticControls; 2294 if ((m_Flags & Flags.DontIgnoreNoisyControls) != 0) 2295 controlEnumerationFlags |= InputControlExtensions.Enumerate.IncludeNoisyControls; 2296 foreach (var control in eventPtr.EnumerateControls(controlEnumerationFlags, device)) 2297 { 2298 var statePtr = control.GetStatePtrFromStateEventUnchecked(eventPtr, eventType); 2299 Debug.Assert(statePtr != null, "If EnumerateControls() returns a control, GetStatePtrFromStateEvent should not return null for it"); 2300 2301 // If the control that cancels has been actuated, abort the operation now. 2302 if (!string.IsNullOrEmpty(m_CancelBinding) && InputControlPath.Matches(m_CancelBinding, control) && 2303 control.HasValueChangeInState(statePtr)) 2304 { 2305 OnCancel(); 2306 break; 2307 } 2308 2309 // If controls must not match certain paths, make sure the control doesn't. 2310 if (m_ExcludePathCount > 0 && HavePathMatch(control, m_ExcludePaths, m_ExcludePathCount)) 2311 continue; 2312 2313 // If controls have to match a certain path, check if this one does. 2314 if (m_IncludePathCount > 0 && !HavePathMatch(control, m_IncludePaths, m_IncludePathCount)) 2315 continue; 2316 2317 // If we're expecting controls of a certain type, skip if control isn't of 2318 // the right type. 2319 if (m_ControlType != null && !m_ControlType.IsInstanceOfType(control)) 2320 continue; 2321 2322 // If we're expecting controls to be based on a specific layout, skip if control 2323 // isn't based on that layout. 2324 if (!m_ExpectedLayout.IsEmpty() && 2325 m_ExpectedLayout != control.m_Layout && 2326 !InputControlLayout.s_Layouts.IsBasedOn(m_ExpectedLayout, control.m_Layout)) 2327 continue; 2328 2329 ////REVIEW: shouldn't we generally require any already actuated control to go back to 0 actuation before considering it for a rebind? 2330 2331 // Skip controls that are in their default state. 2332 // NOTE: This is the cheapest check with respect to looking at actual state. So 2333 // do this first before looking further at the state. 2334 if (control.CheckStateIsAtDefault(statePtr)) 2335 { 2336 // For controls that were already actuated when we started the rebind, we record starting actuations below. 2337 // However, when such a control goes back to default state, we want to reset that recorded value. This makes 2338 // sure that if, for example, a key is down when the rebind started, when the key is released and then pressed 2339 // again, we don't compare to the previously recorded magnitude of 1 but rather to 0. 2340 if (!m_StartingActuations.ContainsKey(control)) 2341 // ...but we also need to record the first time this control appears in it's default state for the case where 2342 // the user is holding a discrete control when rebinding starts. On the first release, we'll record here a 2343 // starting actuation of 0, then when the key is pressed again, the code below will successfully compare the 2344 // starting value of 0 to the pressed value of 1. If we didn't set this to zero on release, the user would 2345 // have to release the key, press and release again, and on the next press, it would register as actuated. 2346 m_StartingActuations.Add(control, 0); 2347 2348 m_StartingActuations[control] = 0; 2349 2350 continue; 2351 } 2352 2353 // At this point the control is a potential candidate for rebinding and therefore the event may need to be suppressed, if that's enabled. 2354 suppressEvent = true; 2355 2356 var magnitude = control.EvaluateMagnitude(statePtr); 2357 if (magnitude >= 0) 2358 { 2359 // Determine starting actuation. 2360 if (m_StartingActuations.TryGetValue(control, out var startingMagnitude) == false) 2361 { 2362 // Haven't seen this control changing actuation yet. Record its current actuation as its 2363 // starting actuation and ignore the control if we haven't reached our actuation threshold yet. 2364 startingMagnitude = control.magnitude; 2365 m_StartingActuations.Add(control, startingMagnitude); 2366 } 2367 2368 // Ignore control if it hasn't exceeded the magnitude threshold relative to its starting actuation yet. 2369 if (Mathf.Abs(startingMagnitude - magnitude) < m_MagnitudeThreshold) 2370 continue; 2371 } 2372 2373 ////REVIEW: this would be more useful by providing the default score *to* the callback (which may alter it or just replace it altogether) 2374 // Compute score. 2375 float score; 2376 if (m_OnComputeScore != null) 2377 { 2378 score = m_OnComputeScore(control, eventPtr); 2379 } 2380 else 2381 { 2382 score = magnitude; 2383 2384 // We don't want synthetic controls to not be bindable at all but they should 2385 // generally cede priority to controls that aren't synthetic. So we bump all 2386 // scores of controls that aren't synthetic. 2387 if (!control.synthetic) 2388 score += 1f; 2389 } 2390 2391 // Control is a candidate. 2392 // See if we already singled the control out as a potential candidate. 2393 var candidateIndex = m_Candidates.IndexOf(control); 2394 if (candidateIndex != -1) 2395 { 2396 // Yes, we did. So just check whether it became a better candidate than before. 2397 if (m_Scores[candidateIndex] < score) 2398 { 2399 haveChangedCandidates = true; 2400 m_Scores[candidateIndex] = score; 2401 2402 if (m_WaitSecondsAfterMatch > 0) 2403 m_LastMatchTime = InputState.currentTime; 2404 } 2405 } 2406 else 2407 { 2408 // No, so add it. 2409 var scoreCount = m_Candidates.Count; 2410 var magnitudeCount = m_Candidates.Count; 2411 m_Candidates.Add(control); 2412 ArrayHelpers.AppendWithCapacity(ref m_Scores, ref scoreCount, score); 2413 ArrayHelpers.AppendWithCapacity(ref m_Magnitudes, ref magnitudeCount, magnitude); 2414 haveChangedCandidates = true; 2415 2416 if (m_WaitSecondsAfterMatch > 0) 2417 m_LastMatchTime = InputState.currentTime; 2418 } 2419 } 2420 2421 // See if we should suppress the event. If so, mark it handled so that the input manager 2422 // will skip further processing of the event. 2423 if (suppressEvent && (m_Flags & Flags.SuppressMatchingEvents) != 0) 2424 eventPtr.handled = true; 2425 2426 if (haveChangedCandidates && !canceled) 2427 { 2428 // If we have a callback that wants to control matching, leave it to the callback to decide 2429 // whether the rebind is complete or not. Otherwise, just complete. 2430 if (m_OnPotentialMatch != null) 2431 { 2432 SortCandidatesByScore(); 2433 m_OnPotentialMatch(this); 2434 } 2435 else if (m_WaitSecondsAfterMatch <= 0) 2436 { 2437 OnComplete(); 2438 } 2439 else 2440 { 2441 SortCandidatesByScore(); 2442 } 2443 } 2444 } 2445 2446 private void SortCandidatesByScore() 2447 { 2448 var candidateCount = m_Candidates.Count; 2449 if (candidateCount <= 1) 2450 return; 2451 2452 // Simple insertion sort that sorts both m_Candidates and m_Scores at the same time. 2453 // Note that we're sorting by *decreasing* score here, not by increasing score. 2454 for (var i = 1; i < candidateCount; ++i) 2455 { 2456 for (var j = i; j > 0 && m_Scores[j - 1] < m_Scores[j]; --j) 2457 { 2458 var k = j - 1; 2459 m_Scores.SwapElements(j, k); 2460 m_Candidates.SwapElements(j, k); 2461 m_Magnitudes.SwapElements(j, k); 2462 } 2463 } 2464 } 2465 2466 private static bool HavePathMatch(InputControl control, string[] paths, int pathCount) 2467 { 2468 for (var i = 0; i < pathCount; ++i) 2469 { 2470 if (InputControlPath.MatchesPrefix(paths[i], control)) 2471 return true; 2472 } 2473 2474 return false; 2475 } 2476 2477 private void HookOnAfterUpdate() 2478 { 2479 if ((m_Flags & Flags.OnAfterUpdateHooked) != 0) 2480 return; 2481 2482 if (m_OnAfterUpdateDelegate == null) 2483 m_OnAfterUpdateDelegate = OnAfterUpdate; 2484 2485 InputSystem.onAfterUpdate += m_OnAfterUpdateDelegate; 2486 m_Flags |= Flags.OnAfterUpdateHooked; 2487 } 2488 2489 private void UnhookOnAfterUpdate() 2490 { 2491 if ((m_Flags & Flags.OnAfterUpdateHooked) == 0) 2492 return; 2493 2494 InputSystem.onAfterUpdate -= m_OnAfterUpdateDelegate; 2495 m_Flags &= ~Flags.OnAfterUpdateHooked; 2496 } 2497 2498 private void OnAfterUpdate() 2499 { 2500 // If we don't have a match yet but we have a timeout and have expired it, 2501 // cancel the operation. 2502 if (m_LastMatchTime < 0 && m_Timeout > 0 && 2503 InputState.currentTime - m_StartTime > m_Timeout) 2504 { 2505 Cancel(); 2506 return; 2507 } 2508 2509 // Sanity check to make sure we're actually waiting for completion. 2510 if (m_WaitSecondsAfterMatch <= 0) 2511 return; 2512 2513 // Can't complete if we have no match yet. 2514 if (m_LastMatchTime < 0) 2515 return; 2516 2517 // Complete if timeout has expired. 2518 if (InputState.currentTime >= m_LastMatchTime + m_WaitSecondsAfterMatch) 2519 Complete(); 2520 } 2521 2522 private void OnComplete() 2523 { 2524 SortCandidatesByScore(); 2525 2526 if (m_Candidates.Count > 0) 2527 { 2528 // Create a path from the selected control. 2529 var selectedControl = m_Candidates[0]; 2530 var path = selectedControl.path; 2531 if (m_OnGeneratePath != null) 2532 { 2533 // We have a callback. Give it a shot to generate a path. If it doesn't, 2534 // fall back to our default logic. 2535 var newPath = m_OnGeneratePath(selectedControl); 2536 if (!string.IsNullOrEmpty(newPath)) 2537 path = newPath; 2538 else if ((m_Flags & Flags.DontGeneralizePathOfSelectedControl) == 0) 2539 path = GeneratePathForControl(selectedControl); 2540 } 2541 else if ((m_Flags & Flags.DontGeneralizePathOfSelectedControl) == 0) 2542 path = GeneratePathForControl(selectedControl); 2543 2544 // If we have a custom callback for applying the binding, let it handle 2545 // everything. 2546 if (m_OnApplyBinding != null) 2547 m_OnApplyBinding(this, path); 2548 else 2549 { 2550 Debug.Assert(m_ActionToRebind != null); 2551 2552 // See if we should modify an existing binding or create a new one. 2553 if ((m_Flags & Flags.AddNewBinding) != 0) 2554 { 2555 // Create new binding. 2556 m_ActionToRebind.AddBinding(path, groups: m_BindingGroupForNewBinding); 2557 } 2558 else 2559 { 2560 // Apply binding override to existing binding. 2561 if (m_TargetBindingIndex >= 0) 2562 { 2563 if (m_TargetBindingIndex >= m_ActionToRebind.bindings.Count) 2564 throw new InvalidOperationException( 2565 $"Target binding index {m_TargetBindingIndex} out of range for action '{m_ActionToRebind}' with {m_ActionToRebind.bindings.Count} bindings"); 2566 2567 m_ActionToRebind.ApplyBindingOverride(m_TargetBindingIndex, path); 2568 } 2569 else if (m_BindingMask != null) 2570 { 2571 var bindingOverride = m_BindingMask.Value; 2572 bindingOverride.overridePath = path; 2573 m_ActionToRebind.ApplyBindingOverride(bindingOverride); 2574 } 2575 else 2576 { 2577 m_ActionToRebind.ApplyBindingOverride(path); 2578 } 2579 } 2580 } 2581 } 2582 2583 // Complete. 2584 m_Flags |= Flags.Completed; 2585 m_OnComplete?.Invoke(this); 2586 2587 ResetAfterMatchCompleted(); 2588 } 2589 2590 private void OnCancel() 2591 { 2592 m_Flags |= Flags.Canceled; 2593 2594 m_OnCancel?.Invoke(this); 2595 2596 ResetAfterMatchCompleted(); 2597 } 2598 2599 private void ResetAfterMatchCompleted() 2600 { 2601 m_Flags &= ~Flags.Started; 2602 m_Candidates.Clear(); 2603 m_Candidates.Capacity = 0; // Release our unmanaged memory. 2604 m_StartTime = -1; 2605 m_StartingActuations.Clear(); 2606 2607 UnhookOnEvent(); 2608 UnhookOnAfterUpdate(); 2609 } 2610 2611 private void ThrowIfRebindInProgress() 2612 { 2613 if (started) 2614 throw new InvalidOperationException("Cannot reconfigure rebinding while operation is in progress"); 2615 } 2616 2617 ////TODO: this *must* be publicly accessible 2618 /// <summary> 2619 /// Based on the chosen control, generate an override path to rebind to. 2620 /// </summary> 2621 private string GeneratePathForControl(InputControl control) 2622 { 2623 var device = control.device; 2624 Debug.Assert(control != device, "Control must not be a device"); 2625 2626 var deviceLayoutName = 2627 InputControlLayout.s_Layouts.FindLayoutThatIntroducesControl(control, m_LayoutCache); 2628 2629 if (m_PathBuilder == null) 2630 m_PathBuilder = new StringBuilder(); 2631 else 2632 m_PathBuilder.Length = 0; 2633 2634 control.BuildPath(deviceLayoutName, m_PathBuilder); 2635 2636 return m_PathBuilder.ToString(); 2637 } 2638 2639 private InputAction m_ActionToRebind; 2640 private InputBinding? m_BindingMask; 2641 private Type m_ControlType; 2642 private InternedString m_ExpectedLayout; 2643 private int m_IncludePathCount; 2644 private string[] m_IncludePaths; 2645 private int m_ExcludePathCount; 2646 private string[] m_ExcludePaths; 2647 private int m_TargetBindingIndex = -1; 2648 private string m_BindingGroupForNewBinding; 2649 private string m_CancelBinding; 2650 private float m_MagnitudeThreshold = kDefaultMagnitudeThreshold; 2651 private float[] m_Scores; // Scores for the controls in m_Candidates. 2652 private float[] m_Magnitudes; 2653 private double m_LastMatchTime; // Last input event time we discovered a better match. 2654 private double m_StartTime; 2655 private float m_Timeout; 2656 private float m_WaitSecondsAfterMatch; 2657 private InputControlList<InputControl> m_Candidates; 2658 private Action<RebindingOperation> m_OnComplete; 2659 private Action<RebindingOperation> m_OnCancel; 2660 private Action<RebindingOperation> m_OnPotentialMatch; 2661 private Func<InputControl, string> m_OnGeneratePath; 2662 private Func<InputControl, InputEventPtr, float> m_OnComputeScore; 2663 private Action<RebindingOperation, string> m_OnApplyBinding; 2664 private Action<InputEventPtr, InputDevice> m_OnEventDelegate; 2665 private Action m_OnAfterUpdateDelegate; 2666 ////TODO: use global cache 2667 private InputControlLayout.Cache m_LayoutCache; 2668 private StringBuilder m_PathBuilder; 2669 private Flags m_Flags; 2670 2671 // Controls may already be actuated by the time we start a rebind. For those, we track starting actuations 2672 // individually and require them to cross the actuation threshold WRT the starting actuation. 2673 private Dictionary<InputControl, float> m_StartingActuations = new Dictionary<InputControl, float>(); 2674 2675 [Flags] 2676 private enum Flags 2677 { 2678 Started = 1 << 0, 2679 Completed = 1 << 1, 2680 Canceled = 1 << 2, 2681 OnEventHooked = 1 << 3, 2682 OnAfterUpdateHooked = 1 << 4, 2683 DontIgnoreNoisyControls = 1 << 6, 2684 DontGeneralizePathOfSelectedControl = 1 << 7, 2685 AddNewBinding = 1 << 8, 2686 SuppressMatchingEvents = 1 << 9, 2687 } 2688 } 2689 2690 /// <summary> 2691 /// Initiate an operation that interactively rebinds the given action based on received input. 2692 /// </summary> 2693 /// <param name="action">Action to perform rebinding on.</param> 2694 /// <param name="bindingIndex">Optional index (within the <see cref="InputAction.bindings"/> array of <paramref name="action"/>) 2695 /// of binding to perform rebinding on. Must not be a composite binding.</param> 2696 /// <returns>A rebind operation configured to perform the rebind.</returns> 2697 /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception> 2698 /// <exception cref="ArgumentOutOfRangeException"><paramref name="bindingIndex"/> is not a valid index.</exception> 2699 /// <exception cref="InvalidOperationException">The binding at <paramref name="bindingIndex"/> is a composite binding.</exception> 2700 /// <remarks> 2701 /// This method will automatically perform a set of configuration on the <see cref="RebindingOperation"/> 2702 /// based on the action and, if specified, binding. In particular, it will apply the following default 2703 /// configuration: 2704 /// 2705 /// <ul> 2706 /// <li><see cref="RebindingOperation.WithAction"/> will be called with <paramref name="action"/></li> 2707 /// <li>The default timeout will be set to 0.05f seconds with <see cref="RebindingOperation.OnMatchWaitForAnother"/>.</li> 2708 /// <li>Pointer <see cref="Pointer.delta"/> and <see cref="Pointer.position"/> as well as touch <see cref="Controls.TouchControl.position"/> 2709 /// and <see cref="Controls.TouchControl.delta"/> controls will be excluded with <see cref="RebindingOperation.WithControlsExcluding"/>. 2710 /// This prevents mouse movement or touch leading to rebinds as it will generally be used to operate the UI.</li> 2711 /// <li><see cref="RebindingOperation.WithMatchingEventsBeingSuppressed"/> will be invoked to suppress input funneled into rebinds 2712 /// from being picked up elsewhere.</li> 2713 /// <li>Except if the rebind is looking for a button, <see cref="Keyboard.escapeKey"/> will be set up to cancel the rebind 2714 /// using <see cref="RebindingOperation.WithCancelingThrough(string)"/>.</li> 2715 /// <li>If <paramref name="bindingIndex"/> is given, <see cref="RebindingOperation.WithTargetBinding"/> is invoked to 2716 /// target the given binding with the rebind.</li> 2717 /// </ul> 2718 /// 2719 /// Note that rebind operations must be disposed of once finished in order to not leak memory. 2720 /// 2721 /// <example> 2722 /// <code> 2723 /// // Target the first binding in the gamepad scheme. 2724 /// var bindingIndex = myAction.GetBindingIndex(InputBinding.MaskByGroup("Gamepad")); 2725 /// var rebind = myAction.PerformInteractiveRebinding(bindingIndex); 2726 /// 2727 /// // Dispose the operation on completion. 2728 /// rebind.OnComplete( 2729 /// operation => 2730 /// { 2731 /// Debug.Log($"Rebound '{myAction}' to '{operation.selectedControl}'"); 2732 /// operation.Dispose(); 2733 /// }; 2734 /// 2735 /// // Start the rebind. This will cause the rebind operation to start running in the 2736 /// // background listening for input. 2737 /// rebind.Start(); 2738 /// </code> 2739 /// </example> 2740 /// </remarks> 2741 public static RebindingOperation PerformInteractiveRebinding(this InputAction action, int bindingIndex = -1) 2742 { 2743 if (action == null) 2744 throw new ArgumentNullException(nameof(action)); 2745 2746 var rebind = new RebindingOperation() 2747 .WithAction(action) 2748 // Give it an ever so slight delay to make sure there isn't a better match immediately 2749 // following the current event. 2750 .OnMatchWaitForAnother(0.05f) 2751 // It doesn't really make sense to interactively bind pointer position input as interactive 2752 // rebinds are usually initiated from UIs which are operated by pointers. So exclude pointer 2753 // position controls by default. 2754 .WithControlsExcluding("<Pointer>/delta") 2755 .WithControlsExcluding("<Pointer>/position") 2756 .WithControlsExcluding("<Touchscreen>/touch*/position") 2757 .WithControlsExcluding("<Touchscreen>/touch*/delta") 2758 .WithControlsExcluding("<Mouse>/clickCount") 2759 .WithMatchingEventsBeingSuppressed(); 2760 2761 // If we're not looking for a button, automatically add keyboard escape key to abort rebind. 2762 if (rebind.expectedControlType != "Button") 2763 rebind.WithCancelingThrough("<Keyboard>/escape"); 2764 2765 if (bindingIndex >= 0) 2766 { 2767 var bindings = action.bindings; 2768 if (bindingIndex >= bindings.Count) 2769 throw new ArgumentOutOfRangeException( 2770 $"Binding index {bindingIndex} is out of range for action '{action}' with {bindings.Count} bindings", 2771 nameof(bindings)); 2772 if (bindings[bindingIndex].isComposite) 2773 throw new InvalidOperationException( 2774 $"Cannot perform rebinding on composite binding '{bindings[bindingIndex]}' of '{action}'"); 2775 2776 rebind.WithTargetBinding(bindingIndex); 2777 } 2778 2779 return rebind; 2780 } 2781 2782 /// <summary> 2783 /// Temporarily suspend immediate re-resolution of bindings. 2784 /// </summary> 2785 /// <remarks> 2786 /// When changing control setups, it may take multiple steps to get to the final setup but each individual 2787 /// step may trigger bindings to be resolved again in order to update controls on actions (see <see cref="InputAction.controls"/>). 2788 /// Using this struct, this can be avoided and binding resolution can be deferred to after the whole operation 2789 /// is complete and the final binding setup is in place. 2790 /// </remarks> 2791 internal static DeferBindingResolutionWrapper DeferBindingResolution() 2792 { 2793 if (s_DeferBindingResolutionWrapper == null) 2794 s_DeferBindingResolutionWrapper = new DeferBindingResolutionWrapper(); 2795 s_DeferBindingResolutionWrapper.Acquire(); 2796 return s_DeferBindingResolutionWrapper; 2797 } 2798 2799 private static DeferBindingResolutionWrapper s_DeferBindingResolutionWrapper; 2800 2801 internal class DeferBindingResolutionWrapper : IDisposable 2802 { 2803 public void Acquire() 2804 { 2805 ++InputActionMap.s_DeferBindingResolution; 2806 } 2807 2808 public void Dispose() 2809 { 2810 if (InputActionMap.s_DeferBindingResolution > 0) 2811 --InputActionMap.s_DeferBindingResolution; 2812 if (InputActionMap.s_DeferBindingResolution == 0) 2813 InputActionState.DeferredResolutionOfBindings(); 2814 } 2815 } 2816 } 2817}