Ingen beskrivning
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

InputActionRebindingExtensions.cs 144KB

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