InputControlScheme.cs 51 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using System.Text;
  6. using Unity.Collections;
  7. using UnityEngine.InputSystem.Layouts;
  8. using UnityEngine.InputSystem.Utilities;
  9. ////TODO: introduce the concept of a "variation"
  10. //// - a variation is just a variant of a control scheme, not a full control scheme by itself
  11. //// - an individual variation can be toggled on and off independently
  12. //// - while a control is is active, all its variations that are toggled on are also active
  13. //// - assignment to variations works the same as assignment to control schemes
  14. //// use case: left/right stick toggles, left/right bumper toggles, etc
  15. ////TODO: introduce concept of precedence where one control scheme will be preferred over another that is also a match
  16. //// (might be its enough to represent this simply through ordering by giving the user control over the ordering through the UI)
  17. ////REVIEW: allow associating control schemes with platforms, too?
  18. namespace UnityEngine.InputSystem
  19. {
  20. /// <summary>
  21. /// A named set of zero or more device requirements along with an associated binding group.
  22. /// </summary>
  23. /// <remarks>
  24. /// Control schemes provide an additional layer on top of binding groups. While binding
  25. /// groups allow differentiating sets of bindings (e.g. a "Keyboard&amp;Mouse" group versus
  26. /// a "Gamepad" group), control schemes impose a set of devices requirements that must be
  27. /// met in order for a specific set of bindings to be usable.
  28. ///
  29. /// Note that control schemes can only be defined at the <see cref="InputActionAsset"/> level.
  30. /// </remarks>
  31. /// <seealso cref="InputActionAsset.controlSchemes"/>
  32. /// <seealso cref="InputActionSetupExtensions.AddControlScheme(InputActionAsset,string)"/>
  33. [Serializable]
  34. public struct InputControlScheme : IEquatable<InputControlScheme>
  35. {
  36. /// <summary>
  37. /// Name of the control scheme. Not <c>null</c> or empty except if InputControlScheme
  38. /// instance is invalid (i.e. default-initialized).
  39. /// </summary>
  40. /// <value>Name of the scheme.</value>
  41. /// <remarks>
  42. /// May be empty or null except if the control scheme is part of an <see cref="InputActionAsset"/>.
  43. /// </remarks>
  44. /// <seealso cref="InputActionSetupExtensions.AddControlScheme(InputActionAsset,string)"/>
  45. public string name => m_Name;
  46. /// <summary>
  47. /// Binding group that is associated with the control scheme. Not <c>null</c> or empty
  48. /// except if InputControlScheme is invalid (i.e. default-initialized).
  49. /// </summary>
  50. /// <value>Binding group for the scheme.</value>
  51. /// <remarks>
  52. /// All bindings in this group are considered to be part of the control scheme.
  53. /// </remarks>
  54. /// <seealso cref="InputBinding.groups"/>
  55. public string bindingGroup
  56. {
  57. get => m_BindingGroup;
  58. set => m_BindingGroup = value;
  59. }
  60. /// <summary>
  61. /// Devices used by the control scheme.
  62. /// </summary>
  63. /// <value>Device requirements of the scheme.</value>
  64. /// <remarks>
  65. /// No two entries will be allowed to match the same control or device at runtime in order for the requirements
  66. /// of the control scheme to be considered satisfied. If, for example, one entry requires a "&lt;Gamepad&gt;" and
  67. /// another entry requires a "&lt;Gamepad&gt;", then at runtime two gamepads will be required even though a single
  68. /// one will match both requirements individually. However, if, for example, one entry requires "&lt;Gamepad&gt;/leftStick"
  69. /// and another requires "&lt;Gamepad&gt;, the same device can match both requirements as each one resolves to
  70. /// a different control.
  71. ///
  72. /// It it allowed to define control schemes without device requirements, i.e. for which this
  73. /// property will be an empty array. Note, however, that features such as automatic control scheme
  74. /// switching in <see cref="PlayerInput"/> will not work with such control schemes.
  75. /// </remarks>
  76. public ReadOnlyArray<DeviceRequirement> deviceRequirements =>
  77. new ReadOnlyArray<DeviceRequirement>(m_DeviceRequirements);
  78. /// <summary>
  79. /// Initialize the control scheme with the given name, device requirements,
  80. /// and binding group.
  81. /// </summary>
  82. /// <param name="name">Name to use for the scheme. Required.</param>
  83. /// <param name="devices">List of device requirements.</param>
  84. /// <param name="bindingGroup">Name to use for the binding group (see <see cref="InputBinding.groups"/>)
  85. /// associated with the control scheme. If this is <c>null</c> or empty, <paramref name="name"/> is
  86. /// used instead (with <see cref="InputBinding.Separator"/> characters stripped from the name).</param>
  87. /// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c> or empty.</exception>
  88. public InputControlScheme(string name, IEnumerable<DeviceRequirement> devices = null, string bindingGroup = null)
  89. : this()
  90. {
  91. if (string.IsNullOrEmpty(name))
  92. throw new ArgumentNullException(nameof(name));
  93. SetNameAndBindingGroup(name, bindingGroup);
  94. m_DeviceRequirements = null;
  95. if (devices != null)
  96. {
  97. m_DeviceRequirements = devices.ToArray();
  98. if (m_DeviceRequirements.Length == 0)
  99. m_DeviceRequirements = null;
  100. }
  101. }
  102. internal void SetNameAndBindingGroup(string name, string bindingGroup = null)
  103. {
  104. m_Name = name;
  105. if (!string.IsNullOrEmpty(bindingGroup))
  106. m_BindingGroup = bindingGroup;
  107. else
  108. m_BindingGroup = name.Contains(InputBinding.Separator)
  109. ? name.Replace(InputBinding.kSeparatorString, "")
  110. : name;
  111. }
  112. /// <summary>
  113. /// Given a list of devices and a list of control schemes, find the most suitable control
  114. /// scheme to use with the devices.
  115. /// </summary>
  116. /// <param name="devices">A list of devices. If the list is empty, only schemes with
  117. /// empty <see cref="deviceRequirements"/> lists will get matched.</param>
  118. /// <param name="schemes">A list of control schemes.</param>
  119. /// <param name="mustIncludeDevice">If not <c>null</c>, a successful match has to include the given device.</param>
  120. /// <param name="allowUnsuccesfulMatch">If true, then allow returning a match that has unsatisfied requirements but still
  121. /// matched at least some requirement. If there are several unsuccessful matches, the returned scheme is still the highest
  122. /// scoring one among those.</param>
  123. /// <typeparam name="TDevices">Collection type to use for the list of devices.</typeparam>
  124. /// <typeparam name="TSchemes">Collection type to use for the list of schemes.</typeparam>
  125. /// <returns>The control scheme that best matched the given devices or <c>null</c> if no
  126. /// scheme was found suitable.</returns>
  127. /// <exception cref="ArgumentNullException"><paramref name="devices"/> is <c>null</c> -or-
  128. /// <paramref name="schemes"/> is <c>null</c>.</exception>
  129. /// <remarks>
  130. /// Any successful match (see <see cref="MatchResult.isSuccessfulMatch"/>) will be considered.
  131. /// The one that matches the most amount of devices (see <see cref="MatchResult.devices"/>)
  132. /// will be returned. If more than one schemes matches equally well, the first one encountered
  133. /// in the list is returned.
  134. ///
  135. /// Note that schemes are not required to match all devices available in the list. The result
  136. /// will simply be the scheme that matched the most devices of what was devices. Use <see
  137. /// cref="PickDevicesFrom{TDevices}"/> to find the devices that a control scheme selects.
  138. ///
  139. /// This method is parameterized over <typeparamref name="TDevices"/> and <typeparamref name="TSchemes"/>
  140. /// to allow avoiding GC heap allocations from boxing of structs such as <see cref="ReadOnlyArray{TValue}"/>.
  141. ///
  142. /// <example>
  143. /// <code>
  144. /// // Create an .inputactions asset.
  145. /// var asset = ScriptableObject.CreateInstance&lt;InputActionAsset&gt;();
  146. ///
  147. /// // Add some control schemes to the asset.
  148. /// asset.AddControlScheme("KeyboardMouse")
  149. /// .WithRequiredDevice&lt;Keyboard&gt;()
  150. /// .WithRequiredDevice&lt;Mouse&gt;());
  151. /// asset.AddControlScheme("Gamepad")
  152. /// .WithRequiredDevice&lt;Gamepad&gt;());
  153. /// asset.AddControlScheme("DualGamepad")
  154. /// .WithRequiredDevice&lt;Gamepad&gt;())
  155. /// .WithOptionalGamepad&lt;Gamepad&gt;());
  156. ///
  157. /// // Add some devices that we can test with.
  158. /// var keyboard = InputSystem.AddDevice&lt;Keyboard&gt;();
  159. /// var mouse = InputSystem.AddDevice&lt;Mouse&gt;();
  160. /// var gamepad1 = InputSystem.AddDevice&lt;Gamepad&gt;();
  161. /// var gamepad2 = InputSystem.AddDevice&lt;Gamepad&gt;();
  162. ///
  163. /// // Matching with just a keyboard won't match any scheme.
  164. /// InputControlScheme.FindControlSchemeForDevices(
  165. /// new InputDevice[] { keyboard }, asset.controlSchemes);
  166. ///
  167. /// // Matching with a keyboard and mouse with match the "KeyboardMouse" scheme.
  168. /// InputControlScheme.FindControlSchemeForDevices(
  169. /// new InputDevice[] { keyboard, mouse }, asset.controlSchemes);
  170. ///
  171. /// // Matching with a single gamepad will match the "Gamepad" scheme.
  172. /// // Note that since the second gamepad is optional in "DualGamepad" could
  173. /// // match the same set of devices but it doesn't match any better than
  174. /// // "Gamepad" and that one comes first in the list.
  175. /// InputControlScheme.FindControlSchemeForDevices(
  176. /// new InputDevice[] { gamepad1 }, asset.controlSchemes);
  177. ///
  178. /// // Matching with two gamepads will match the "DualGamepad" scheme.
  179. /// // Note that "Gamepad" will match this device list as well. If "DualGamepad"
  180. /// // didn't exist, "Gamepad" would be the result here. However, "DualGamepad"
  181. /// // matches the list better than "Gamepad" so that's what gets returned here.
  182. /// InputControlScheme.FindControlSchemeForDevices(
  183. /// new InputDevice[] { gamepad1, gamepad2 }, asset.controlSchemes);
  184. /// </code>
  185. /// </example>
  186. /// </remarks>
  187. public static InputControlScheme? FindControlSchemeForDevices<TDevices, TSchemes>(TDevices devices, TSchemes schemes, InputDevice mustIncludeDevice = null, bool allowUnsuccesfulMatch = false)
  188. where TDevices : IReadOnlyList<InputDevice>
  189. where TSchemes : IEnumerable<InputControlScheme>
  190. {
  191. if (devices == null)
  192. throw new ArgumentNullException(nameof(devices));
  193. if (schemes == null)
  194. throw new ArgumentNullException(nameof(schemes));
  195. if (!FindControlSchemeForDevices(devices, schemes, out var controlScheme, out var matchResult, mustIncludeDevice, allowUnsuccesfulMatch))
  196. return null;
  197. matchResult.Dispose();
  198. return controlScheme;
  199. }
  200. public static bool FindControlSchemeForDevices<TDevices, TSchemes>(TDevices devices, TSchemes schemes,
  201. out InputControlScheme controlScheme, out MatchResult matchResult, InputDevice mustIncludeDevice = null, bool allowUnsuccessfulMatch = false)
  202. where TDevices : IReadOnlyList<InputDevice>
  203. where TSchemes : IEnumerable<InputControlScheme>
  204. {
  205. if (devices == null)
  206. throw new ArgumentNullException(nameof(devices));
  207. if (schemes == null)
  208. throw new ArgumentNullException(nameof(schemes));
  209. MatchResult? bestResult = null;
  210. InputControlScheme? bestScheme = null;
  211. foreach (var scheme in schemes)
  212. {
  213. var result = scheme.PickDevicesFrom(devices, favorDevice: mustIncludeDevice);
  214. // Ignore if scheme doesn't fit devices.
  215. if (!result.isSuccessfulMatch && (!allowUnsuccessfulMatch || result.score <= 0))
  216. {
  217. result.Dispose();
  218. continue;
  219. }
  220. // Ignore if we have a device we specifically want to be part of the result and
  221. // the current match doesn't have it.
  222. if (mustIncludeDevice != null && !result.devices.Contains(mustIncludeDevice))
  223. {
  224. result.Dispose();
  225. continue;
  226. }
  227. // Ignore if it does fit but we already have a better fit.
  228. if (bestResult != null && bestResult.Value.score >= result.score)
  229. {
  230. result.Dispose();
  231. continue;
  232. }
  233. bestResult?.Dispose();
  234. bestResult = result;
  235. bestScheme = scheme;
  236. }
  237. matchResult = bestResult ?? default;
  238. controlScheme = bestScheme ?? default;
  239. return bestResult.HasValue;
  240. }
  241. /// <summary>
  242. /// Return the first control schemes from the given list that supports the given
  243. /// device (see <see cref="SupportsDevice"/>).
  244. /// </summary>
  245. /// <param name="device">An input device.</param>
  246. /// <param name="schemes">A list of control schemes. Can be empty.</param>
  247. /// <typeparam name="TSchemes">Collection type to use for the list of schemes.</typeparam>
  248. /// <returns>The first schemes from <paramref name="schemes"/> that supports <paramref name="device"/>
  249. /// or <c>null</c> if none of the schemes is usable with the device.</returns>
  250. /// <exception cref="ArgumentNullException"><paramref name="device"/> is <c>null</c> -or-
  251. /// <paramref name="schemes"/> is <c>null</c>.</exception>
  252. public static InputControlScheme? FindControlSchemeForDevice<TSchemes>(InputDevice device, TSchemes schemes)
  253. where TSchemes : IEnumerable<InputControlScheme>
  254. {
  255. if (schemes == null)
  256. throw new ArgumentNullException(nameof(schemes));
  257. if (device == null)
  258. throw new ArgumentNullException(nameof(device));
  259. return FindControlSchemeForDevices(new OneOrMore<InputDevice, ReadOnlyArray<InputDevice>>(device), schemes);
  260. }
  261. /// <summary>
  262. /// Whether the control scheme has a requirement in <see cref="deviceRequirements"/> that
  263. /// targets the given device.
  264. /// </summary>
  265. /// <param name="device">An input device.</param>
  266. /// <returns>True if the control scheme has a device requirement matching the device.</returns>
  267. /// <exception cref="ArgumentNullException"><paramref name="device"/> is <c>null</c>.</exception>
  268. /// <remarks>
  269. /// Note that both optional (see <see cref="DeviceRequirement.isOptional"/>) and non-optional
  270. /// device requirements are taken into account.
  271. ///
  272. /// </remarks>
  273. public bool SupportsDevice(InputDevice device)
  274. {
  275. if (device == null)
  276. throw new ArgumentNullException(nameof(device));
  277. ////REVIEW: does this need to take AND and OR into account?
  278. for (var i = 0; i < m_DeviceRequirements.Length; ++i)
  279. {
  280. var control = InputControlPath.TryFindControl(device, m_DeviceRequirements[i].controlPath);
  281. if (control != null)
  282. return true;
  283. }
  284. return false;
  285. }
  286. ////REVIEW: have mode where instead of matching only the first device that matches a requirement, we match as many
  287. //// as we can get? (could be useful for single-player)
  288. /// <summary>
  289. /// Based on a list of devices, make a selection that matches the <see cref="deviceRequirements">requirements</see>
  290. /// imposed by the control scheme.
  291. /// </summary>
  292. /// <param name="devices">A list of devices to choose from.</param>
  293. /// <param name="favorDevice">If not null, the device will be favored over other devices in <paramref name="devices"/>.
  294. /// Note that the device must be present in the list also.</param>
  295. /// <returns>A <see cref="MatchResult"/> structure containing the result of the pick. Note that this structure
  296. /// must be manually <see cref="MatchResult.Dispose">disposed</see> or unmanaged memory will be leaked.</returns>
  297. /// <remarks>
  298. /// Does not allocate managed memory.
  299. /// </remarks>
  300. public MatchResult PickDevicesFrom<TDevices>(TDevices devices, InputDevice favorDevice = null)
  301. where TDevices : IReadOnlyList<InputDevice>
  302. {
  303. // Empty device requirements match anything while not really picking anything.
  304. if (m_DeviceRequirements == null || m_DeviceRequirements.Length == 0)
  305. {
  306. return new MatchResult
  307. {
  308. m_Result = MatchResult.Result.AllSatisfied,
  309. // Prevent zero score on successful match but make less than one which would
  310. // result from having a single requirement.
  311. m_Score = 0.5f,
  312. };
  313. }
  314. // Go through each requirement and match it.
  315. // NOTE: Even if `devices` is empty, we don't know yet whether we have a NoMatch.
  316. // All our devices may be optional.
  317. var haveAllRequired = true;
  318. var haveAllOptional = true;
  319. var requirementCount = m_DeviceRequirements.Length;
  320. var score = 0f;
  321. var controls = new InputControlList<InputControl>(Allocator.Persistent, requirementCount);
  322. try
  323. {
  324. var orChainIsSatisfied = false;
  325. var orChainHasRequiredDevices = false;
  326. for (var i = 0; i < requirementCount; ++i)
  327. {
  328. var isOR = m_DeviceRequirements[i].isOR;
  329. var isOptional = m_DeviceRequirements[i].isOptional;
  330. // If this is an OR requirement and we already have a match in this OR chain,
  331. // skip this requirement.
  332. if (isOR && orChainIsSatisfied)
  333. {
  334. // Skill need to add an entry for this requirement.
  335. controls.Add(null);
  336. continue;
  337. }
  338. // Null and empty paths shouldn't make it into the list but make double
  339. // sure here. Simply ignore entries that don't have a path.
  340. var path = m_DeviceRequirements[i].controlPath;
  341. if (string.IsNullOrEmpty(path))
  342. {
  343. score += 1;
  344. controls.Add(null);
  345. continue;
  346. }
  347. // Find the first matching control among the devices we have.
  348. InputControl match = null;
  349. for (var n = 0; n < devices.Count; ++n)
  350. {
  351. var device = devices[n];
  352. // If we should favor a device, we swap it in at index #0 regardless
  353. // of where in the list the device occurs (it MUST, however, occur in the list).
  354. if (favorDevice != null)
  355. {
  356. if (n == 0)
  357. device = favorDevice;
  358. else if (device == favorDevice)
  359. device = devices[0];
  360. }
  361. // See if we have a match.
  362. var matchedControl = InputControlPath.TryFindControl(device, path);
  363. if (matchedControl == null)
  364. continue; // No.
  365. // We have a match but if we've already matched the same control through another requirement,
  366. // we can't use the match.
  367. if (controls.Contains(matchedControl))
  368. continue;
  369. match = matchedControl;
  370. // Compute score for match.
  371. var deviceLayoutOfControlPath = new InternedString(InputControlPath.TryGetDeviceLayout(path));
  372. if (deviceLayoutOfControlPath.IsEmpty())
  373. {
  374. // Generic match adds 1 to score.
  375. score += 1;
  376. }
  377. else
  378. {
  379. var deviceLayoutOfControl = matchedControl.device.m_Layout;
  380. if (InputControlLayout.s_Layouts.ComputeDistanceInInheritanceHierarchy(deviceLayoutOfControlPath,
  381. deviceLayoutOfControl, out var distance))
  382. {
  383. score += 1 + 1f / (Math.Abs(distance) + 1);
  384. }
  385. else
  386. {
  387. // Shouldn't really get here as for the control to be a match for the path, the device layouts
  388. // would be expected to be related to each other. But just add 1 for a generic match and go on.
  389. score += 1;
  390. }
  391. }
  392. break;
  393. }
  394. // Check requirements in AND and OR chains. We look ahead here to find out whether
  395. // the next requirement is starting an OR chain. As the OR combines with the previous
  396. // requirement in the list, this affects our current requirement.
  397. var nextIsOR = i + 1 < requirementCount && m_DeviceRequirements[i + 1].isOR;
  398. if (nextIsOR)
  399. {
  400. // Shouldn't get here if the chain is already satisfied. Should be handled
  401. // at beginning of loop and we shouldn't even be looking at finding controls
  402. // in that case.
  403. Debug.Assert(!orChainIsSatisfied);
  404. // It's an OR with the next requirement. Depends on the outcome of other matches whether
  405. // we're good or not.
  406. if (match != null)
  407. {
  408. // First match in this chain.
  409. orChainIsSatisfied = true;
  410. }
  411. else
  412. {
  413. // Chain not satisfied yet.
  414. if (!isOptional)
  415. orChainHasRequiredDevices = true;
  416. }
  417. }
  418. else if (isOR && i == requirementCount - 1)
  419. {
  420. // It's an OR at the very end of the requirements list. Terminate
  421. // the OR chain.
  422. if (match == null)
  423. {
  424. if (orChainHasRequiredDevices)
  425. haveAllRequired = false;
  426. else
  427. haveAllOptional = false;
  428. }
  429. }
  430. else
  431. {
  432. // It's an AND.
  433. if (match == null)
  434. {
  435. if (isOptional)
  436. haveAllOptional = false;
  437. else
  438. haveAllRequired = false;
  439. }
  440. // Terminate ongoing OR chain.
  441. if (i > 0 && m_DeviceRequirements[i - 1].isOR)
  442. {
  443. if (!orChainIsSatisfied)
  444. {
  445. if (orChainHasRequiredDevices)
  446. haveAllRequired = false;
  447. else
  448. haveAllOptional = false;
  449. }
  450. orChainIsSatisfied = false;
  451. }
  452. }
  453. // Add match to list. Maybe null.
  454. controls.Add(match);
  455. }
  456. // We should have matched each of our requirements.
  457. Debug.Assert(controls.Count == requirementCount);
  458. }
  459. catch (Exception)
  460. {
  461. controls.Dispose();
  462. throw;
  463. }
  464. return new MatchResult
  465. {
  466. m_Result = !haveAllRequired
  467. ? MatchResult.Result.MissingRequired
  468. : !haveAllOptional
  469. ? MatchResult.Result.MissingOptional
  470. : MatchResult.Result.AllSatisfied,
  471. m_Controls = controls,
  472. m_Requirements = m_DeviceRequirements,
  473. m_Score = score,
  474. };
  475. }
  476. public bool Equals(InputControlScheme other)
  477. {
  478. if (!(string.Equals(m_Name, other.m_Name, StringComparison.InvariantCultureIgnoreCase) &&
  479. string.Equals(m_BindingGroup, other.m_BindingGroup, StringComparison.InvariantCultureIgnoreCase)))
  480. return false;
  481. // Compare device requirements.
  482. if (m_DeviceRequirements == null || m_DeviceRequirements.Length == 0)
  483. return other.m_DeviceRequirements == null || other.m_DeviceRequirements.Length == 0;
  484. if (other.m_DeviceRequirements == null || m_DeviceRequirements.Length != other.m_DeviceRequirements.Length)
  485. return false;
  486. var deviceCount = m_DeviceRequirements.Length;
  487. for (var i = 0; i < deviceCount; ++i)
  488. {
  489. var device = m_DeviceRequirements[i];
  490. var haveMatch = false;
  491. for (var n = 0; n < deviceCount; ++n)
  492. {
  493. if (other.m_DeviceRequirements[n] == device)
  494. {
  495. haveMatch = true;
  496. break;
  497. }
  498. }
  499. if (!haveMatch)
  500. return false;
  501. }
  502. return true;
  503. }
  504. public override bool Equals(object obj)
  505. {
  506. if (ReferenceEquals(null, obj))
  507. return false;
  508. return obj is InputControlScheme && Equals((InputControlScheme)obj);
  509. }
  510. public override int GetHashCode()
  511. {
  512. unchecked
  513. {
  514. var hashCode = (m_Name != null ? m_Name.GetHashCode() : 0);
  515. hashCode = (hashCode * 397) ^ (m_BindingGroup != null ? m_BindingGroup.GetHashCode() : 0);
  516. hashCode = (hashCode * 397) ^ (m_DeviceRequirements != null ? m_DeviceRequirements.GetHashCode() : 0);
  517. return hashCode;
  518. }
  519. }
  520. public override string ToString()
  521. {
  522. if (string.IsNullOrEmpty(m_Name))
  523. return base.ToString();
  524. if (m_DeviceRequirements == null)
  525. return m_Name;
  526. var builder = new StringBuilder();
  527. builder.Append(m_Name);
  528. builder.Append('(');
  529. var isFirst = true;
  530. foreach (var device in m_DeviceRequirements)
  531. {
  532. if (!isFirst)
  533. builder.Append(',');
  534. builder.Append(device.controlPath);
  535. isFirst = false;
  536. }
  537. builder.Append(')');
  538. return builder.ToString();
  539. }
  540. public static bool operator==(InputControlScheme left, InputControlScheme right)
  541. {
  542. return left.Equals(right);
  543. }
  544. public static bool operator!=(InputControlScheme left, InputControlScheme right)
  545. {
  546. return !left.Equals(right);
  547. }
  548. [SerializeField] internal string m_Name;
  549. [SerializeField] internal string m_BindingGroup;
  550. [SerializeField] internal DeviceRequirement[] m_DeviceRequirements;
  551. /// <summary>
  552. /// The result of matching a list of <see cref="InputDevice">devices</see> against a list of
  553. /// <see cref="DeviceRequirement">requirements</see> in an <see cref="InputControlScheme"/>.
  554. /// </summary>
  555. /// <remarks>
  556. /// This struct uses <see cref="InputControlList{TControl}"/> which allocates unmanaged memory
  557. /// and thus must be disposed in order to not leak unmanaged heap memory.
  558. /// </remarks>
  559. /// <seealso cref="InputControlScheme.PickDevicesFrom{TDevices}"/>
  560. public struct MatchResult : IEnumerable<MatchResult.Match>, IDisposable
  561. {
  562. /// <summary>
  563. /// Overall, relative measure for how well the control scheme matches.
  564. /// </summary>
  565. /// <value>Scoring value for the control scheme match.</value>
  566. /// <remarks>
  567. /// Two control schemes may, for example, both support gamepads but one may be tailored to a specific
  568. /// gamepad whereas the other one is a generic gamepad control scheme. To differentiate the two, we need
  569. /// to know not only that a control schemes but how well it matches relative to other schemes. This is
  570. /// what the score value is used for.
  571. ///
  572. /// Scores are computed primarily based on layouts referenced from device requirements. To start with, each
  573. /// matching device requirement (whether optional or mandatory) will add 1 to the score. This the base
  574. /// score of a match. Then, for each requirement a delta is computed from the device layout referenced by
  575. /// the requirement to the device layout used by the matching control. For example, if the requirement is
  576. /// <c>"&lt;Gamepad&gt;</c> and the matching control uses the <see cref="DualShock.DualShock4GamepadHID"/>
  577. /// layout, the delta is 2 as the latter layout is derived from <see cref="Gamepad"/> via the intermediate
  578. /// <see cref="DualShock.DualShockGamepad"/> layout, i.e. two steps in the inheritance hierarchy. The
  579. /// <em>inverse</em> of the delta plus one, i.e. <c>1/(delta+1)</c> is then added to the score. This means
  580. /// that an exact match will add an additional 1 to the score and less exact matches will add progressively
  581. /// smaller values to the score (proportional to the distance of the actual layout to the one used in the
  582. /// requirement).
  583. ///
  584. /// What this leads to is that, for example, a control scheme with a <c>"&lt;Gamepad&gt;"</c> requirement
  585. /// will match a <see cref="DualShock.DualShock4GamepadHID"/> with a <em>lower</em> score than a control
  586. /// scheme with a <c>"&lt;DualShockGamepad&gt;"</c> requirement as the <see cref="Gamepad"/> layout is
  587. /// further removed (i.e. smaller inverse delta) from <see cref="DualShock.DualShock4GamepadHID"/> than
  588. /// <see cref="DualShock.DualShockGamepad"/>.
  589. /// </remarks>
  590. public float score => m_Score;
  591. /// <summary>
  592. /// Whether the device requirements got successfully matched.
  593. /// </summary>
  594. /// <value>True if the scheme's device requirements were satisfied.</value>
  595. public bool isSuccessfulMatch => m_Result != Result.MissingRequired;
  596. /// <summary>
  597. /// Whether there are missing required devices.
  598. /// </summary>
  599. /// <value>True if there are missing, non-optional devices.</value>
  600. /// <seealso cref="DeviceRequirement.isOptional"/>
  601. public bool hasMissingRequiredDevices => m_Result == Result.MissingRequired;
  602. /// <summary>
  603. /// Whether there are missing optional devices. This does not prevent
  604. /// a successful match.
  605. /// </summary>
  606. /// <value>True if there are missing optional devices.</value>
  607. /// <seealso cref="DeviceRequirement.isOptional"/>
  608. public bool hasMissingOptionalDevices => m_Result == Result.MissingOptional;
  609. /// <summary>
  610. /// The devices that got picked from the available devices.
  611. /// </summary>
  612. public InputControlList<InputDevice> devices
  613. {
  614. get
  615. {
  616. // Lazily construct the device list. If we have missing required
  617. // devices, though, always return an empty list. The user can still see
  618. // the individual matches on each of the requirement entries but we
  619. // consider the device picking itself failed.
  620. if (m_Devices.Count == 0 && !hasMissingRequiredDevices)
  621. {
  622. var controlCount = m_Controls.Count;
  623. if (controlCount != 0)
  624. {
  625. m_Devices.Capacity = controlCount;
  626. for (var i = 0; i < controlCount; ++i)
  627. {
  628. var control = m_Controls[i];
  629. if (control == null)
  630. continue;
  631. var device = control.device;
  632. if (m_Devices.Contains(device))
  633. continue; // Duplicate match of same device.
  634. m_Devices.Add(device);
  635. }
  636. }
  637. }
  638. return m_Devices;
  639. }
  640. }
  641. public Match this[int index]
  642. {
  643. get
  644. {
  645. if (index < 0 || m_Requirements == null || index >= m_Requirements.Length)
  646. throw new ArgumentOutOfRangeException("index");
  647. return new Match
  648. {
  649. m_RequirementIndex = index,
  650. m_Requirements = m_Requirements,
  651. m_Controls = m_Controls,
  652. };
  653. }
  654. }
  655. /// <summary>
  656. /// Enumerate the match for each individual <see cref="DeviceRequirement"/> in the control scheme.
  657. /// </summary>
  658. /// <returns>An enumerate going over each individual match.</returns>
  659. public IEnumerator<Match> GetEnumerator()
  660. {
  661. return new Enumerator
  662. {
  663. m_Index = -1,
  664. m_Requirements = m_Requirements,
  665. m_Controls = m_Controls,
  666. };
  667. }
  668. /// <summary>
  669. /// Enumerate the match for each individual <see cref="DeviceRequirement"/> in the control scheme.
  670. /// </summary>
  671. /// <returns>An enumerate going over each individual match.</returns>
  672. IEnumerator IEnumerable.GetEnumerator()
  673. {
  674. return GetEnumerator();
  675. }
  676. /// <summary>
  677. /// Discard the list of devices.
  678. /// </summary>
  679. public void Dispose()
  680. {
  681. m_Controls.Dispose();
  682. m_Devices.Dispose();
  683. }
  684. internal Result m_Result;
  685. internal float m_Score;
  686. internal InputControlList<InputDevice> m_Devices;
  687. internal InputControlList<InputControl> m_Controls;
  688. internal DeviceRequirement[] m_Requirements;
  689. internal enum Result
  690. {
  691. AllSatisfied,
  692. MissingRequired,
  693. MissingOptional,
  694. }
  695. ////REVIEW: would be great to not have to repeatedly copy InputControlLists around
  696. /// <summary>
  697. /// A single matched <see cref="DeviceRequirement"/>.
  698. /// </summary>
  699. /// <remarks>
  700. /// Links the control that was matched with the respective device requirement.
  701. /// </remarks>
  702. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1724:TypeNamesShouldNotMatchNamespaces", Justification = "Conflicts with UnityEngine.Networking.Match, which is deprecated and will go away.")]
  703. public struct Match
  704. {
  705. /// <summary>
  706. /// The control that was match from the requirement's <see cref="DeviceRequirement.controlPath"/>
  707. /// </summary>
  708. /// <remarks>
  709. /// This is the same as <see cref="device"/> if the <see cref="DeviceRequirement.controlPath">control
  710. /// path</see> matches the device directly rather than matching a control on the device.
  711. ///
  712. /// Note that while a control path can match arbitrary many controls, only the first matched control
  713. /// will be returned here. To get all controls that were matched by a specific requirement, a
  714. /// manual query must be performed using <see cref="InputControlPath"/>.
  715. ///
  716. /// If the match failed, this will be null.
  717. /// </remarks>
  718. public InputControl control => m_Controls[m_RequirementIndex];
  719. /// <summary>
  720. /// The device that got matched.
  721. /// </summary>
  722. /// <remarks>
  723. /// If a specific control on the device was matched, this will be <see cref="InputControl.device"/> or
  724. /// <see cref="control"/>. If a device was matched directly, this will be the same as <see cref="control"/>.
  725. /// </remarks>
  726. public InputDevice device
  727. {
  728. get
  729. {
  730. var control = this.control;
  731. return control?.device;
  732. }
  733. }
  734. /// <summary>
  735. /// Index of the requirement in <see cref="InputControlScheme.deviceRequirements"/>.
  736. /// </summary>
  737. public int requirementIndex => m_RequirementIndex;
  738. /// <summary>
  739. /// The device requirement that got matched.
  740. /// </summary>
  741. public DeviceRequirement requirement => m_Requirements[m_RequirementIndex];
  742. public bool isOptional => requirement.isOptional;
  743. internal int m_RequirementIndex;
  744. internal DeviceRequirement[] m_Requirements;
  745. internal InputControlList<InputControl> m_Controls;
  746. }
  747. private struct Enumerator : IEnumerator<Match>
  748. {
  749. public bool MoveNext()
  750. {
  751. ++m_Index;
  752. return m_Requirements != null && m_Index < m_Requirements.Length;
  753. }
  754. public void Reset()
  755. {
  756. m_Index = -1;
  757. }
  758. public Match Current
  759. {
  760. get
  761. {
  762. if (m_Requirements == null || m_Index < 0 || m_Index >= m_Requirements.Length)
  763. throw new InvalidOperationException("Enumerator is not valid");
  764. return new Match
  765. {
  766. m_RequirementIndex = m_Index,
  767. m_Requirements = m_Requirements,
  768. m_Controls = m_Controls,
  769. };
  770. }
  771. }
  772. object IEnumerator.Current => Current;
  773. public void Dispose()
  774. {
  775. }
  776. internal int m_Index;
  777. internal DeviceRequirement[] m_Requirements;
  778. internal InputControlList<InputControl> m_Controls;
  779. }
  780. }
  781. /// <summary>
  782. ///
  783. /// </summary>
  784. /// <remarks>
  785. /// Note that device requirements may require specific controls to be present rather than only requiring
  786. /// the presence of a certain type of device. For example, a requirement with a <see cref="controlPath"/>
  787. /// of "*/{PrimaryAction}" will be satisfied by any device that has a control marked as <see cref="CommonUsages.PrimaryAction"/>.
  788. ///
  789. /// Requirements are ordered in a list and can combine with their previous requirement in either <see cref="isAND">
  790. /// AND</see> or in <see cref="isOR">OR</see> fashion. The default is for requirements to combine with AND.
  791. ///
  792. /// Note that it is not possible to express nested constraints like <c>(a AND b) OR (c AND d)</c>. Also note that
  793. /// operator precedence is the opposite of C#, meaning that OR has *higher* precedence than AND. This means
  794. /// that <c>a OR b AND c OR d</c> reads as <c>(a OR b) AND (c OR d)</c> (in C# it would read as <c>a OR
  795. /// (b AND c) OR d</c>.
  796. ///
  797. /// More complex expressions can often be expressed differently. For example, <c>(a AND b) OR (c AND d)</c>
  798. /// can be expressed as <c>a OR c AND b OR d</c>.
  799. /// </remarks>
  800. [Serializable]
  801. public struct DeviceRequirement : IEquatable<DeviceRequirement>
  802. {
  803. /// <summary>
  804. /// <see cref="InputControlPath">Control path</see> that is matched against a device to determine
  805. /// whether it qualifies for the control scheme.
  806. /// </summary>
  807. /// <remarks>
  808. /// </remarks>
  809. /// <example>
  810. /// <code>
  811. /// // A left-hand XR controller.
  812. /// "&lt;XRController&gt;{LeftHand}"
  813. ///
  814. /// // A gamepad.
  815. /// "&lt;Gamepad&gt;"
  816. /// </code>
  817. /// </example>
  818. public string controlPath
  819. {
  820. get => m_ControlPath;
  821. set => m_ControlPath = value;
  822. }
  823. /// <summary>
  824. /// If true, a device with the given <see cref="controlPath">device path</see> is employed by the
  825. /// control scheme if one is available. If none is available, the control scheme is still
  826. /// functional.
  827. /// </summary>
  828. public bool isOptional
  829. {
  830. get => (m_Flags & Flags.Optional) != 0;
  831. set
  832. {
  833. if (value)
  834. m_Flags |= Flags.Optional;
  835. else
  836. m_Flags &= ~Flags.Optional;
  837. }
  838. }
  839. /// <summary>
  840. /// Whether the requirement combines with the previous requirement (if any) as a boolean AND.
  841. /// </summary>
  842. /// <remarks>
  843. /// This is the default. For example, to require both a left hand and a right XR controller,
  844. /// the first requirement would be for "&lt;XRController&gt;{LeftHand}" and the second
  845. /// requirement would be for "&gt;XRController&gt;{RightHand}" and would return true for this
  846. /// property.
  847. /// </remarks>
  848. /// <seealso cref="isOR"/>
  849. public bool isAND
  850. {
  851. get => !isOR;
  852. set => isOR = !value;
  853. }
  854. /// <summary>
  855. /// Whether the requirement combines with the previous requirement (if any) as a boolean OR.
  856. /// </summary>
  857. /// <remarks>
  858. /// This allows designing control schemes that flexibly work with combinations of devices such that
  859. /// if one specific device isn't present, another device can substitute for it.
  860. ///
  861. /// For example, to design a mouse+keyboard control scheme that can alternatively work with a pen
  862. /// instead of a mouse, the first requirement could be for "&lt;Keyboard&gt;", the second one
  863. /// could be for "&lt;Mouse&gt;" and the third one could be for "&lt;Pen&gt;" and return true
  864. /// for this property. Both the mouse and the pen would be marked as required (i.e. not <see cref="isOptional"/>)
  865. /// but the device requirements are satisfied even if either device is present.
  866. ///
  867. /// Note that if both a pen and a mouse are present at the same time, still only one device is
  868. /// picked. In this case, the mouse "wins" as it comes first in the list of requirements.
  869. /// </remarks>
  870. public bool isOR
  871. {
  872. get => (m_Flags & Flags.Or) != 0;
  873. set
  874. {
  875. if (value)
  876. m_Flags |= Flags.Or;
  877. else
  878. m_Flags &= ~Flags.Or;
  879. }
  880. }
  881. [SerializeField] internal string m_ControlPath;
  882. [SerializeField] internal Flags m_Flags;
  883. [Flags]
  884. internal enum Flags
  885. {
  886. None = 0,
  887. Optional = 1 << 0,
  888. Or = 1 << 1,
  889. }
  890. public override string ToString()
  891. {
  892. if (!string.IsNullOrEmpty(controlPath))
  893. {
  894. if (isOptional)
  895. return controlPath + " (Optional)";
  896. return controlPath + " (Required)";
  897. }
  898. return base.ToString();
  899. }
  900. public bool Equals(DeviceRequirement other)
  901. {
  902. return string.Equals(m_ControlPath, other.m_ControlPath) && m_Flags == other.m_Flags &&
  903. string.Equals(controlPath, other.controlPath) && isOptional == other.isOptional;
  904. }
  905. public override bool Equals(object obj)
  906. {
  907. if (ReferenceEquals(null, obj))
  908. return false;
  909. return obj is DeviceRequirement && Equals((DeviceRequirement)obj);
  910. }
  911. public override int GetHashCode()
  912. {
  913. unchecked
  914. {
  915. var hashCode = (m_ControlPath != null ? m_ControlPath.GetHashCode() : 0);
  916. hashCode = (hashCode * 397) ^ m_Flags.GetHashCode();
  917. hashCode = (hashCode * 397) ^ (controlPath != null ? controlPath.GetHashCode() : 0);
  918. hashCode = (hashCode * 397) ^ isOptional.GetHashCode();
  919. return hashCode;
  920. }
  921. }
  922. public static bool operator==(DeviceRequirement left, DeviceRequirement right)
  923. {
  924. return left.Equals(right);
  925. }
  926. public static bool operator!=(DeviceRequirement left, DeviceRequirement right)
  927. {
  928. return !left.Equals(right);
  929. }
  930. }
  931. /// <summary>
  932. /// JSON-serialized form of a control scheme.
  933. /// </summary>
  934. [Serializable]
  935. internal struct SchemeJson
  936. {
  937. public string name;
  938. public string bindingGroup;
  939. public DeviceJson[] devices;
  940. [Serializable]
  941. public struct DeviceJson
  942. {
  943. public string devicePath;
  944. public bool isOptional;
  945. public bool isOR;
  946. public DeviceRequirement ToDeviceEntry()
  947. {
  948. return new DeviceRequirement
  949. {
  950. controlPath = devicePath,
  951. isOptional = isOptional,
  952. isOR = isOR,
  953. };
  954. }
  955. public static DeviceJson From(DeviceRequirement requirement)
  956. {
  957. return new DeviceJson
  958. {
  959. devicePath = requirement.controlPath,
  960. isOptional = requirement.isOptional,
  961. isOR = requirement.isOR,
  962. };
  963. }
  964. }
  965. public InputControlScheme ToScheme()
  966. {
  967. DeviceRequirement[] deviceRequirements = null;
  968. if (devices != null && devices.Length > 0)
  969. {
  970. var count = devices.Length;
  971. deviceRequirements = new DeviceRequirement[count];
  972. for (var i = 0; i < count; ++i)
  973. deviceRequirements[i] = devices[i].ToDeviceEntry();
  974. }
  975. return new InputControlScheme
  976. {
  977. m_Name = string.IsNullOrEmpty(name) ? null : name,
  978. m_BindingGroup = string.IsNullOrEmpty(bindingGroup) ? null : bindingGroup,
  979. m_DeviceRequirements = deviceRequirements,
  980. };
  981. }
  982. public static SchemeJson ToJson(InputControlScheme scheme)
  983. {
  984. DeviceJson[] devices = null;
  985. if (scheme.m_DeviceRequirements != null && scheme.m_DeviceRequirements.Length > 0)
  986. {
  987. var count = scheme.m_DeviceRequirements.Length;
  988. devices = new DeviceJson[count];
  989. for (var i = 0; i < count; ++i)
  990. devices[i] = DeviceJson.From(scheme.m_DeviceRequirements[i]);
  991. }
  992. return new SchemeJson
  993. {
  994. name = scheme.m_Name,
  995. bindingGroup = scheme.m_BindingGroup,
  996. devices = devices,
  997. };
  998. }
  999. public static SchemeJson[] ToJson(InputControlScheme[] schemes)
  1000. {
  1001. if (schemes == null || schemes.Length == 0)
  1002. return null;
  1003. var count = schemes.Length;
  1004. var result = new SchemeJson[count];
  1005. for (var i = 0; i < count; ++i)
  1006. result[i] = ToJson(schemes[i]);
  1007. return result;
  1008. }
  1009. public static InputControlScheme[] ToSchemes(SchemeJson[] schemes)
  1010. {
  1011. if (schemes == null || schemes.Length == 0)
  1012. return null;
  1013. var count = schemes.Length;
  1014. var result = new InputControlScheme[count];
  1015. for (var i = 0; i < count; ++i)
  1016. result[i] = schemes[i].ToScheme();
  1017. return result;
  1018. }
  1019. }
  1020. }
  1021. }