InputActionRebindingExtensions.cs 103 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using UnityEngine.InputSystem.Layouts;
  5. using UnityEngine.InputSystem.LowLevel;
  6. using UnityEngine.InputSystem.Utilities;
  7. // The way target bindings for overrides are found:
  8. // - If specified, directly by index (e.g. "apply this override to the third binding in the map")
  9. // - By path (e.g. "search for binding to '<Gamepad>/leftStick' and override it with '<Gamepad>/rightStick'")
  10. // - By group (e.g. "search for binding on action 'fire' with group 'keyboard&mouse' and override it with '<Keyboard>/space'")
  11. // - By action (e.g. "bind action 'fire' from whatever it is right now to '<Gamepad>/leftStick'")
  12. ////TODO: make this work implicitly with PlayerInputs such that rebinds can be restricted to the device's of a specific player
  13. ////TODO: allow rebinding by GUIDs now that we have IDs on bindings
  14. ////FIXME: properly work with composites
  15. ////REVIEW: how well are we handling the case of rebinding to joysticks? (mostly auto-generated HID layouts)
  16. namespace UnityEngine.InputSystem
  17. {
  18. /// <summary>
  19. /// Extensions to help with dynamically rebinding <see cref="InputAction"/>s in
  20. /// various ways.
  21. /// </summary>
  22. /// <remarks>
  23. /// Unlike <see cref="InputActionSetupExtensions"/>, the extension methods in here are meant to be
  24. /// called during normal game operation, i.e. as part of screens whether the user can rebind
  25. /// controls.
  26. ///
  27. /// The two primary duties of these extensions are to apply binding overrides that non-destructively
  28. /// redirect existing bindings and to facilitate user-controlled rebinding by listening for controls
  29. /// actuated by the user.
  30. /// </remarks>
  31. /// <seealso cref="InputActionSetupExtensions"/>
  32. /// <seealso cref="InputBinding"/>
  33. /// <seealso cref="InputAction.bindings"/>
  34. public static class InputActionRebindingExtensions
  35. {
  36. /// <summary>
  37. /// Get the index of the first binding in <see cref="InputAction.bindings"/> on <paramref name="action"/>
  38. /// that matches the given binding mask.
  39. /// </summary>
  40. /// <param name="action">An input action.</param>
  41. /// <param name="bindingMask">Binding mask to match (see <see cref="InputBinding.Matches"/>).</param>
  42. /// <returns>The first binding on the action matching <paramref name="bindingMask"/> or -1 if no binding
  43. /// on the action matches the mask.</returns>
  44. /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
  45. /// <seealso cref="InputBinding.Matches"/>
  46. public static int GetBindingIndex(this InputAction action, InputBinding bindingMask)
  47. {
  48. if (action == null)
  49. throw new ArgumentNullException(nameof(action));
  50. var bindings = action.bindings;
  51. for (var i = 0; i < bindings.Count; ++i)
  52. if (bindingMask.Matches(bindings[i]))
  53. return i;
  54. return -1;
  55. }
  56. /// <summary>
  57. /// Get the index of the first binding in <see cref="InputAction.bindings"/> on <paramref name="action"/>
  58. /// that matches the given binding group and/or path.
  59. /// </summary>
  60. /// <param name="action">An input action.</param>
  61. /// <param name="group">Binding group to match (see <see cref="InputBinding.groups"/>).</param>
  62. /// <param name="path">Binding path to match (see <see cref="InputBinding.path"/>).</param>
  63. /// <returns>The first binding on the action matching the given group and/or path or -1 if no binding
  64. /// on the action matches.</returns>
  65. /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
  66. /// <seealso cref="InputBinding.Matches"/>
  67. public static int GetBindingIndex(this InputAction action, string group = default, string path = default)
  68. {
  69. if (action == null)
  70. throw new ArgumentNullException(nameof(action));
  71. return action.GetBindingIndex(new InputBinding(groups: group, path: path));
  72. }
  73. /// <summary>
  74. /// Return the binding that the given control resolved from.
  75. /// </summary>
  76. /// <param name="action">An input action that may be using the given control.</param>
  77. /// <param name="control">Control to look for a binding for.</param>
  78. /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c> -or- <paramref name="control"/>
  79. /// is <c>null</c>.</exception>
  80. /// <returns>The binding from which <paramref name="control"/> has been resolved or <c>null</c> if no such binding
  81. /// could be found on <paramref name="action"/>.</returns>
  82. public static InputBinding? GetBindingForControl(this InputAction action, InputControl control)
  83. {
  84. if (action == null)
  85. throw new ArgumentNullException(nameof(action));
  86. if (control == null)
  87. throw new ArgumentNullException(nameof(control));
  88. var bindingIndex = GetBindingIndexForControl(action, control);
  89. if (bindingIndex == -1)
  90. return null;
  91. return action.bindings[bindingIndex];
  92. }
  93. /// <summary>
  94. /// Return the index into <paramref name="action"/>'s <see cref="InputAction.bindings"/> that corresponds
  95. /// to <paramref name="control"/> bound to the action.
  96. /// </summary>
  97. /// <param name="action">The input action whose bindings to use.</param>
  98. /// <param name="control">An input control for which to look for a binding.</param>
  99. /// <returns>The index into the action's binding array for the binding that <paramref name="control"/> was
  100. /// resolved from or -1 if the control is not currently bound to the action.</returns>
  101. /// <remarks>
  102. /// Note that this method will only take currently active bindings into consideration. This means that if
  103. /// the given control <em>could</em> come from one of the bindings on the action but does not currently
  104. /// do so, the method still returns -1.
  105. ///
  106. /// In case you want to manually find out which of the bindings on the action could match the given control,
  107. /// you can do so using <see cref="InputControlPath.Matches"/>:
  108. ///
  109. /// <example>
  110. /// <code>
  111. /// // Find the binding on 'action' that matches the given 'control'.
  112. /// foreach (var binding in action.bindings)
  113. /// if (InputControlPath.Matches(binding.effectivePath, control))
  114. /// Debug.Log($"Binding for {control}: {binding}");
  115. /// </code>
  116. /// </example>
  117. /// </remarks>
  118. /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c> -or- <paramref name="control"/>
  119. /// is <c>null</c>.</exception>
  120. public static unsafe int GetBindingIndexForControl(this InputAction action, InputControl control)
  121. {
  122. if (action == null)
  123. throw new ArgumentNullException(nameof(action));
  124. if (control == null)
  125. throw new ArgumentNullException(nameof(control));
  126. var actionMap = action.GetOrCreateActionMap();
  127. actionMap.ResolveBindingsIfNecessary();
  128. var state = actionMap.m_State;
  129. Debug.Assert(state != null, "Bindings are expected to have been resolved at this point");
  130. // Find index of control in state.
  131. var controlIndex = Array.IndexOf(state.controls, control);
  132. if (controlIndex == -1)
  133. return -1;
  134. // Map to binding index.
  135. var actionIndex = action.m_ActionIndexInState;
  136. var bindingCount = state.totalBindingCount;
  137. for (var i = 0; i < bindingCount; ++i)
  138. {
  139. var bindingStatePtr = &state.bindingStates[i];
  140. if (bindingStatePtr->actionIndex == actionIndex && bindingStatePtr->controlStartIndex <= controlIndex &&
  141. controlIndex < bindingStatePtr->controlStartIndex + bindingStatePtr->controlCount)
  142. {
  143. var bindingIndexInMap = state.GetBindingIndexInMap(i);
  144. return action.BindingIndexOnMapToBindingIndexOnAction(bindingIndexInMap);
  145. }
  146. }
  147. return -1;
  148. }
  149. /// <summary>
  150. /// Return a string suitable for display in UIs that shows what the given action is currently bound to.
  151. /// </summary>
  152. /// <param name="action">Action to create a display string for.</param>
  153. /// <param name="options">Optional set of formatting flags.</param>
  154. /// <param name="group">Optional binding group to restrict the operation to. If this is supplied, it effectively
  155. /// becomes the binding mask (see <see cref="InputBinding.Matches(InputBinding)"/>) to supply to <see
  156. /// cref="GetBindingDisplayString(InputAction,InputBinding,InputBinding.DisplayStringOptions)"/>.</param>
  157. /// <returns>A string suitable for display in rebinding UIs.</returns>
  158. /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
  159. /// <remarks>
  160. /// This method will take into account any binding masks (such as from control schemes) in effect on the action
  161. /// (such as <see cref="InputAction.bindingMask"/> on the action itself, the <see cref="InputActionMap.bindingMask"/>
  162. /// on its action map, or the <see cref="InputActionAsset.bindingMask"/> on its asset) as well as the actual controls
  163. /// that the action is currently bound to (see <see cref="InputAction.controls"/>).
  164. ///
  165. /// <example>
  166. /// <code>
  167. /// var action = new InputAction();
  168. ///
  169. /// action.AddBinding("&lt;Gamepad&gt;/buttonSouth", groups: "Gamepad");
  170. /// action.AddBinding("&lt;Mouse&gt;/leftButton", groups: "KeyboardMouse");
  171. ///
  172. /// // Prints "A | LMB".
  173. /// Debug.Log(action.GetBindingDisplayString());
  174. ///
  175. /// // Prints "A".
  176. /// Debug.Log(action.GetBindingDisplayString(group: "Gamepad");
  177. ///
  178. /// // Prints "LMB".
  179. /// Debug.Log(action.GetBindingDisplayString(group: "KeyboardMouse");
  180. /// </code>
  181. /// </example>
  182. /// </remarks>
  183. /// <seealso cref="InputBinding.ToDisplayString(InputBinding.DisplayStringOptions,InputControl)"/>
  184. /// <seealso cref="InputControlPath.ToHumanReadableString(string,InputControlPath.HumanReadableStringOptions,InputControl)"/>
  185. public static string GetBindingDisplayString(this InputAction action, InputBinding.DisplayStringOptions options = default,
  186. string group = default)
  187. {
  188. if (action == null)
  189. throw new ArgumentNullException(nameof(action));
  190. // Default binding mask to the one found on the action or any of its
  191. // containers.
  192. InputBinding bindingMask;
  193. if (!string.IsNullOrEmpty(group))
  194. {
  195. bindingMask = InputBinding.MaskByGroup(group);
  196. }
  197. else
  198. {
  199. var mask = action.FindEffectiveBindingMask();
  200. if (mask.HasValue)
  201. bindingMask = mask.Value;
  202. else
  203. bindingMask = default;
  204. }
  205. return GetBindingDisplayString(action, bindingMask, options);
  206. }
  207. /// <summary>
  208. /// Return a string suitable for display in UIs that shows what the given action is currently bound to.
  209. /// </summary>
  210. /// <param name="action">Action to create a display string for.</param>
  211. /// <param name="bindingMask">Mask for bindings to take into account. Any binding on the action not
  212. /// matching (see <see cref="InputBinding.Matches(InputBinding)"/>) the mask is ignored and not included
  213. /// in the resulting string.</param>
  214. /// <param name="options">Optional set of formatting flags.</param>
  215. /// <returns>A string suitable for display in rebinding UIs.</returns>
  216. /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
  217. /// <remarks>
  218. /// This method will take into account any binding masks (such as from control schemes) in effect on the action
  219. /// (such as <see cref="InputAction.bindingMask"/> on the action itself, the <see cref="InputActionMap.bindingMask"/>
  220. /// on its action map, or the <see cref="InputActionAsset.bindingMask"/> on its asset) as well as the actual controls
  221. /// that the action is currently bound to (see <see cref="InputAction.controls"/>).
  222. ///
  223. /// <example>
  224. /// <code>
  225. /// var action = new InputAction();
  226. ///
  227. /// action.AddBinding("&lt;Gamepad&gt;/buttonSouth", groups: "Gamepad");
  228. /// action.AddBinding("&lt;Mouse&gt;/leftButton", groups: "KeyboardMouse");
  229. ///
  230. /// // Prints "A".
  231. /// Debug.Log(action.GetBindingDisplayString(InputBinding.MaskByGroup("Gamepad"));
  232. ///
  233. /// // Prints "LMB".
  234. /// Debug.Log(action.GetBindingDisplayString(InputBinding.MaskByGroup("KeyboardMouse"));
  235. /// </code>
  236. /// </example>
  237. /// </remarks>
  238. /// <seealso cref="InputBinding.ToDisplayString(InputBinding.DisplayStringOptions,InputControl)"/>
  239. /// <seealso cref="InputControlPath.ToHumanReadableString(string,InputControlPath.HumanReadableStringOptions,InputControl)"/>
  240. public static string GetBindingDisplayString(this InputAction action, InputBinding bindingMask,
  241. InputBinding.DisplayStringOptions options = default)
  242. {
  243. if (action == null)
  244. throw new ArgumentNullException(nameof(action));
  245. var result = string.Empty;
  246. var bindings = action.bindings;
  247. for (var i = 0; i < bindings.Count; ++i)
  248. {
  249. if (!bindingMask.Matches(bindings[i]))
  250. continue;
  251. ////REVIEW: should this filter out bindings that are not resolving to any controls?
  252. var text = action.GetBindingDisplayString(i, options);
  253. if (result != "")
  254. result = $"{result} | {text}";
  255. else
  256. result = text;
  257. }
  258. return result;
  259. }
  260. /// <summary>
  261. /// Return a string suitable for display in UIs that shows what the given action is currently bound to.
  262. /// </summary>
  263. /// <param name="action">Action to create a display string for.</param>
  264. /// <param name="bindingIndex">Index of the binding in the <see cref="InputAction.bindings"/> array of
  265. /// <paramref name="action"/> for which to get a display string.</param>
  266. /// <param name="options">Optional set of formatting flags.</param>
  267. /// <returns>A string suitable for display in rebinding UIs.</returns>
  268. /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
  269. /// <remarks>
  270. /// This method will ignore active binding masks and return the display string for the given binding whether it
  271. /// is masked out (disabled) or not.
  272. ///
  273. /// <example>
  274. /// <code>
  275. /// var action = new InputAction();
  276. ///
  277. /// action.AddBinding("&lt;Gamepad&gt;/buttonSouth", groups: "Gamepad");
  278. /// action.AddBinding("&lt;Mouse&gt;/leftButton", groups: "KeyboardMouse");
  279. ///
  280. /// // Prints "A".
  281. /// Debug.Log(action.GetBindingDisplayString(0));
  282. ///
  283. /// // Prints "LMB".
  284. /// Debug.Log(action.GetBindingDisplayString(1));
  285. /// </code>
  286. /// </example>
  287. /// </remarks>
  288. /// <seealso cref="InputBinding.ToDisplayString(InputBinding.DisplayStringOptions,InputControl)"/>
  289. /// <seealso cref="InputControlPath.ToHumanReadableString(string,InputControlPath.HumanReadableStringOptions,InputControl)"/>
  290. public static string GetBindingDisplayString(this InputAction action, int bindingIndex, InputBinding.DisplayStringOptions options = default)
  291. {
  292. if (action == null)
  293. throw new ArgumentNullException(nameof(action));
  294. return action.GetBindingDisplayString(bindingIndex, out var _, out var _, options);
  295. }
  296. /// <summary>
  297. /// Return a string suitable for display in UIs that shows what the given action is currently bound to.
  298. /// </summary>
  299. /// <param name="action">Action to create a display string for.</param>
  300. /// <param name="bindingIndex">Index of the binding in the <see cref="InputAction.bindings"/> array of
  301. /// <paramref name="action"/> for which to get a display string.</param>
  302. /// <param name="deviceLayoutName">Receives the name of the <see cref="InputControlLayout"/> used for the
  303. /// device in the given binding, if applicable. Otherwise is set to <c>null</c>. If, for example, the binding
  304. /// is <c>"&lt;Gamepad&gt;/buttonSouth"</c>, the resulting value is <c>"Gamepad</c>.</param>
  305. /// <param name="controlPath">Receives the path to the control on the device referenced in the given binding,
  306. /// if applicable. Otherwise is set to <c>null</c>. If, for example, the binding is <c>"&lt;Gamepad&gt;/leftStick/x"</c>,
  307. /// the resulting value is <c>"leftStick/x"</c>.</param>
  308. /// <param name="options">Optional set of formatting flags.</param>
  309. /// <returns>A string suitable for display in rebinding UIs.</returns>
  310. /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
  311. /// <remarks>
  312. /// The information returned by <paramref name="deviceLayoutName"/> and <paramref name="controlPath"/> can be used, for example,
  313. /// to associate images with controls. Based on knowing which layout is used and which control on the layout is referenced, you
  314. /// can look up an image dynamically. For example, if the layout is based on <see cref="DualShock.DualShockGamepad"/> (use
  315. /// <see cref="InputSystem.IsFirstLayoutBasedOnSecond"/> to determine inheritance), you can pick a PlayStation-specific image
  316. /// for the control as named by <paramref name="controlPath"/>.
  317. ///
  318. /// <example>
  319. /// <code>
  320. /// var action = new InputAction();
  321. ///
  322. /// action.AddBinding("&lt;Gamepad&gt;/dpad/up", groups: "Gamepad");
  323. /// action.AddBinding("&lt;Mouse&gt;/leftButton", groups: "KeyboardMouse");
  324. ///
  325. /// // Prints "A", then "Gamepad", then "dpad/up".
  326. /// Debug.Log(action.GetBindingDisplayString(InputBinding.MaskByGroup("Gamepad", out var deviceLayoutNameA, out var controlPathA));
  327. /// Debug.Log(deviceLayoutNameA);
  328. /// Debug.Log(controlPathA);
  329. ///
  330. /// // Prints "LMB", then "Mouse", then "leftButton".
  331. /// Debug.Log(action.GetBindingDisplayString(InputBinding.MaskByGroup("KeyboardMouse", out var deviceLayoutNameB, out var controlPathB));
  332. /// Debug.Log(deviceLayoutNameB);
  333. /// Debug.Log(controlPathB);
  334. /// </code>
  335. /// </example>
  336. /// </remarks>
  337. /// <seealso cref="InputBinding.ToDisplayString(InputBinding.DisplayStringOptions,InputControl)"/>
  338. /// <seealso cref="InputControlPath.ToHumanReadableString(string,InputControlPath.HumanReadableStringOptions,InputControl)"/>
  339. public static unsafe string GetBindingDisplayString(this InputAction action, int bindingIndex,
  340. out string deviceLayoutName, out string controlPath,
  341. InputBinding.DisplayStringOptions options = default)
  342. {
  343. if (action == null)
  344. throw new ArgumentNullException(nameof(action));
  345. deviceLayoutName = null;
  346. controlPath = null;
  347. var bindings = action.bindings;
  348. var bindingCount = bindings.Count;
  349. if (bindingIndex < 0 || bindingIndex >= bindingCount)
  350. throw new ArgumentOutOfRangeException(
  351. $"Binding index {bindingIndex} is out of range on action '{action}' with {bindings.Count} bindings",
  352. nameof(bindingIndex));
  353. // If the binding is a composite, compose a string using the display format string for
  354. // the composite.
  355. // NOTE: In this case, there won't be a deviceLayoutName returned from the method.
  356. if (bindings[bindingIndex].isComposite)
  357. {
  358. var compositeName = NameAndParameters.Parse(bindings[bindingIndex].effectivePath).name;
  359. // Determine what parts we have.
  360. var firstPartIndex = bindingIndex + 1;
  361. var lastPartIndex = firstPartIndex;
  362. while (lastPartIndex < bindingCount && bindings[lastPartIndex].isPartOfComposite)
  363. ++lastPartIndex;
  364. var partCount = lastPartIndex - firstPartIndex;
  365. // Get the display string for each part.
  366. var partStrings = new string[partCount];
  367. for (var i = 0; i < partCount; ++i)
  368. partStrings[i] = action.GetBindingDisplayString(firstPartIndex + i, options);
  369. // Put the parts together based on the display format string for
  370. // the composite.
  371. var displayFormatString = InputBindingComposite.GetDisplayFormatString(compositeName);
  372. if (string.IsNullOrEmpty(displayFormatString))
  373. {
  374. // No display format string. Simply go and combine all part strings.
  375. return StringHelpers.Join("/", partStrings);
  376. }
  377. return StringHelpers.ExpandTemplateString(displayFormatString,
  378. fragment =>
  379. {
  380. var result = string.Empty;
  381. // Go through all parts and look for one with the given name.
  382. for (var i = 0; i < partCount; ++i)
  383. {
  384. if (!string.Equals(bindings[firstPartIndex + i].name, fragment, StringComparison.InvariantCultureIgnoreCase))
  385. continue;
  386. if (!string.IsNullOrEmpty(result))
  387. result = $"{result}|{partStrings[i]}";
  388. else
  389. result = partStrings[i];
  390. }
  391. return result;
  392. });
  393. }
  394. // See if the binding maps to controls.
  395. InputControl control = null;
  396. var actionMap = action.GetOrCreateActionMap();
  397. actionMap.ResolveBindingsIfNecessary();
  398. var actionState = actionMap.m_State;
  399. Debug.Assert(actionState != null, "Expecting action state to be in place at this point");
  400. var bindingIndexInMap = action.BindingIndexOnActionToBindingIndexOnMap(bindingIndex);
  401. var bindingIndexInState = actionState.GetBindingIndexInState(actionMap.m_MapIndexInState, bindingIndexInMap);
  402. Debug.Assert(bindingIndexInState >= 0 && bindingIndexInState < actionState.totalBindingCount,
  403. "Computed binding index is out of range");
  404. var bindingStatePtr = &actionState.bindingStates[bindingIndexInState];
  405. if (bindingStatePtr->controlCount > 0)
  406. {
  407. ////REVIEW: does it make sense to just take a single control here?
  408. control = actionState.controls[bindingStatePtr->controlStartIndex];
  409. }
  410. // Take interactions applied to the action into account.
  411. var binding = bindings[bindingIndex];
  412. if (string.IsNullOrEmpty(binding.effectiveInteractions))
  413. binding.overrideInteractions = action.interactions;
  414. else if (!string.IsNullOrEmpty(action.interactions))
  415. binding.overrideInteractions = $"{binding.effectiveInteractions};action.interactions";
  416. return binding.ToDisplayString(out deviceLayoutName, out controlPath, options, control: control);
  417. }
  418. /// <summary>
  419. /// Put an override on all matching bindings of <paramref name="action"/>.
  420. /// </summary>
  421. /// <param name="action">Action to apply the override to.</param>
  422. /// <param name="newPath">New binding path to take effect. Supply an empty string
  423. /// to disable the binding(s). See <see cref="InputControlPath"/> for details on
  424. /// the path language.</param>
  425. /// <param name="group">Optional list of binding groups to target the override
  426. /// to. For example, <c>"Keyboard;Gamepad"</c> will only apply overrides to bindings
  427. /// that either have the <c>"Keyboard"</c> or the <c>"Gamepad"</c> binding group
  428. /// listed in <see cref="InputBinding.groups"/>.</param>
  429. /// <param name="path">Only override bindings that have this exact path.</param>
  430. /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
  431. /// <remarks>
  432. /// Calling this method is equivalent to calling <see cref="ApplyBindingOverride(InputAction,InputBinding)"/>
  433. /// with the properties of the given <see cref="InputBinding"/> initialized accordingly.
  434. ///
  435. /// <example>
  436. /// <code>
  437. /// // Override the binding to the gamepad A button with a binding to
  438. /// // the Y button.
  439. /// fireAction.ApplyBindingOverride("&lt;Gamepad&gt;/buttonNorth",
  440. /// path: "&lt;Gamepad&gt;/buttonSouth);
  441. /// </code>
  442. /// </example>
  443. /// </remarks>
  444. /// <seealso cref="ApplyBindingOverride(InputAction,InputBinding)"/>
  445. /// <seealso cref="InputBinding.effectivePath"/>
  446. /// <seealso cref="InputBinding.overridePath"/>
  447. /// <seealso cref="InputBinding.Matches"/>
  448. public static void ApplyBindingOverride(this InputAction action, string newPath, string group = null, string path = null)
  449. {
  450. if (action == null)
  451. throw new ArgumentNullException(nameof(action));
  452. ApplyBindingOverride(action, new InputBinding {overridePath = newPath, groups = group, path = path});
  453. }
  454. /// <summary>
  455. /// Apply overrides to all bindings on <paramref name="action"/> that match <paramref name="bindingOverride"/>.
  456. /// The override values are taken from <see cref="InputBinding.overridePath"/>, <see cref="InputBinding.overrideProcessors"/>,
  457. /// and <seealso cref="InputBinding.overrideInteractions"/> on <paramref name="bindingOverride"/>.
  458. /// </summary>
  459. /// <param name="action">Action to override bindings on.</param>
  460. /// <param name="bindingOverride">A binding that both acts as a mask (see <see cref="InputBinding.Matches"/>)
  461. /// on the bindings to <paramref name="action"/> and as a container for the override values.</param>
  462. /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
  463. /// <remarks>
  464. /// The method will go through all of the bindings for <paramref name="action"/> (i.e. its <see cref="InputAction.bindings"/>)
  465. /// and call <see cref="InputBinding.Matches"/> on them with <paramref name="bindingOverride"/>.
  466. /// For every binding that returns <c>true</c> from <c>Matches</c>, the override values from the
  467. /// binding (i.e. <see cref="InputBinding.overridePath"/>, <see cref="InputBinding.overrideProcessors"/>,
  468. /// and <see cref="InputBinding.overrideInteractions"/>) are copied into the binding.
  469. ///
  470. /// Binding overrides are non-destructive. They do not change the bindings set up for an action
  471. /// but rather apply non-destructive modifications that change the paths of existing bindings.
  472. /// However, this also means that for overrides to work, there have to be existing bindings that
  473. /// can be modified.
  474. ///
  475. /// This is achieved by setting <see cref="InputBinding.overridePath"/> which is a non-serialized
  476. /// property. When resolving bindings, the system will use <see cref="InputBinding.effectivePath"/>
  477. /// which uses <see cref="InputBinding.overridePath"/> if set or <see cref="InputBinding.path"/>
  478. /// otherwise. The same applies to <see cref="InputBinding.effectiveProcessors"/> and <see
  479. /// cref="InputBinding.effectiveInteractions"/>.
  480. ///
  481. /// <example>
  482. /// <code>
  483. /// // Override the binding in the "KeyboardMouse" group on 'fireAction'
  484. /// // by setting its override binding path to the space bar on the keyboard.
  485. /// fireAction.ApplyBindingOverride(new InputBinding
  486. /// {
  487. /// groups = "KeyboardMouse",
  488. /// overridePath = "&lt;Keyboard&gt;/space"
  489. /// });
  490. /// </code>
  491. /// </example>
  492. ///
  493. /// If the given action is enabled when calling this method, the effect will be immediate,
  494. /// i.e. binding resolution takes place and <see cref="InputAction.controls"/> are updated.
  495. /// If the action is not enabled, binding resolution is deferred to when controls are needed
  496. /// next (usually when either <see cref="InputAction.controls"/> is queried or when the
  497. /// action is enabled).
  498. /// </remarks>
  499. /// <seealso cref="InputAction.bindings"/>
  500. /// <seealso cref="InputBinding.Matches"/>
  501. public static void ApplyBindingOverride(this InputAction action, InputBinding bindingOverride)
  502. {
  503. if (action == null)
  504. throw new ArgumentNullException(nameof(action));
  505. bindingOverride.action = action.name;
  506. var actionMap = action.GetOrCreateActionMap();
  507. ApplyBindingOverride(actionMap, bindingOverride);
  508. }
  509. /// <summary>
  510. /// Apply a binding override to the Nth binding on the given action.
  511. /// </summary>
  512. /// <param name="action">Action to apply the binding override to.</param>
  513. /// <param name="bindingIndex">Index of the binding in <see cref="InputAction.bindings"/> to
  514. /// which to apply the override to.</param>
  515. /// <param name="bindingOverride">A binding that specifies the overrides to apply. In particular,
  516. /// the <see cref="InputBinding.overridePath"/>, <see cref="InputBinding.overrideProcessors"/>, and
  517. /// <see cref="InputBinding.overrideInteractions"/> properties will be copied into the binding
  518. /// in <see cref="InputAction.bindings"/>. The remaining fields will be ignored by this method.</param>
  519. /// <exception cref="ArgumentNullException"><paramref name="action"/> is null.</exception>
  520. /// <exception cref="ArgumentOutOfRangeException"><paramref name="bindingIndex"/> is out of range.</exception>
  521. /// <remarks>
  522. /// Unlike <see cref="ApplyBindingOverride(InputAction,InputBinding)"/> this method will
  523. /// not use <see cref="InputBinding.Matches"/> to determine which binding to apply the
  524. /// override to. Instead, it will apply the override to the binding at the given index
  525. /// and to that binding alone.
  526. ///
  527. /// The remaining details of applying overrides are identical to <see
  528. /// cref="ApplyBindingOverride(InputAction,InputBinding)"/>.
  529. ///
  530. /// Note that calling this method with an empty (default-constructed) <paramref name="bindingOverride"/>
  531. /// is equivalent to resetting all overrides on the given binding.
  532. ///
  533. /// <example>
  534. /// <code>
  535. /// // Reset the overrides on the second binding on 'fireAction'.
  536. /// fireAction.ApplyBindingOverride(1, default);
  537. /// </code>
  538. /// </example>
  539. /// </remarks>
  540. /// <seealso cref="ApplyBindingOverride(InputAction,InputBinding)"/>
  541. public static void ApplyBindingOverride(this InputAction action, int bindingIndex, InputBinding bindingOverride)
  542. {
  543. if (action == null)
  544. throw new ArgumentNullException(nameof(action));
  545. var indexOnMap = action.BindingIndexOnActionToBindingIndexOnMap(bindingIndex);
  546. bindingOverride.action = action.name;
  547. ApplyBindingOverride(action.GetOrCreateActionMap(), indexOnMap, bindingOverride);
  548. }
  549. /// <summary>
  550. /// Apply a binding override to the Nth binding on the given action.
  551. /// </summary>
  552. /// <param name="action">Action to apply the binding override to.</param>
  553. /// <param name="bindingIndex">Index of the binding in <see cref="InputAction.bindings"/> to
  554. /// which to apply the override to.</param>
  555. /// <param name="path">Override path (<see cref="InputBinding.overridePath"/>) to set on
  556. /// the given binding in <see cref="InputAction.bindings"/>.</param>
  557. /// <exception cref="ArgumentNullException"><paramref name="action"/> is null.</exception>
  558. /// <exception cref="ArgumentOutOfRangeException"><paramref name="bindingIndex"/> is out of range.</exception>
  559. /// <remarks>
  560. /// Calling this method is equivalent to calling <see cref="ApplyBindingOverride(InputAction,int,InputBinding)"/>
  561. /// like so:
  562. ///
  563. /// <example>
  564. /// <code>
  565. /// action.ApplyBindingOverride(new InputBinding { overridePath = path });
  566. /// </code>
  567. /// </example>
  568. /// </remarks>
  569. /// <seealso cref="ApplyBindingOverride(InputAction,int,InputBinding)"/>
  570. public static void ApplyBindingOverride(this InputAction action, int bindingIndex, string path)
  571. {
  572. if (path == null)
  573. throw new ArgumentException("Binding path cannot be null", nameof(path));
  574. ApplyBindingOverride(action, bindingIndex, new InputBinding {overridePath = path});
  575. }
  576. /// <summary>
  577. /// Apply the given binding override to all bindings in the map that are matched by the override.
  578. /// </summary>
  579. /// <param name="actionMap"></param>
  580. /// <param name="bindingOverride"></param>
  581. /// <returns>The number of bindings overridden in the given map.</returns>
  582. /// <exception cref="ArgumentNullException"><paramref name="actionMap"/> is <c>null</c>.</exception>
  583. public static int ApplyBindingOverride(this InputActionMap actionMap, InputBinding bindingOverride)
  584. {
  585. if (actionMap == null)
  586. throw new ArgumentNullException(nameof(actionMap));
  587. var bindings = actionMap.m_Bindings;
  588. if (bindings == null)
  589. return 0;
  590. // Go through all bindings in the map and match them to the override.
  591. var bindingCount = bindings.Length;
  592. var matchCount = 0;
  593. for (var i = 0; i < bindingCount; ++i)
  594. {
  595. if (!bindingOverride.Matches(ref bindings[i]))
  596. continue;
  597. // Set overrides on binding.
  598. bindings[i].overridePath = bindingOverride.overridePath;
  599. bindings[i].overrideInteractions = bindingOverride.overrideInteractions;
  600. bindings[i].overrideProcessors = bindingOverride.overrideProcessors;
  601. ++matchCount;
  602. }
  603. if (matchCount > 0)
  604. {
  605. actionMap.ClearPerActionCachedBindingData();
  606. actionMap.LazyResolveBindings();
  607. }
  608. return matchCount;
  609. }
  610. public static void ApplyBindingOverride(this InputActionMap actionMap, int bindingIndex, InputBinding bindingOverride)
  611. {
  612. if (actionMap == null)
  613. throw new ArgumentNullException(nameof(actionMap));
  614. var bindingsCount = actionMap.m_Bindings?.Length ?? 0;
  615. if (bindingIndex < 0 || bindingIndex >= bindingsCount)
  616. throw new ArgumentOutOfRangeException(nameof(bindingIndex),
  617. $"Cannot apply override to binding at index {bindingIndex} in map '{actionMap}' with only {bindingsCount} bindings");
  618. actionMap.m_Bindings[bindingIndex].overridePath = bindingOverride.overridePath;
  619. actionMap.m_Bindings[bindingIndex].overrideInteractions = bindingOverride.overrideInteractions;
  620. actionMap.m_Bindings[bindingIndex].overrideProcessors = bindingOverride.overrideProcessors;
  621. actionMap.ClearPerActionCachedBindingData();
  622. actionMap.LazyResolveBindings();
  623. }
  624. /// <summary>
  625. /// Remove any overrides from the binding on <paramref name="action"/> with the given index.
  626. /// </summary>
  627. /// <param name="action">Action whose bindings to modify.</param>
  628. /// <param name="bindingIndex">Index of the binding within <paramref name="action"/>'s <see cref="InputAction.bindings"/>.</param>
  629. /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
  630. /// <exception cref="ArgumentOutOfRangeException"><paramref name="bindingIndex"/> is invalid.</exception>
  631. public static void RemoveBindingOverride(this InputAction action, int bindingIndex)
  632. {
  633. if (action == null)
  634. throw new ArgumentNullException(nameof(action));
  635. action.ApplyBindingOverride(bindingIndex, default(InputBinding));
  636. }
  637. /// <summary>
  638. /// Remove any overrides from the binding on <paramref name="action"/> matching the given binding mask.
  639. /// </summary>
  640. /// <param name="action">Action whose bindings to modify.</param>
  641. /// <param name="bindingMask">Mask that will be matched against the bindings on <paramref name="action"/>. All bindings
  642. /// that match the mask (see <see cref="InputBinding.Matches"/>) will have their overrides removed. If none of the
  643. /// bindings on the action match the mask, no bindings will be modified.</param>
  644. /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
  645. /// <remarks>
  646. /// <example>
  647. /// <code>
  648. /// // Remove all binding overrides from bindings associated with the "Gamepad" binding group.
  649. /// myAction.RemoveBindingOverride(InputBinding.MaskByGroup("Gamepad"));
  650. /// </code>
  651. /// </example>
  652. /// </remarks>
  653. public static void RemoveBindingOverride(this InputAction action, InputBinding bindingMask)
  654. {
  655. if (action == null)
  656. throw new ArgumentNullException(nameof(action));
  657. bindingMask.overridePath = null;
  658. bindingMask.overrideInteractions = null;
  659. bindingMask.overrideProcessors = null;
  660. // Simply apply but with a null binding.
  661. ApplyBindingOverride(action, bindingMask);
  662. }
  663. private static void RemoveBindingOverride(this InputActionMap actionMap, InputBinding bindingMask)
  664. {
  665. if (actionMap == null)
  666. throw new ArgumentNullException(nameof(actionMap));
  667. bindingMask.overridePath = null;
  668. bindingMask.overrideInteractions = null;
  669. bindingMask.overrideProcessors = null;
  670. // Simply apply but with a null binding.
  671. ApplyBindingOverride(actionMap, bindingMask);
  672. }
  673. /// <summary>
  674. /// Remove all binding overrides on <paramref name="action"/>, i.e. clear all <see cref="InputBinding.overridePath"/>,
  675. /// <see cref="InputBinding.overrideProcessors"/>, and <see cref="InputBinding.overrideInteractions"/> set on bindings
  676. /// for the given action.
  677. /// </summary>
  678. /// <param name="action">Action to remove overrides from.</param>
  679. /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
  680. public static void RemoveAllBindingOverrides(this InputAction action)
  681. {
  682. if (action == null)
  683. throw new ArgumentNullException(nameof(action));
  684. var actionName = action.name;
  685. var actionMap = action.GetOrCreateActionMap();
  686. var bindings = actionMap.m_Bindings;
  687. if (bindings == null)
  688. return;
  689. var bindingCount = bindings.Length;
  690. for (var i = 0; i < bindingCount; ++i)
  691. {
  692. if (string.Compare(bindings[i].action, actionName, StringComparison.InvariantCultureIgnoreCase) != 0)
  693. continue;
  694. bindings[i].overridePath = null;
  695. bindings[i].overrideInteractions = null;
  696. bindings[i].overrideProcessors = null;
  697. }
  698. actionMap.ClearPerActionCachedBindingData();
  699. actionMap.LazyResolveBindings();
  700. }
  701. ////REVIEW: are the IEnumerable variations worth having?
  702. public static void ApplyBindingOverrides(this InputActionMap actionMap, IEnumerable<InputBinding> overrides)
  703. {
  704. if (actionMap == null)
  705. throw new ArgumentNullException(nameof(actionMap));
  706. if (overrides == null)
  707. throw new ArgumentNullException(nameof(overrides));
  708. foreach (var binding in overrides)
  709. ApplyBindingOverride(actionMap, binding);
  710. }
  711. public static void RemoveBindingOverrides(this InputActionMap actionMap, IEnumerable<InputBinding> overrides)
  712. {
  713. if (actionMap == null)
  714. throw new ArgumentNullException(nameof(actionMap));
  715. if (overrides == null)
  716. throw new ArgumentNullException(nameof(overrides));
  717. foreach (var binding in overrides)
  718. RemoveBindingOverride(actionMap, binding);
  719. }
  720. /// <summary>
  721. /// Restore all bindings in the map to their defaults.
  722. /// </summary>
  723. /// <param name="actionMap">Action map to remove overrides from.</param>
  724. /// <exception cref="ArgumentNullException"><paramref name="actionMap"/> is <c>null</c>.</exception>
  725. public static void RemoveAllBindingOverrides(this InputActionMap actionMap)
  726. {
  727. if (actionMap == null)
  728. throw new ArgumentNullException(nameof(actionMap));
  729. if (actionMap.m_Bindings == null)
  730. return; // No bindings in map.
  731. var emptyBinding = new InputBinding();
  732. var bindingCount = actionMap.m_Bindings.Length;
  733. for (var i = 0; i < bindingCount; ++i)
  734. ApplyBindingOverride(actionMap, i, emptyBinding);
  735. }
  736. ////REVIEW: how does this system work in combination with actual user overrides
  737. //// (answer: we rebind based on the base path not the override path; thus user overrides are unaffected;
  738. //// and hopefully operate on more than just the path; probably action+path or something)
  739. ////TODO: add option to suppress any non-matching binding by setting its override to an empty path
  740. ////TODO: need ability to do this with a list of controls
  741. // For all bindings in the given action, if a binding matches a control in the given control
  742. // hierarchy, set an override on the binding to refer specifically to that control.
  743. //
  744. // Returns the number of overrides that have been applied.
  745. //
  746. // Use case: Say you have a local co-op game and a single action map that represents the
  747. // actions of any single player. To end up with action maps that are specific to
  748. // a certain player, you could, for example, clone the action map four times, and then
  749. // take four gamepad devices and use the methods here to have bindings be overridden
  750. // on each map to refer to a specific gamepad instance.
  751. //
  752. // Another example is having two XRControllers and two action maps can be on either hand.
  753. // At runtime you can dynamically override and re-override the bindings on the action maps
  754. // to use them with the controllers as desired.
  755. public static int ApplyBindingOverridesOnMatchingControls(this InputAction action, InputControl control)
  756. {
  757. if (action == null)
  758. throw new ArgumentNullException(nameof(action));
  759. if (control == null)
  760. throw new ArgumentNullException(nameof(control));
  761. var bindings = action.bindings;
  762. var bindingsCount = bindings.Count;
  763. var numMatchingControls = 0;
  764. for (var i = 0; i < bindingsCount; ++i)
  765. {
  766. var matchingControl = InputControlPath.TryFindControl(control, bindings[i].path);
  767. if (matchingControl == null)
  768. continue;
  769. action.ApplyBindingOverride(i, matchingControl.path);
  770. ++numMatchingControls;
  771. }
  772. return numMatchingControls;
  773. }
  774. public static int ApplyBindingOverridesOnMatchingControls(this InputActionMap actionMap, InputControl control)
  775. {
  776. if (actionMap == null)
  777. throw new ArgumentNullException(nameof(actionMap));
  778. if (control == null)
  779. throw new ArgumentNullException(nameof(control));
  780. var actions = actionMap.actions;
  781. var actionCount = actions.Count;
  782. var numMatchingControls = 0;
  783. for (var i = 0; i < actionCount; ++i)
  784. {
  785. var action = actions[i];
  786. numMatchingControls = action.ApplyBindingOverridesOnMatchingControls(control);
  787. }
  788. return numMatchingControls;
  789. }
  790. ////TODO: allow overwriting magnitude with custom values; maybe turn more into an overall "score" for a control
  791. /// <summary>
  792. /// An ongoing rebinding operation.
  793. /// </summary>
  794. /// <remarks>
  795. /// This class acts as both a configuration interface for rebinds as well as a controller while
  796. /// the rebind is ongoing. An instance can be reused arbitrary many times. Doing so can avoid allocating
  797. /// additional GC memory (the class internally retains state that it can reuse for multiple rebinds).
  798. ///
  799. /// Note, however, that during rebinding it can be necessary to look at the <see cref="InputControlLayout"/>
  800. /// information registered in the system which means that layouts may have to be loaded. These will be
  801. /// cached for as long as the rebind operation is not disposed of.
  802. ///
  803. /// To reset the configuration of a rebind operation without releasing its memory, call <see cref="Reset"/>.
  804. /// Note that changing configuration while a rebind is in progress in not allowed and will throw
  805. /// <see cref="InvalidOperationException"/>.
  806. ///
  807. /// <example>
  808. /// <code>
  809. /// var rebind = new RebindingOperation()
  810. /// .WithAction(myAction)
  811. /// .WithBindingGroup("Gamepad")
  812. /// .WithCancelingThrough("&lt;Keyboard&gt;/escape");
  813. ///
  814. /// rebind.Start();
  815. /// </code>
  816. /// </example>
  817. ///
  818. /// Note that instances of this class <em>must</em> be disposed of to not leak memory on the unmanaged heap.
  819. /// </remarks>
  820. /// <seealso cref="InputActionRebindingExtensions.PerformInteractiveRebinding"/>
  821. public sealed class RebindingOperation : IDisposable
  822. {
  823. public const float kDefaultMagnitudeThreshold = 0.2f;
  824. /// <summary>
  825. /// The action that rebinding is being performed on.
  826. /// </summary>
  827. /// <seealso cref="WithAction"/>
  828. public InputAction action => m_ActionToRebind;
  829. /// <summary>
  830. /// Optional mask to determine which bindings to apply overrides to.
  831. /// </summary>
  832. /// <remarks>
  833. /// If this is not null, all bindings that match this mask will have overrides applied to them.
  834. /// </remarks>
  835. public InputBinding? bindingMask => m_BindingMask;
  836. ////REVIEW: exposing this as InputControlList is very misleading as users will not get an error when modifying the list;
  837. //// however, exposing through an interface will lead to boxing...
  838. /// <summary>
  839. /// Controls that had input and were deemed potential matches to rebind to.
  840. /// </summary>
  841. /// <remarks>
  842. /// Controls in the list should be ordered by priority with the first element in the list being
  843. /// considered the best match.
  844. /// </remarks>
  845. /// <seealso cref="AddCandidate"/>
  846. /// <seealso cref="RemoveCandidate"/>
  847. public InputControlList<InputControl> candidates => m_Candidates;
  848. /// <summary>
  849. /// The matching score for each control in <see cref="candidates"/>.
  850. /// </summary>
  851. /// <value>A relative floating-point score for each control in <see cref="candidates"/>.</value>
  852. /// <remarks>
  853. /// Candidates are ranked and sorted by their score. By default, a score is computed for each candidate
  854. /// control automatically. However, this can be overridden using <see cref="OnComputeScore"/>.
  855. ///
  856. /// Default scores are directly based on magnitudes (see <see cref="InputControl.EvaluateMagnitude()"/>).
  857. /// The greater the magnitude of actuation, the greater the score associated with the control. This means,
  858. /// for example, that if both X and Y are actuated on a gamepad stick, the axis with the greater amount
  859. /// of actuation will get scored higher and thus be more likely to get picked.
  860. ///
  861. /// In addition, 1 is added to each default score if the respective control is non-synthetic (see <see
  862. /// cref="InputControl.synthetic"/>). This will give controls that correspond to actual controls present
  863. /// on the device precedence over those added internally. For example, if both are actuated, the synthetic
  864. /// <see cref="Controls.StickControl.up"/> button on stick controls will be ranked lower than the <see
  865. /// cref="Gamepad.buttonSouth"/> which is an actual button on the device.
  866. /// </remarks>
  867. /// <seealso cref="OnComputeScore"/>
  868. /// <seealso cref="candidates"/>
  869. public ReadOnlyArray<float> scores => new ReadOnlyArray<float>(m_Scores, 0, m_Candidates.Count);
  870. /// <summary>
  871. /// The matching control actuation level (see <see cref="InputControl.EvaluateMagnitude()"/> for each control in <see cref="candidates"/>.
  872. /// </summary>
  873. /// <value><see cref="InputControl.EvaluateMagnitude()"/> result for each <see cref="InputControl"/> in <see cref="candidates"/>.</value>
  874. /// <remarks>
  875. /// This array mirrors <see cref="candidates"/>, i.e. each entry corresponds to the entry in <see cref="candidates"/> at
  876. /// the same index.
  877. /// </remarks>
  878. /// <seealso cref="InputControl.EvaluateMagnitude()"/>
  879. /// <seealso cref="candidates"/>
  880. public ReadOnlyArray<float> magnitudes => new ReadOnlyArray<float>(m_Magnitudes, 0, m_Candidates.Count);
  881. /// <summary>
  882. /// The control currently deemed the best candidate.
  883. /// </summary>
  884. /// <value>Primary candidate control at this point.</value>
  885. /// <remarks>
  886. /// If there are no candidates yet, this returns <c>null</c>. If there are candidates,
  887. /// it returns the first element of <see cref="candidates"/> which is always the control
  888. /// with the highest matching score.
  889. /// </remarks>
  890. public InputControl selectedControl
  891. {
  892. get
  893. {
  894. if (m_Candidates.Count == 0)
  895. return null;
  896. return m_Candidates[0];
  897. }
  898. }
  899. /// <summary>
  900. /// Whether the rebind is currently in progress.
  901. /// </summary>
  902. /// <value>Whether rebind is in progress.</value>
  903. /// <remarks>
  904. /// This is true after calling <see cref="Start"/> and set to false when
  905. /// <see cref="OnComplete"/> or <see cref="OnCancel"/> is called.
  906. /// </remarks>
  907. /// <seealso cref="Start"/>
  908. /// <seealso cref="completed"/>
  909. /// <seealso cref="canceled"/>
  910. public bool started => (m_Flags & Flags.Started) != 0;
  911. /// <summary>
  912. /// Whether the rebind has been completed.
  913. /// </summary>
  914. /// <value>True if the rebind has been completed.</value>
  915. /// <seealso cref="OnComplete(Action{RebindingOperation})"/>
  916. public bool completed => (m_Flags & Flags.Completed) != 0;
  917. public bool canceled => (m_Flags & Flags.Canceled) != 0;
  918. public double startTime => m_StartTime;
  919. public float timeout => m_Timeout;
  920. public string expectedControlType => m_ExpectedLayout;
  921. /// <summary>
  922. /// Perform rebinding on the bindings of the given action.
  923. /// </summary>
  924. /// <param name="action">Action to perform rebinding on.</param>
  925. /// <returns>The same RebindingOperation instance.</returns>
  926. /// <remarks>
  927. /// Note that by default, a rebind does not have a binding mask or any other setting
  928. /// that constrains which binding the rebind is applied to. This means that if the action
  929. /// has multiple bindings, all of them will have overrides applied to them.
  930. ///
  931. /// To target specific bindings, either set a binding index with <see cref="WithTargetBinding"/>,
  932. /// or set a binding mask with <see cref="WithBindingMask"/> or <see cref="WithBindingGroup"/>.
  933. ///
  934. /// If the action has an associated <see cref="InputAction.expectedControlType"/> set,
  935. /// it will automatically be passed to <see cref="WithExpectedControlType(string)"/>.
  936. /// </remarks>
  937. /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
  938. /// <exception cref="InvalidOperationException"><paramref name="action"/> is currently enabled.</exception>
  939. /// <seealso cref="PerformInteractiveRebinding"/>
  940. public RebindingOperation WithAction(InputAction action)
  941. {
  942. ThrowIfRebindInProgress();
  943. if (action == null)
  944. throw new ArgumentNullException(nameof(action));
  945. if (action.enabled)
  946. throw new InvalidOperationException($"Cannot rebind action '{action}' while it is enabled");
  947. m_ActionToRebind = action;
  948. // If the action has an associated expected layout, constrain ourselves by it.
  949. // NOTE: We do *NOT* translate this to a control type and constrain by that as a whole chain
  950. // of derived layouts may share the same control type.
  951. if (!string.IsNullOrEmpty(action.expectedControlType))
  952. WithExpectedControlType(action.expectedControlType);
  953. else if (action.type == InputActionType.Button)
  954. WithExpectedControlType("Button");
  955. return this;
  956. }
  957. /// <summary>
  958. /// Prevent all input events that have input matching the rebind operation's configuration from reaching
  959. /// its targeted <see cref="InputDevice"/>s and thus taking effect.
  960. /// </summary>
  961. /// <returns>The same RebindingOperation instance.</returns>
  962. /// <remarks>
  963. /// While rebinding interactively, it is usually for the most part undesirable for input to actually have an effect.
  964. /// For example, when rebind gamepad input, pressing the "A" button should not lead to a "submit" action in the UI.
  965. /// For this reason, a rebind can be configured to automatically swallow any input event except the ones having
  966. /// input on controls matching <see cref="WithControlsExcluding"/>.
  967. ///
  968. /// Not at all input necessarily should be suppressed. For example, it can be desirable to have UI that
  969. /// allows the user to cancel an ongoing rebind by clicking with the mouse. This means that mouse position and
  970. /// click input should come through. For this reason, input from controls matching <see cref="WithControlsExcluding"/>
  971. /// is still let through.
  972. /// </remarks>
  973. public RebindingOperation WithMatchingEventsBeingSuppressed(bool value = true)
  974. {
  975. ThrowIfRebindInProgress();
  976. if (value)
  977. m_Flags |= Flags.SuppressMatchingEvents;
  978. else
  979. m_Flags &= ~Flags.SuppressMatchingEvents;
  980. return this;
  981. }
  982. /// <summary>
  983. /// Set the control path that is matched against actuated controls.
  984. /// </summary>
  985. /// <param name="binding">A control path (see <see cref="InputControlPath"/>) such as <c>"&lt;Keyboard&gt;/escape"</c>.</param>
  986. /// <returns>The same RebindingOperation instance.</returns>
  987. /// <remarks>
  988. /// Note that every rebind operation has only one such path. Calling this method repeatedly will overwrite
  989. /// the path set from prior calls.
  990. ///
  991. /// <code>
  992. /// var rebind = new RebindingOperation();
  993. ///
  994. /// // Cancel from keyboard escape key.
  995. /// rebind
  996. /// .WithCancelingThrough("&lt;Keyboard&gt;/escape");
  997. ///
  998. /// // Cancel from any control with "Cancel" usage.
  999. /// // NOTE: This can be dangerous. The control that the wants to bind to may have the "Cancel"
  1000. /// // usage assigned to it, thus making it impossible for the user to bind to the control.
  1001. /// rebind
  1002. /// .WithCancelingThrough("*/{Cancel}");
  1003. /// </code>
  1004. /// </remarks>
  1005. public RebindingOperation WithCancelingThrough(string binding)
  1006. {
  1007. ThrowIfRebindInProgress();
  1008. m_CancelBinding = binding;
  1009. return this;
  1010. }
  1011. public RebindingOperation WithCancelingThrough(InputControl control)
  1012. {
  1013. ThrowIfRebindInProgress();
  1014. if (control == null)
  1015. throw new ArgumentNullException(nameof(control));
  1016. return WithCancelingThrough(control.path);
  1017. }
  1018. public RebindingOperation WithExpectedControlType(string layoutName)
  1019. {
  1020. ThrowIfRebindInProgress();
  1021. m_ExpectedLayout = new InternedString(layoutName);
  1022. return this;
  1023. }
  1024. public RebindingOperation WithExpectedControlType(Type type)
  1025. {
  1026. ThrowIfRebindInProgress();
  1027. if (type != null && !typeof(InputControl).IsAssignableFrom(type))
  1028. throw new ArgumentException($"Type '{type.Name}' is not an InputControl", "type");
  1029. m_ControlType = type;
  1030. return this;
  1031. }
  1032. public RebindingOperation WithExpectedControlType<TControl>()
  1033. where TControl : InputControl
  1034. {
  1035. ThrowIfRebindInProgress();
  1036. return WithExpectedControlType(typeof(TControl));
  1037. }
  1038. ////TODO: allow targeting bindings by name (i.e. be able to say WithTargetBinding("Left"))
  1039. public RebindingOperation WithTargetBinding(int bindingIndex)
  1040. {
  1041. m_TargetBindingIndex = bindingIndex;
  1042. return this;
  1043. }
  1044. public RebindingOperation WithBindingMask(InputBinding? bindingMask)
  1045. {
  1046. m_BindingMask = bindingMask;
  1047. return this;
  1048. }
  1049. public RebindingOperation WithBindingGroup(string group)
  1050. {
  1051. return WithBindingMask(new InputBinding {groups = group});
  1052. }
  1053. /// <summary>
  1054. /// Disable the default behavior of automatically generalizing the path of a selected control.
  1055. /// </summary>
  1056. /// <returns>The same RebindingOperation instance.</returns>
  1057. /// <remarks>
  1058. /// At runtime, every <see cref="InputControl"/> has a unique path in the system (<see cref="InputControl.path"/>).
  1059. /// However, when performing rebinds, we are not generally interested in the specific runtime path of the
  1060. /// control -- which may depend on the number and types of devices present. In fact, most of the time we are not
  1061. /// even interested in what particular brand of device the user is rebinding to but rather want to just bind based
  1062. /// on the device's broad category.
  1063. ///
  1064. /// For example, if the user has a DualShock controller and performs an interactive rebind, we usually do not want
  1065. /// to generate override paths that reflects that the input specifically came from a DualShock controller. Rather,
  1066. /// we're usually interested in the fact that it came from a gamepad.
  1067. /// </remarks>
  1068. /// <seealso cref="InputBinding.overridePath"/>
  1069. public RebindingOperation WithoutGeneralizingPathOfSelectedControl()
  1070. {
  1071. m_Flags |= Flags.DontGeneralizePathOfSelectedControl;
  1072. return this;
  1073. }
  1074. public RebindingOperation WithRebindAddingNewBinding(string group = null)
  1075. {
  1076. m_Flags |= Flags.AddNewBinding;
  1077. m_BindingGroupForNewBinding = group;
  1078. return this;
  1079. }
  1080. /// <summary>
  1081. /// Require actuation of controls to exceed a certain level.
  1082. /// </summary>
  1083. /// <param name="magnitude">Minimum magnitude threshold that has to be reached on a control
  1084. /// for it to be considered a candidate. See <see cref="InputControl.EvaluateMagnitude()"/> for
  1085. /// details about magnitude evaluations.</param>
  1086. /// <returns>The same RebindingOperation instance.</returns>
  1087. /// <exception cref="ArgumentException"><paramref name="magnitude"/> is negative.</exception>
  1088. /// <remarks>
  1089. /// Rebind operations use a default threshold of 0.2. This means that the actuation level
  1090. /// of any control as returned by <see cref="InputControl.EvaluateMagnitude()"/> must be equal
  1091. /// or greater than 0.2 for it to be considered a potential candidate. This helps filter out
  1092. /// controls that are actuated incidentally as part of actuating other controls.
  1093. ///
  1094. /// For example, if the player wants to bind an action to the X axis of the gamepad's right
  1095. /// stick, the player will almost unavoidably also actuate the Y axis to a certain degree.
  1096. /// However, if actuation of the Y axis stays under 2.0, it will automatically get filtered out.
  1097. ///
  1098. /// Note that the magnitude threshold is not the only mechanism that helps trying to find
  1099. /// the most actuated control. In fact, all controls will eventually be sorted by magnitude
  1100. /// of actuation so even if both X and Y of a stick make it into the candidate list, if X
  1101. /// is actuated more strongly than Y, it will be favored.
  1102. ///
  1103. /// Note that you can also use this method to <em>lower</em> the default threshold of 0.2
  1104. /// in case you want more controls to make it through the matching process.
  1105. /// </remarks>
  1106. public RebindingOperation WithMagnitudeHavingToBeGreaterThan(float magnitude)
  1107. {
  1108. ThrowIfRebindInProgress();
  1109. if (magnitude < 0)
  1110. throw new ArgumentException($"Magnitude has to be positive but was {magnitude}",
  1111. nameof(magnitude));
  1112. m_MagnitudeThreshold = magnitude;
  1113. return this;
  1114. }
  1115. /// <summary>
  1116. /// Do not ignore input from noisy controls.
  1117. /// </summary>
  1118. /// <returns>The same RebindingOperation instance.</returns>
  1119. /// <remarks>
  1120. /// By default, noisy controls are ignored for rebinds. This means that, for example, a gyro
  1121. /// inside a gamepad will not be considered as a potential candidate control as it is hard
  1122. /// to tell valid user interaction on the control apart from random jittering that occurs
  1123. /// on noisy controls.
  1124. ///
  1125. /// By calling this method, this behavior can be disabled. This is usually only useful when
  1126. /// implementing custom candidate selection through <see cref="OnPotentialMatch"/>.
  1127. /// </remarks>
  1128. /// <seealso cref="InputControl.noisy"/>
  1129. public RebindingOperation WithoutIgnoringNoisyControls()
  1130. {
  1131. ThrowIfRebindInProgress();
  1132. m_Flags |= Flags.DontIgnoreNoisyControls;
  1133. return this;
  1134. }
  1135. /// <summary>
  1136. /// Restrict candidate controls using a control path (see <see cref="InputControlPath"/>).
  1137. /// </summary>
  1138. /// <param name="path">A control path. See <see cref="InputControlPath"/>.</param>
  1139. /// <returns>The same RebindingOperation instance.</returns>
  1140. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c> or empty.</exception>
  1141. /// <remarks>
  1142. /// This method is most useful to, for example, restrict controls to specific types of devices.
  1143. /// If, say, you want to let the player only bind to gamepads, you can do so using
  1144. ///
  1145. /// <example>
  1146. /// <code>
  1147. /// rebind.WithControlsHavingToMatchPath("&lt;Gamepad&gt;");
  1148. /// </code>
  1149. /// </example>
  1150. ///
  1151. /// This method can be called repeatedly to add multiple paths. The effect is that candidates
  1152. /// are accepted if <em>any</em> of the given paths matches. To reset the list, call <see
  1153. /// cref="Reset"/>.
  1154. /// </remarks>
  1155. public RebindingOperation WithControlsHavingToMatchPath(string path)
  1156. {
  1157. ThrowIfRebindInProgress();
  1158. if (string.IsNullOrEmpty(path))
  1159. throw new ArgumentNullException(nameof(path));
  1160. for (var i = 0; i < m_IncludePathCount; ++i)
  1161. if (string.Compare(m_IncludePaths[i], path, StringComparison.InvariantCultureIgnoreCase) == 0)
  1162. return this;
  1163. ArrayHelpers.AppendWithCapacity(ref m_IncludePaths, ref m_IncludePathCount, path);
  1164. return this;
  1165. }
  1166. /// <summary>
  1167. /// Prevent specific controls from being considered as candidate controls.
  1168. /// </summary>
  1169. /// <param name="path">A control path. See <see cref="InputControlPath"/>.</param>
  1170. /// <returns>The same RebindingOperation instance.</returns>
  1171. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c> or empty.</exception>
  1172. /// <remarks>
  1173. /// Some controls can be undesirable to include in the candidate selection process even
  1174. /// though they constitute valid, non-noise user input. For example, in a desktop application,
  1175. /// the mouse will usually be used to navigate the UI including a rebinding UI that makes
  1176. /// use of RebindingOperation. It can thus be advisable to exclude specific pointer controls
  1177. /// like so:
  1178. ///
  1179. /// <example>
  1180. /// <code>
  1181. /// rebind
  1182. /// .WithControlsExcluding("&lt;Pointer&gt;/position") // Don't bind to mouse position
  1183. /// .WithControlsExcluding("&lt;Pointer&gt;/delta") // Don't bind to mouse movement deltas
  1184. /// .WithControlsExcluding("&lt;Pointer&gt;/{PrimaryAction}") // don't bind to controls such as leftButton and taps.
  1185. /// </code>
  1186. /// </example>
  1187. ///
  1188. /// This method can be called repeatedly to add multiple exclusions. To reset the list,
  1189. /// call <see cref="Reset"/>.
  1190. /// </remarks>
  1191. public RebindingOperation WithControlsExcluding(string path)
  1192. {
  1193. ThrowIfRebindInProgress();
  1194. if (string.IsNullOrEmpty(path))
  1195. throw new ArgumentNullException(nameof(path));
  1196. for (var i = 0; i < m_ExcludePathCount; ++i)
  1197. if (string.Compare(m_ExcludePaths[i], path, StringComparison.InvariantCultureIgnoreCase) == 0)
  1198. return this;
  1199. ArrayHelpers.AppendWithCapacity(ref m_ExcludePaths, ref m_ExcludePathCount, path);
  1200. return this;
  1201. }
  1202. public RebindingOperation WithTimeout(float timeInSeconds)
  1203. {
  1204. m_Timeout = timeInSeconds;
  1205. return this;
  1206. }
  1207. public RebindingOperation OnComplete(Action<RebindingOperation> callback)
  1208. {
  1209. m_OnComplete = callback;
  1210. return this;
  1211. }
  1212. public RebindingOperation OnCancel(Action<RebindingOperation> callback)
  1213. {
  1214. m_OnCancel = callback;
  1215. return this;
  1216. }
  1217. public RebindingOperation OnPotentialMatch(Action<RebindingOperation> callback)
  1218. {
  1219. m_OnPotentialMatch = callback;
  1220. return this;
  1221. }
  1222. /// <summary>
  1223. /// Set function to call when generating the final binding path for a control
  1224. /// that has been selected.
  1225. /// </summary>
  1226. /// <param name="callback">Delegate to call </param>
  1227. /// <returns></returns>
  1228. public RebindingOperation OnGeneratePath(Func<InputControl, string> callback)
  1229. {
  1230. m_OnGeneratePath = callback;
  1231. return this;
  1232. }
  1233. public RebindingOperation OnComputeScore(Func<InputControl, InputEventPtr, float> callback)
  1234. {
  1235. m_OnComputeScore = callback;
  1236. return this;
  1237. }
  1238. public RebindingOperation OnApplyBinding(Action<RebindingOperation, string> callback)
  1239. {
  1240. m_OnApplyBinding = callback;
  1241. return this;
  1242. }
  1243. public RebindingOperation OnMatchWaitForAnother(float seconds)
  1244. {
  1245. m_WaitSecondsAfterMatch = seconds;
  1246. return this;
  1247. }
  1248. public RebindingOperation Start()
  1249. {
  1250. // Ignore if already started.
  1251. if (started)
  1252. return this;
  1253. // Make sure our configuration is sound.
  1254. if (m_ActionToRebind != null && m_ActionToRebind.bindings.Count == 0 && (m_Flags & Flags.AddNewBinding) == 0)
  1255. throw new InvalidOperationException(
  1256. $"Action '{action}' must have at least one existing binding or must be used with WithRebindingAddNewBinding()");
  1257. if (m_ActionToRebind == null && m_OnApplyBinding == null)
  1258. throw new InvalidOperationException(
  1259. "Must either have an action (call WithAction()) to apply binding to or have a custom callback to apply the binding (call OnApplyBinding())");
  1260. m_StartTime = InputRuntime.s_Instance.currentTime;
  1261. if (m_WaitSecondsAfterMatch > 0 || m_Timeout > 0)
  1262. {
  1263. HookOnAfterUpdate();
  1264. m_LastMatchTime = -1;
  1265. }
  1266. HookOnEvent();
  1267. m_Flags |= Flags.Started;
  1268. m_Flags &= ~Flags.Canceled;
  1269. m_Flags &= ~Flags.Completed;
  1270. return this;
  1271. }
  1272. public void Cancel()
  1273. {
  1274. if (!started)
  1275. return;
  1276. OnCancel();
  1277. }
  1278. /// <summary>
  1279. /// Manually complete the rebinding operation.
  1280. /// </summary>
  1281. public void Complete()
  1282. {
  1283. if (!started)
  1284. return;
  1285. OnComplete();
  1286. }
  1287. public void AddCandidate(InputControl control, float score, float magnitude = -1)
  1288. {
  1289. if (control == null)
  1290. throw new ArgumentNullException(nameof(control));
  1291. // If it's already added, update score.
  1292. var index = m_Candidates.IndexOf(control);
  1293. if (index != -1)
  1294. {
  1295. m_Scores[index] = score;
  1296. }
  1297. else
  1298. {
  1299. // Otherwise, add it.
  1300. var scoreCount = m_Candidates.Count;
  1301. var magnitudeCount = m_Candidates.Count;
  1302. m_Candidates.Add(control);
  1303. ArrayHelpers.AppendWithCapacity(ref m_Scores, ref scoreCount, score);
  1304. ArrayHelpers.AppendWithCapacity(ref m_Magnitudes, ref magnitudeCount, magnitude);
  1305. }
  1306. SortCandidatesByScore();
  1307. }
  1308. public void RemoveCandidate(InputControl control)
  1309. {
  1310. if (control == null)
  1311. throw new ArgumentNullException(nameof(control));
  1312. var index = m_Candidates.IndexOf(control);
  1313. if (index == -1)
  1314. return;
  1315. var candidateCount = m_Candidates.Count;
  1316. m_Candidates.RemoveAt(index);
  1317. ArrayHelpers.EraseAtWithCapacity(m_Scores, ref candidateCount, index);
  1318. }
  1319. public void Dispose()
  1320. {
  1321. UnhookOnEvent();
  1322. UnhookOnAfterUpdate();
  1323. m_Candidates.Dispose();
  1324. m_LayoutCache.Clear();
  1325. }
  1326. ~RebindingOperation()
  1327. {
  1328. Dispose();
  1329. }
  1330. /// <summary>
  1331. /// Reset the configuration on the rebind.
  1332. /// </summary>
  1333. /// <returns>The same RebindingOperation instance.</returns>
  1334. /// <remarks>
  1335. /// Call this method to reset the effects of calling methods such as <see cref="WithAction"/>,
  1336. /// <see cref="WithBindingGroup"/>, etc. but retain other data that the rebind operation
  1337. /// may have allocated already. If you are reusing the same <c>RebindingOperation</c>
  1338. /// multiple times, a good strategy is to reset and reconfigure the operation before starting
  1339. /// it again.
  1340. /// </remarks>
  1341. public RebindingOperation Reset()
  1342. {
  1343. Cancel();
  1344. m_ActionToRebind = default;
  1345. m_BindingMask = default;
  1346. m_ControlType = default;
  1347. m_ExpectedLayout = default;
  1348. m_IncludePathCount = default;
  1349. m_ExcludePathCount = default;
  1350. m_TargetBindingIndex = -1;
  1351. m_BindingGroupForNewBinding = default;
  1352. m_CancelBinding = default;
  1353. m_MagnitudeThreshold = kDefaultMagnitudeThreshold;
  1354. m_Timeout = default;
  1355. m_WaitSecondsAfterMatch = default;
  1356. m_Flags = default;
  1357. m_StartingActuationControls.Clear();
  1358. m_StartingActuationsCount = 0;
  1359. return this;
  1360. }
  1361. private void HookOnEvent()
  1362. {
  1363. if ((m_Flags & Flags.OnEventHooked) != 0)
  1364. return;
  1365. if (m_OnEventDelegate == null)
  1366. m_OnEventDelegate = OnEvent;
  1367. InputSystem.onEvent += m_OnEventDelegate;
  1368. m_Flags |= Flags.OnEventHooked;
  1369. }
  1370. private void UnhookOnEvent()
  1371. {
  1372. if ((m_Flags & Flags.OnEventHooked) == 0)
  1373. return;
  1374. InputSystem.onEvent -= m_OnEventDelegate;
  1375. m_Flags &= ~Flags.OnEventHooked;
  1376. }
  1377. private unsafe void OnEvent(InputEventPtr eventPtr, InputDevice device)
  1378. {
  1379. // Ignore if not a state event.
  1380. if (!eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>())
  1381. return;
  1382. ////TODO: add callback that shows the candidate *and* the event to the user (this is particularly useful when we are suppressing
  1383. //// and thus throwing away events)
  1384. // Go through controls and see if there's anything interesting in the event.
  1385. // NOTE: We go through quite a few steps and operations here. However, the chief goal here is trying to be as robust
  1386. // as we can in isolating the control the user really means to single out. If this code here does its job, that
  1387. // control should always pop up as the first entry in the candidates list (if the configuration of the rebind
  1388. // operation is otherwise sane).
  1389. var controls = device.allControls;
  1390. var controlCount = controls.Count;
  1391. var haveChangedCandidates = false;
  1392. var suppressEvent = false;
  1393. for (var i = 0; i < controlCount; ++i)
  1394. {
  1395. var control = controls[i];
  1396. // Skip controls that have no state in the event.
  1397. var statePtr = control.GetStatePtrFromStateEvent(eventPtr);
  1398. if (statePtr == null)
  1399. continue;
  1400. // If the control that cancels has been actuated, abort the operation now.
  1401. if (!string.IsNullOrEmpty(m_CancelBinding) && InputControlPath.Matches(m_CancelBinding, control) &&
  1402. !control.CheckStateIsAtDefault(statePtr) && control.HasValueChangeInState(statePtr))
  1403. {
  1404. OnCancel();
  1405. break;
  1406. }
  1407. // Skip noisy controls.
  1408. if (control.noisy && (m_Flags & Flags.DontIgnoreNoisyControls) == 0)
  1409. continue;
  1410. // If controls must not match certain paths, make sure the control doesn't.
  1411. if (m_ExcludePathCount > 0 && HavePathMatch(control, m_ExcludePaths, m_ExcludePathCount))
  1412. continue;
  1413. // The control is not explicitly excluded so we suppress the event, if that's enabled.
  1414. suppressEvent = true;
  1415. // If controls have to match a certain path, check if this one does.
  1416. if (m_IncludePathCount > 0 && !HavePathMatch(control, m_IncludePaths, m_IncludePathCount))
  1417. continue;
  1418. // If we're expecting controls of a certain type, skip if control isn't of
  1419. // the right type.
  1420. if (m_ControlType != null && !m_ControlType.IsInstanceOfType(control))
  1421. continue;
  1422. // If we're expecting controls to be based on a specific layout, skip if control
  1423. // isn't based on that layout.
  1424. if (!m_ExpectedLayout.IsEmpty() &&
  1425. m_ExpectedLayout != control.m_Layout &&
  1426. !InputControlLayout.s_Layouts.IsBasedOn(m_ExpectedLayout, control.m_Layout))
  1427. continue;
  1428. // Skip controls that are in their default state.
  1429. // NOTE: This is the cheapest check with respect to looking at actual state. So
  1430. // do this first before looking further at the state.
  1431. if (control.CheckStateIsAtDefault(statePtr))
  1432. {
  1433. // For controls that were already actuated when we started the rebind, we record starting actuations below.
  1434. // However, when such a control goes back to default state, we want to reset that recorded value. This makes
  1435. // sure that if, for example, a key is down when the rebind started, when the key is released and then pressed
  1436. // again, we don't compare to the previously recorded magnitude of 1 but rather to 0.
  1437. var staringActuationIndex = m_StartingActuationControls.IndexOfReference(control);
  1438. if (staringActuationIndex != -1)
  1439. m_StaringActuationMagnitudes[staringActuationIndex] = 0;
  1440. continue;
  1441. }
  1442. var magnitude = control.EvaluateMagnitude(statePtr);
  1443. if (magnitude >= 0)
  1444. {
  1445. // Determine starting actuation.
  1446. float startingMagnitude;
  1447. var startingActuationIndex = m_StartingActuationControls.IndexOfReference(control);
  1448. if (startingActuationIndex != -1)
  1449. {
  1450. // We have seen this control start actuating before and have recorded its starting actuation.
  1451. startingMagnitude = m_StaringActuationMagnitudes[startingActuationIndex];
  1452. }
  1453. else
  1454. {
  1455. // Haven't seen this control changing actuation yet. Record its current actuation as its
  1456. // starting actuation and ignore the control if we haven't reached our actuation threshold yet.
  1457. startingMagnitude = control.EvaluateMagnitude();
  1458. var count = m_StartingActuationsCount;
  1459. ArrayHelpers.AppendWithCapacity(ref m_StartingActuationControls, ref m_StartingActuationsCount, control);
  1460. ArrayHelpers.AppendWithCapacity(ref m_StaringActuationMagnitudes, ref count, startingMagnitude);
  1461. }
  1462. // Ignore control if it hasn't exceeded the magnitude threshold relative to its starting actuation yet.
  1463. if (Mathf.Abs(startingMagnitude - magnitude) < m_MagnitudeThreshold)
  1464. continue;
  1465. }
  1466. ////REVIEW: this would be more useful by providing the default score *to* the callback (which may alter it or just replace it altogether)
  1467. // Compute score.
  1468. float score;
  1469. if (m_OnComputeScore != null)
  1470. {
  1471. score = m_OnComputeScore(control, eventPtr);
  1472. }
  1473. else
  1474. {
  1475. score = magnitude;
  1476. // We don't want synthetic controls to not be bindable at all but they should
  1477. // generally cede priority to controls that aren't synthetic. So we bump all
  1478. // scores of controls that aren't synthetic.
  1479. if (!control.synthetic)
  1480. score += 1f;
  1481. }
  1482. // Control is a candidate.
  1483. // See if we already singled the control out as a potential candidate.
  1484. var candidateIndex = m_Candidates.IndexOf(control);
  1485. if (candidateIndex != -1)
  1486. {
  1487. // Yes, we did. So just check whether it became a better candidate than before.
  1488. if (m_Scores[candidateIndex] < score)
  1489. {
  1490. haveChangedCandidates = true;
  1491. m_Scores[candidateIndex] = score;
  1492. if (m_WaitSecondsAfterMatch > 0)
  1493. m_LastMatchTime = InputRuntime.s_Instance.currentTime;
  1494. }
  1495. }
  1496. else
  1497. {
  1498. // No, so add it.
  1499. var scoreCount = m_Candidates.Count;
  1500. var magnitudeCount = m_Candidates.Count;
  1501. m_Candidates.Add(control);
  1502. ArrayHelpers.AppendWithCapacity(ref m_Scores, ref scoreCount, score);
  1503. ArrayHelpers.AppendWithCapacity(ref m_Magnitudes, ref magnitudeCount, magnitude);
  1504. haveChangedCandidates = true;
  1505. if (m_WaitSecondsAfterMatch > 0)
  1506. m_LastMatchTime = InputRuntime.s_Instance.currentTime;
  1507. }
  1508. }
  1509. // See if we should suppress the event. If so, mark it handled so that the input manager
  1510. // will skip further processing of the event.
  1511. if (suppressEvent && (m_Flags & Flags.SuppressMatchingEvents) != 0)
  1512. eventPtr.handled = true;
  1513. if (haveChangedCandidates && !canceled)
  1514. {
  1515. // If we have a callback that wants to control matching, leave it to the callback to decide
  1516. // whether the rebind is complete or not. Otherwise, just complete.
  1517. if (m_OnPotentialMatch != null)
  1518. {
  1519. SortCandidatesByScore();
  1520. m_OnPotentialMatch(this);
  1521. }
  1522. else if (m_WaitSecondsAfterMatch <= 0)
  1523. {
  1524. OnComplete();
  1525. }
  1526. else
  1527. {
  1528. SortCandidatesByScore();
  1529. }
  1530. }
  1531. }
  1532. private void SortCandidatesByScore()
  1533. {
  1534. var candidateCount = m_Candidates.Count;
  1535. if (candidateCount <= 1)
  1536. return;
  1537. // Simple insertion sort that sorts both m_Candidates and m_Scores at the same time.
  1538. // Note that we're sorting by *decreasing* score here, not by increasing score.
  1539. for (var i = 1; i < candidateCount; ++i)
  1540. {
  1541. for (var j = i; j > 0 && m_Scores[j - 1] < m_Scores[j]; --j)
  1542. {
  1543. m_Scores.SwapElements(j, j - 1);
  1544. m_Candidates.SwapElements(j, j - 1);
  1545. m_Magnitudes.SwapElements(i, j - 1);
  1546. }
  1547. }
  1548. }
  1549. private static bool HavePathMatch(InputControl control, string[] paths, int pathCount)
  1550. {
  1551. for (var i = 0; i < pathCount; ++i)
  1552. {
  1553. if (InputControlPath.MatchesPrefix(paths[i], control))
  1554. return true;
  1555. }
  1556. return false;
  1557. }
  1558. private void HookOnAfterUpdate()
  1559. {
  1560. if ((m_Flags & Flags.OnAfterUpdateHooked) != 0)
  1561. return;
  1562. if (m_OnAfterUpdateDelegate == null)
  1563. m_OnAfterUpdateDelegate = OnAfterUpdate;
  1564. InputSystem.onAfterUpdate += m_OnAfterUpdateDelegate;
  1565. m_Flags |= Flags.OnAfterUpdateHooked;
  1566. }
  1567. private void UnhookOnAfterUpdate()
  1568. {
  1569. if ((m_Flags & Flags.OnAfterUpdateHooked) == 0)
  1570. return;
  1571. InputSystem.onAfterUpdate -= m_OnAfterUpdateDelegate;
  1572. m_Flags &= ~Flags.OnAfterUpdateHooked;
  1573. }
  1574. private void OnAfterUpdate()
  1575. {
  1576. // If we don't have a match yet but we have a timeout and have expired it,
  1577. // cancel the operation.
  1578. if (m_LastMatchTime < 0 && m_Timeout > 0 &&
  1579. InputRuntime.s_Instance.currentTime - m_StartTime > m_Timeout)
  1580. {
  1581. Cancel();
  1582. return;
  1583. }
  1584. // Sanity check to make sure we're actually waiting for completion.
  1585. if (m_WaitSecondsAfterMatch <= 0)
  1586. return;
  1587. // Can't complete if we have no match yet.
  1588. if (m_LastMatchTime < 0)
  1589. return;
  1590. // Complete if timeout has expired.
  1591. if (InputRuntime.s_Instance.currentTime >= m_LastMatchTime + m_WaitSecondsAfterMatch)
  1592. Complete();
  1593. }
  1594. private void OnComplete()
  1595. {
  1596. SortCandidatesByScore();
  1597. if (m_Candidates.Count > 0)
  1598. {
  1599. // Create a path from the selected control.
  1600. var selectedControl = m_Candidates[0];
  1601. var path = selectedControl.path;
  1602. if (m_OnGeneratePath != null)
  1603. {
  1604. // We have a callback. Give it a shot to generate a path. If it doesn't,
  1605. // fall back to our default logic.
  1606. var newPath = m_OnGeneratePath(selectedControl);
  1607. if (!string.IsNullOrEmpty(newPath))
  1608. path = newPath;
  1609. else if ((m_Flags & Flags.DontGeneralizePathOfSelectedControl) == 0)
  1610. path = GeneratePathForControl(selectedControl);
  1611. }
  1612. else if ((m_Flags & Flags.DontGeneralizePathOfSelectedControl) == 0)
  1613. path = GeneratePathForControl(selectedControl);
  1614. // If we have a custom callback for applying the binding, let it handle
  1615. // everything.
  1616. if (m_OnApplyBinding != null)
  1617. m_OnApplyBinding(this, path);
  1618. else
  1619. {
  1620. Debug.Assert(m_ActionToRebind != null);
  1621. // See if we should modify an existing binding or create a new one.
  1622. if ((m_Flags & Flags.AddNewBinding) != 0)
  1623. {
  1624. // Create new binding.
  1625. m_ActionToRebind.AddBinding(path, groups: m_BindingGroupForNewBinding);
  1626. }
  1627. else
  1628. {
  1629. // Apply binding override to existing binding.
  1630. if (m_TargetBindingIndex >= 0)
  1631. {
  1632. if (m_TargetBindingIndex >= m_ActionToRebind.bindings.Count)
  1633. throw new InvalidOperationException(
  1634. $"Target binding index {m_TargetBindingIndex} out of range for action '{m_ActionToRebind}' with {m_ActionToRebind.bindings.Count} bindings");
  1635. m_ActionToRebind.ApplyBindingOverride(m_TargetBindingIndex, path);
  1636. }
  1637. else if (m_BindingMask != null)
  1638. {
  1639. var bindingOverride = m_BindingMask.Value;
  1640. bindingOverride.overridePath = path;
  1641. m_ActionToRebind.ApplyBindingOverride(bindingOverride);
  1642. }
  1643. else
  1644. {
  1645. m_ActionToRebind.ApplyBindingOverride(path);
  1646. }
  1647. }
  1648. }
  1649. }
  1650. // Complete.
  1651. m_Flags |= Flags.Completed;
  1652. m_OnComplete?.Invoke(this);
  1653. ResetAfterMatchCompleted();
  1654. }
  1655. private void OnCancel()
  1656. {
  1657. m_Flags |= Flags.Canceled;
  1658. m_OnCancel?.Invoke(this);
  1659. ResetAfterMatchCompleted();
  1660. }
  1661. private void ResetAfterMatchCompleted()
  1662. {
  1663. m_Flags &= ~Flags.Started;
  1664. m_Candidates.Clear();
  1665. m_Candidates.Capacity = 0; // Release our unmanaged memory.
  1666. m_StartTime = -1;
  1667. m_StartingActuationControls.Clear();
  1668. m_StartingActuationsCount = 0;
  1669. UnhookOnEvent();
  1670. UnhookOnAfterUpdate();
  1671. }
  1672. private void ThrowIfRebindInProgress()
  1673. {
  1674. if (started)
  1675. throw new InvalidOperationException("Cannot reconfigure rebinding while operation is in progress");
  1676. }
  1677. /// <summary>
  1678. /// Based on the chosen control, generate an override path to rebind to.
  1679. /// </summary>
  1680. private string GeneratePathForControl(InputControl control)
  1681. {
  1682. var device = control.device;
  1683. Debug.Assert(control != device, "Control must not be a device");
  1684. var deviceLayoutName =
  1685. InputControlLayout.s_Layouts.FindLayoutThatIntroducesControl(control, m_LayoutCache);
  1686. if (m_PathBuilder == null)
  1687. m_PathBuilder = new StringBuilder();
  1688. else
  1689. m_PathBuilder.Length = 0;
  1690. control.BuildPath(deviceLayoutName, m_PathBuilder);
  1691. return m_PathBuilder.ToString();
  1692. }
  1693. private InputAction m_ActionToRebind;
  1694. private InputBinding? m_BindingMask;
  1695. private Type m_ControlType;
  1696. private InternedString m_ExpectedLayout;
  1697. private int m_IncludePathCount;
  1698. private string[] m_IncludePaths;
  1699. private int m_ExcludePathCount;
  1700. private string[] m_ExcludePaths;
  1701. private int m_TargetBindingIndex = -1;
  1702. private string m_BindingGroupForNewBinding;
  1703. private string m_CancelBinding;
  1704. private float m_MagnitudeThreshold = kDefaultMagnitudeThreshold;
  1705. private float[] m_Scores; // Scores for the controls in m_Candidates.
  1706. private float[] m_Magnitudes;
  1707. private double m_LastMatchTime; // Last input event time we discovered a better match.
  1708. private double m_StartTime;
  1709. private float m_Timeout;
  1710. private float m_WaitSecondsAfterMatch;
  1711. private InputControlList<InputControl> m_Candidates;
  1712. private Action<RebindingOperation> m_OnComplete;
  1713. private Action<RebindingOperation> m_OnCancel;
  1714. private Action<RebindingOperation> m_OnPotentialMatch;
  1715. private Func<InputControl, string> m_OnGeneratePath;
  1716. private Func<InputControl, InputEventPtr, float> m_OnComputeScore;
  1717. private Action<RebindingOperation, string> m_OnApplyBinding;
  1718. private Action<InputEventPtr, InputDevice> m_OnEventDelegate;
  1719. private Action m_OnAfterUpdateDelegate;
  1720. ////TODO: use global cache
  1721. private InputControlLayout.Cache m_LayoutCache;
  1722. private StringBuilder m_PathBuilder;
  1723. private Flags m_Flags;
  1724. // Controls may already be actuated by the time we start a rebind. For those, we track starting actutations
  1725. // individually and require them to cross the actuation threshold WRT the starting actuation.
  1726. private int m_StartingActuationsCount;
  1727. private float[] m_StaringActuationMagnitudes;
  1728. private InputControl[] m_StartingActuationControls;
  1729. [Flags]
  1730. private enum Flags
  1731. {
  1732. Started = 1 << 0,
  1733. Completed = 1 << 1,
  1734. Canceled = 1 << 2,
  1735. OnEventHooked = 1 << 3,
  1736. OnAfterUpdateHooked = 1 << 4,
  1737. DontIgnoreNoisyControls = 1 << 6,
  1738. DontGeneralizePathOfSelectedControl = 1 << 7,
  1739. AddNewBinding = 1 << 8,
  1740. SuppressMatchingEvents = 1 << 9,
  1741. }
  1742. }
  1743. /// <summary>
  1744. /// Initiate an operation that interactively rebinds the given action based on received input.
  1745. /// </summary>
  1746. /// <param name="action">Action to perform rebinding on.</param>
  1747. /// <param name="bindingIndex">Optional index (within the <see cref="InputAction.bindings"/> array of <paramref name="action"/>)
  1748. /// of binding to perform rebinding on. Must not be a composite binding.</param>
  1749. /// <returns>A rebind operation configured to perform the rebind.</returns>
  1750. /// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
  1751. /// <exception cref="ArgumentOutOfRangeException"><paramref name="bindingIndex"/> is not a valid index.</exception>
  1752. /// <exception cref="InvalidOperationException">The binding at <paramref name="bindingIndex"/> is a composite binding.</exception>
  1753. /// <remarks>
  1754. /// This method will automatically perform a set of configuration on the <see cref="RebindingOperation"/>
  1755. /// based on the action and, if specified, binding.
  1756. ///
  1757. /// TODO
  1758. ///
  1759. /// Note that rebind operations must be disposed of once finished in order to not leak memory.
  1760. ///
  1761. /// <example>
  1762. /// <code>
  1763. /// // Target the first binding in the gamepad scheme.
  1764. /// var bindingIndex = myAction.GetBindingIndex(InputBinding.MaskByGroup("Gamepad"));
  1765. /// var rebind = myAction.PerformInteractiveRebinding(bindingIndex);
  1766. ///
  1767. /// // Dispose the operation on completion.
  1768. /// rebind.OnComplete(
  1769. /// operation =>
  1770. /// {
  1771. /// Debug.Log($"Rebound '{myAction}' to '{operation.selectedControl}'");
  1772. /// operation.Dispose();
  1773. /// };
  1774. ///
  1775. /// // Start the rebind. This will cause the rebind operation to start running in the
  1776. /// // background listening for input.
  1777. /// rebind.Start();
  1778. /// </code>
  1779. /// </example>
  1780. /// </remarks>
  1781. public static RebindingOperation PerformInteractiveRebinding(this InputAction action, int bindingIndex = -1)
  1782. {
  1783. if (action == null)
  1784. throw new ArgumentNullException(nameof(action));
  1785. var rebind = new RebindingOperation()
  1786. .WithAction(action)
  1787. // Give it an ever so slight delay to make sure there isn't a better match immediately
  1788. // following the current event.
  1789. .OnMatchWaitForAnother(0.05f)
  1790. // It doesn't really make sense to interactively bind pointer position input as interactive
  1791. // rebinds are usually initiated from UIs which are operated by pointers. So exclude pointer
  1792. // position controls by default.
  1793. .WithControlsExcluding("<Pointer>/delta")
  1794. .WithControlsExcluding("<Pointer>/position")
  1795. .WithControlsExcluding("<Touchscreen>/touch*/position")
  1796. .WithControlsExcluding("<Touchscreen>/touch*/delta")
  1797. .WithControlsExcluding("<Mouse>/clickCount")
  1798. .WithMatchingEventsBeingSuppressed();
  1799. // If we're not looking for a button, automatically add keyboard escape key to abort rebind.
  1800. if (rebind.expectedControlType != "Button")
  1801. rebind.WithCancelingThrough("<Keyboard>/escape");
  1802. if (bindingIndex >= 0)
  1803. {
  1804. var bindings = action.bindings;
  1805. if (bindingIndex >= bindings.Count)
  1806. throw new ArgumentOutOfRangeException(
  1807. $"Binding index {bindingIndex} is out of range for action '{action}' with {bindings.Count} bindings",
  1808. nameof(bindings));
  1809. if (bindings[bindingIndex].isComposite)
  1810. throw new InvalidOperationException(
  1811. $"Cannot perform rebinding on composite binding '{bindings[bindingIndex]}' of '{action}'");
  1812. rebind.WithTargetBinding(bindingIndex);
  1813. // If the binding is a part binding, switch from the action's expected control type to
  1814. // that expected by the composite's part.
  1815. if (bindings[bindingIndex].isPartOfComposite)
  1816. {
  1817. // Search for composite.
  1818. var compositeIndex = bindingIndex - 1;
  1819. while (compositeIndex > 0 && !bindings[compositeIndex].isComposite)
  1820. --compositeIndex;
  1821. if (compositeIndex >= 0 && bindings[compositeIndex].isComposite)
  1822. {
  1823. var compositeName = bindings[compositeIndex].GetNameOfComposite();
  1824. var controlTypeExpectedByPart = InputBindingComposite.GetExpectedControlLayoutName(compositeName, bindings[bindingIndex].name);
  1825. if (!string.IsNullOrEmpty(controlTypeExpectedByPart))
  1826. rebind.WithExpectedControlType(controlTypeExpectedByPart);
  1827. }
  1828. }
  1829. // If the binding is part of a control scheme, only accept controls
  1830. // that also match device requirements.
  1831. var bindingGroups = bindings[bindingIndex].groups;
  1832. var asset = action.actionMap?.asset;
  1833. if (asset != null && !string.IsNullOrEmpty(action.bindings[bindingIndex].groups))
  1834. {
  1835. foreach (var group in bindingGroups.Split(InputBinding.Separator))
  1836. {
  1837. var controlSchemeIndex =
  1838. asset.controlSchemes.IndexOf(x => group.Equals(x.bindingGroup, StringComparison.InvariantCultureIgnoreCase));
  1839. if (controlSchemeIndex == -1)
  1840. continue;
  1841. ////TODO: make this deal with and/or requirements
  1842. var controlScheme = asset.controlSchemes[controlSchemeIndex];
  1843. foreach (var requirement in controlScheme.deviceRequirements)
  1844. rebind.WithControlsHavingToMatchPath(requirement.controlPath);
  1845. }
  1846. }
  1847. }
  1848. return rebind;
  1849. }
  1850. /// <summary>
  1851. /// Temporarily suspend immediate re-resolution of bindings.
  1852. /// </summary>
  1853. /// <remarks>
  1854. /// When changing control setups, it may take multiple steps to get to the final setup but each individual
  1855. /// step may trigger bindings to be resolved again in order to update controls on actions (see <see cref="InputAction.controls"/>).
  1856. /// Using this struct, this can be avoided and binding resolution can be deferred to after the whole operation
  1857. /// is complete and the final binding setup is in place.
  1858. /// </remarks>
  1859. internal static IDisposable DeferBindingResolution()
  1860. {
  1861. if (s_DeferBindingResolutionWrapper == null)
  1862. s_DeferBindingResolutionWrapper = new DeferBindingResolutionWrapper();
  1863. s_DeferBindingResolutionWrapper.Acquire();
  1864. return s_DeferBindingResolutionWrapper;
  1865. }
  1866. private static DeferBindingResolutionWrapper s_DeferBindingResolutionWrapper;
  1867. private class DeferBindingResolutionWrapper : IDisposable
  1868. {
  1869. public void Acquire()
  1870. {
  1871. ++InputActionMap.s_DeferBindingResolution;
  1872. }
  1873. public void Dispose()
  1874. {
  1875. if (InputActionMap.s_DeferBindingResolution > 0)
  1876. --InputActionMap.s_DeferBindingResolution;
  1877. InputActionState.DeferredResolutionOfBindings();
  1878. }
  1879. }
  1880. }
  1881. }