A game about forced loneliness, made by TACStudios
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("<Gamepad>/buttonSouth", groups: "Gamepad");
238 /// action.AddBinding("<Mouse>/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("<Gamepad>/buttonSouth", groups: "Gamepad");
299 /// action.AddBinding("<Mouse>/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("<Gamepad>/buttonSouth", groups: "Gamepad");
356 /// action.AddBinding("<Mouse>/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>"<Gamepad>/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>"<Gamepad>/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("<Gamepad>/dpad/up", groups: "Gamepad");
403 /// action.AddBinding("<Mouse>/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("<Gamepad>/buttonNorth",
543 /// path: "<Gamepad>/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 = "<Keyboard>/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<Gamepad>();
946 /// var gamepad2 = InputSystem.AddDevice<Gamepad>();
947 ///
948 /// // Create an action that binds to the A button on gamepads.
949 /// var action = new InputAction();
950 /// action.AddBinding("<Gamepad>/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 "<Gamepad>/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<Gamepad>();
1014 /// var gamepad2 = InputSystem.AddDevice<Gamepad>();
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: "<Gamepad>/buttonSouth");
1020 /// var bButtonAction = actionMap.AddAction("b", binding: "<Gamepad>/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>"<Keyboard>/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("<Keyboard>/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", "<Keyboard>/w")
1657 /// .With("Down", "<Keyboard>/s")
1658 /// .With("Left", "<Keyboard>/a")
1659 /// .With("Right", "<Keyboard>/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("<Gamepad>");
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("<Pointer>/position") // Don't bind to mouse position
1896 /// .WithControlsExcluding("<Pointer>/delta") // Don't bind to mouse movement deltas
1897 /// .WithControlsExcluding("<Pointer>/{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>"<Gamepad>/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}