InputControlList.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using System.Linq;
  6. using System.Text;
  7. using Unity.Collections;
  8. using Unity.Collections.LowLevel.Unsafe;
  9. using UnityEngine.InputSystem.Utilities;
  10. ////TODO: add a device setup version to InputManager and add version check here to ensure we're not going out of sync
  11. ////REVIEW: can we have a read-only version of this
  12. ////REVIEW: this would *really* profit from having a global ordering of InputControls that can be indexed
  13. ////REVIEW: move this to .LowLevel? this one is pretty peculiar to use and doesn't really work like what you'd expect given C#'s List<>
  14. namespace UnityEngine.InputSystem
  15. {
  16. /// <summary>
  17. /// Keep a list of <see cref="InputControl"/>s without allocating managed memory.
  18. /// </summary>
  19. /// <remarks>
  20. /// This struct is mainly used by methods such as <see cref="InputSystem.FindControls(string)"/>
  21. /// or <see cref="InputControlPath.TryFindControls{TControl}"/> to store an arbitrary length
  22. /// list of resulting matches without having to allocate GC heap memory.
  23. ///
  24. /// Requires the control setup in the system to not change while the list is being used. If devices are
  25. /// removed from the system, the list will no longer be valid. Also, only works with controls of devices that
  26. /// have been added to the system (<see cref="InputDevice.added"/>). The reason for these constraints is
  27. /// that internally, the list only stores integer indices that are translates to <see cref="InputControl"/>
  28. /// references on the fly. If the device setup in the system changes, the indices may become invalid.
  29. ///
  30. /// This struct allocates unmanaged memory and thus must be disposed or it will leak memory. By default
  31. /// allocates <c>Allocator.Persistent</c> memory. You can direct it to use another allocator by
  32. /// passing an <see cref="Allocator"/> value to one of the constructors.
  33. ///
  34. /// <example>
  35. /// <code>
  36. /// // Find all controls with the "Submit" usage in the system.
  37. /// // By wrapping it in a `using` block, the list of controls will automatically be disposed at the end.
  38. /// using (var controls = InputSystem.FindControls("*/{Submit}"))
  39. /// /* ... */;
  40. /// </code>
  41. /// </example>
  42. /// </remarks>
  43. /// <typeparam name="TControl">Type of <see cref="InputControl"/> to store in the list.</typeparam>
  44. [DebuggerDisplay("Count = {Count}")]
  45. #if UNITY_EDITOR || DEVELOPMENT_BUILD
  46. [DebuggerTypeProxy(typeof(InputControlListDebugView<>))]
  47. #endif
  48. public unsafe struct InputControlList<TControl> : IList<TControl>, IReadOnlyList<TControl>, IDisposable
  49. where TControl : InputControl
  50. {
  51. /// <summary>
  52. /// Current number of controls in the list.
  53. /// </summary>
  54. /// <value>Number of controls currently in the list.</value>
  55. public int Count => m_Count;
  56. /// <summary>
  57. /// Total number of controls that can currently be stored in the list.
  58. /// </summary>
  59. /// <value>Total size of array as currently allocated.</value>
  60. /// <remarks>
  61. /// This can be set ahead of time to avoid repeated allocations.
  62. ///
  63. /// <example>
  64. /// <code>
  65. /// // Add all keys from the keyboard to a list.
  66. /// var keys = Keyboard.current.allKeys;
  67. /// var list = new InputControlList&lt;KeyControl&gt;(keys.Count);
  68. /// list.AddRange(keys);
  69. /// </code>
  70. /// </example>
  71. /// </remarks>
  72. public int Capacity
  73. {
  74. get
  75. {
  76. if (!m_Indices.IsCreated)
  77. return 0;
  78. return m_Indices.Length;
  79. }
  80. set
  81. {
  82. if (value < 0)
  83. throw new ArgumentException("Capacity cannot be negative", nameof(value));
  84. if (value == 0)
  85. {
  86. if (m_Count != 0)
  87. m_Indices.Dispose();
  88. m_Count = 0;
  89. return;
  90. }
  91. var newSize = value;
  92. var allocator = m_Allocator != Allocator.Invalid ? m_Allocator : Allocator.Persistent;
  93. ArrayHelpers.Resize(ref m_Indices, newSize, allocator);
  94. }
  95. }
  96. /// <summary>
  97. /// This is always false.
  98. /// </summary>
  99. /// <value>Always false.</value>
  100. public bool IsReadOnly => false;
  101. /// <summary>
  102. /// Return the control at the given index.
  103. /// </summary>
  104. /// <param name="index">Index of control.</param>
  105. /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is less than 0 or greater than or equal to <see cref="Count"/>
  106. /// </exception>
  107. /// <remarks>
  108. /// Internally, the list only stores indices. Resolution to <see cref="InputControl">controls</see> happens
  109. /// dynamically by looking them up globally.
  110. /// </remarks>
  111. public TControl this[int index]
  112. {
  113. get
  114. {
  115. if (index < 0 || index >= m_Count)
  116. throw new ArgumentOutOfRangeException(
  117. nameof(index), $"Index {index} is out of range in list with {m_Count} entries");
  118. return FromIndex(m_Indices[index]);
  119. }
  120. set
  121. {
  122. if (index < 0 || index >= m_Count)
  123. throw new ArgumentOutOfRangeException(
  124. nameof(index), $"Index {index} is out of range in list with {m_Count} entries");
  125. m_Indices[index] = ToIndex(value);
  126. }
  127. }
  128. /// <summary>
  129. /// Construct a list that allocates unmanaged memory from the given allocator.
  130. /// </summary>
  131. /// <param name="allocator">Allocator to use for requesting unmanaged memory.</param>
  132. /// <param name="initialCapacity">If greater than zero, will immediately allocate
  133. /// memory and set <see cref="Capacity"/> accordingly.</param>
  134. /// <example>
  135. /// <code>
  136. /// // Create a control list that allocates from the temporary memory allocator.
  137. /// using (var list = new InputControlList(Allocator.Temp))
  138. /// {
  139. /// // Add all gamepads to the list.
  140. /// InputSystem.FindControls("&lt;Gamepad&gt;", ref list);
  141. /// }
  142. /// </code>
  143. /// </example>
  144. public InputControlList(Allocator allocator, int initialCapacity = 0)
  145. {
  146. m_Allocator = allocator;
  147. m_Indices = new NativeArray<ulong>();
  148. m_Count = 0;
  149. if (initialCapacity != 0)
  150. Capacity = initialCapacity;
  151. }
  152. /// <summary>
  153. /// Construct a list and populate it with the given values.
  154. /// </summary>
  155. /// <param name="values">Sequence of values to populate the list with.</param>
  156. /// <param name="allocator">Allocator to use for requesting unmanaged memory.</param>
  157. /// <exception cref="ArgumentNullException"><paramref name="values"/> is <c>null</c>.</exception>
  158. public InputControlList(IEnumerable<TControl> values, Allocator allocator = Allocator.Persistent)
  159. : this(allocator)
  160. {
  161. if (values == null)
  162. throw new ArgumentNullException(nameof(values));
  163. foreach (var value in values)
  164. Add(value);
  165. }
  166. /// <summary>
  167. /// Construct a list and add the given values to it.
  168. /// </summary>
  169. /// <param name="values">Sequence of controls to add to the list.</param>
  170. /// <exception cref="ArgumentNullException"><paramref name="values"/> is null.</exception>
  171. public InputControlList(params TControl[] values)
  172. : this()
  173. {
  174. if (values == null)
  175. throw new ArgumentNullException(nameof(values));
  176. var count = values.Length;
  177. Capacity = Mathf.Max(count, 10);
  178. for (var i = 0; i < count; ++i)
  179. Add(values[i]);
  180. }
  181. /// <summary>
  182. /// Add a control to the list.
  183. /// </summary>
  184. /// <param name="item">Control to add. Allowed to be <c>null</c>.</param>
  185. /// <remarks>
  186. /// If necessary, <see cref="Capacity"/> will be increased.
  187. ///
  188. /// It is allowed to add nulls to the list. This can be useful, for example, when
  189. /// specific indices in the list correlate with specific matches and a given match
  190. /// needs to be marked as "matches nothing".
  191. /// </remarks>
  192. /// <seealso cref="Remove"/>
  193. public void Add(TControl item)
  194. {
  195. var index = ToIndex(item);
  196. var allocator = m_Allocator != Allocator.Invalid ? m_Allocator : Allocator.Persistent;
  197. ArrayHelpers.AppendWithCapacity(ref m_Indices, ref m_Count, index, allocator: allocator);
  198. }
  199. /// <summary>
  200. /// Add a slice of elements taken from the given list.
  201. /// </summary>
  202. /// <param name="list">List to take the slice of values from.</param>
  203. /// <param name="count">Number of elements to copy from <paramref name="list"/>.</param>
  204. /// <param name="destinationIndex">Starting index in the current control list to copy to.
  205. /// This can be beyond <see cref="Count"/> or even <see cref="Capacity"/>. Memory is allocated
  206. /// as needed.</param>
  207. /// <param name="sourceIndex">Source index in <paramref name="list"/> to start copying from.
  208. /// <paramref name="count"/> elements are copied starting at <paramref name="sourceIndex"/>.</param>
  209. /// <typeparam name="TList">Type of list. This is a type parameter to avoid boxing in case the
  210. /// given list is a struct (such as InputControlList itself).</typeparam>
  211. /// <exception cref="ArgumentOutOfRangeException">The range of <paramref name="count"/>
  212. /// and <paramref name="sourceIndex"/> is at least partially outside the range of values
  213. /// available in <paramref name="list"/>.</exception>
  214. public void AddSlice<TList>(TList list, int count = -1, int destinationIndex = -1, int sourceIndex = 0)
  215. where TList : IReadOnlyList<TControl>
  216. {
  217. if (count < 0)
  218. count = list.Count;
  219. if (destinationIndex < 0)
  220. destinationIndex = Count;
  221. if (count == 0)
  222. return;
  223. if (sourceIndex + count > list.Count)
  224. throw new ArgumentOutOfRangeException(nameof(count),
  225. $"Count of {count} elements starting at index {sourceIndex} exceeds length of list of {list.Count}");
  226. // Make space in the list.
  227. if (Capacity < m_Count + count)
  228. Capacity = Math.Max(m_Count + count, 10);
  229. if (destinationIndex < Count)
  230. NativeArray<ulong>.Copy(m_Indices, destinationIndex, m_Indices, destinationIndex + count,
  231. Count - destinationIndex);
  232. // Add elements.
  233. for (var i = 0; i < count; ++i)
  234. m_Indices[destinationIndex + i] = ToIndex(list[sourceIndex + i]);
  235. m_Count += count;
  236. }
  237. /// <summary>
  238. /// Add a sequence of controls to the list.
  239. /// </summary>
  240. /// <param name="list">Sequence of controls to add.</param>
  241. /// <param name="count">Number of controls from <paramref name="list"/> to add. If negative
  242. /// (default), all controls from <paramref name="list"/> will be added.</param>
  243. /// <param name="destinationIndex">Index in the control list to start inserting controls
  244. /// at. If negative (default), controls will be appended to the end of the control list.</param>
  245. /// <exception cref="ArgumentNullException"><paramref name="list"/> is <c>null</c>.</exception>
  246. /// <remarks>
  247. /// If <paramref name="count"/> is not supplied, <paramref name="list"/> will be iterated
  248. /// over twice.
  249. /// </remarks>
  250. public void AddRange(IEnumerable<TControl> list, int count = -1, int destinationIndex = -1)
  251. {
  252. if (list == null)
  253. throw new ArgumentNullException(nameof(list));
  254. if (count < 0)
  255. count = list.Count();
  256. if (destinationIndex < 0)
  257. destinationIndex = Count;
  258. if (count == 0)
  259. return;
  260. // Make space in the list.
  261. if (Capacity < m_Count + count)
  262. Capacity = Math.Max(m_Count + count, 10);
  263. if (destinationIndex < Count)
  264. NativeArray<ulong>.Copy(m_Indices, destinationIndex, m_Indices, destinationIndex + count,
  265. Count - destinationIndex);
  266. // Add elements.
  267. foreach (var element in list)
  268. {
  269. m_Indices[destinationIndex++] = ToIndex(element);
  270. ++m_Count;
  271. --count;
  272. if (count == 0)
  273. break;
  274. }
  275. }
  276. /// <summary>
  277. /// Remove a control from the list.
  278. /// </summary>
  279. /// <param name="item">Control to remove. Can be null.</param>
  280. /// <returns>True if the control was found in the list and removed, false otherwise.</returns>
  281. /// <seealso cref="Add"/>
  282. public bool Remove(TControl item)
  283. {
  284. if (m_Count == 0)
  285. return false;
  286. var index = ToIndex(item);
  287. for (var i = 0; i < m_Count; ++i)
  288. {
  289. if (m_Indices[i] == index)
  290. {
  291. ArrayHelpers.EraseAtWithCapacity(m_Indices, ref m_Count, i);
  292. return true;
  293. }
  294. }
  295. return false;
  296. }
  297. /// <summary>
  298. /// Remove the control at the given index.
  299. /// </summary>
  300. /// <param name="index">Index of control to remove.</param>
  301. /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is negative or equal
  302. /// or greater than <see cref="Count"/>.</exception>
  303. public void RemoveAt(int index)
  304. {
  305. if (index < 0 || index >= m_Count)
  306. throw new ArgumentOutOfRangeException(
  307. nameof(index), $"Index {index} is out of range in list with {m_Count} elements");
  308. ArrayHelpers.EraseAtWithCapacity(m_Indices, ref m_Count, index);
  309. }
  310. public void CopyTo(TControl[] array, int arrayIndex)
  311. {
  312. throw new NotImplementedException();
  313. }
  314. public int IndexOf(TControl item)
  315. {
  316. if (m_Count == 0)
  317. return -1;
  318. var index = ToIndex(item);
  319. var indices = (ulong*)m_Indices.GetUnsafeReadOnlyPtr();
  320. for (var i = 0; i < m_Count; ++i)
  321. if (indices[i] == index)
  322. return i;
  323. return -1;
  324. }
  325. public void Insert(int index, TControl item)
  326. {
  327. throw new NotImplementedException();
  328. }
  329. public void Clear()
  330. {
  331. m_Count = 0;
  332. }
  333. public bool Contains(TControl item)
  334. {
  335. return IndexOf(item) != -1;
  336. }
  337. public void SwapElements(int index1, int index2)
  338. {
  339. if (index1 < 0 || index1 >= m_Count)
  340. throw new ArgumentOutOfRangeException(nameof(index1));
  341. if (index2 < 0 || index2 >= m_Count)
  342. throw new ArgumentOutOfRangeException(nameof(index2));
  343. if (index1 != index2)
  344. m_Indices.SwapElements(index1, index2);
  345. }
  346. public void Sort<TCompare>(int startIndex, int count, TCompare comparer)
  347. where TCompare : IComparer<TControl>
  348. {
  349. if (startIndex < 0 || startIndex >= Count)
  350. throw new ArgumentOutOfRangeException(nameof(startIndex));
  351. if (startIndex + count >= Count)
  352. throw new ArgumentOutOfRangeException(nameof(count));
  353. // Simple insertion sort.
  354. for (var i = 1; i < count; ++i)
  355. for (var j = i; j > 0 && comparer.Compare(this[j - 1], this[j]) < 0; --j)
  356. SwapElements(j, j - 1);
  357. }
  358. /// <summary>
  359. /// Convert the contents of the list to an array.
  360. /// </summary>
  361. /// <param name="dispose">If true, the control list will be disposed of as part of the operation, i.e.
  362. /// <see cref="Dispose"/> will be called as a side-effect.</param>
  363. /// <returns>An array mirroring the contents of the list. Not null.</returns>
  364. public TControl[] ToArray(bool dispose = false)
  365. {
  366. // Somewhat pointless to allocate an empty array if we have no elements instead
  367. // of returning null, but other ToArray() implementations work that way so we do
  368. // the same to avoid surprises.
  369. var result = new TControl[m_Count];
  370. for (var i = 0; i < m_Count; ++i)
  371. result[i] = this[i];
  372. if (dispose)
  373. Dispose();
  374. return result;
  375. }
  376. internal void AppendTo(ref TControl[] array, ref int count)
  377. {
  378. for (var i = 0; i < m_Count; ++i)
  379. ArrayHelpers.AppendWithCapacity(ref array, ref count, this[i]);
  380. }
  381. public void Dispose()
  382. {
  383. if (m_Indices.IsCreated)
  384. m_Indices.Dispose();
  385. }
  386. public IEnumerator<TControl> GetEnumerator()
  387. {
  388. return new Enumerator(this);
  389. }
  390. IEnumerator IEnumerable.GetEnumerator()
  391. {
  392. return GetEnumerator();
  393. }
  394. public override string ToString()
  395. {
  396. if (Count == 0)
  397. return "()";
  398. var builder = new StringBuilder();
  399. builder.Append('(');
  400. for (var i = 0; i < Count; ++i)
  401. {
  402. if (i != 0)
  403. builder.Append(',');
  404. builder.Append(this[i]);
  405. }
  406. builder.Append(')');
  407. return builder.ToString();
  408. }
  409. private int m_Count;
  410. private NativeArray<ulong> m_Indices;
  411. private readonly Allocator m_Allocator;
  412. private const ulong kInvalidIndex = 0xffffffffffffffff;
  413. private static ulong ToIndex(TControl control)
  414. {
  415. if (control == null)
  416. return kInvalidIndex;
  417. var device = control.device;
  418. var deviceIndex = device.m_DeviceIndex;
  419. var controlIndex = !ReferenceEquals(device, control)
  420. ? ArrayHelpers.IndexOfReference(device.m_ChildrenForEachControl, control) + 1
  421. : 0;
  422. // There is a known documented bug with the new Rosyln
  423. // compiler where it warns on casts with following line that
  424. // was perfectly legal in previous CSC compiler.
  425. // Below is silly conversion to get rid of warning, or we can pragma
  426. // out the warning.
  427. //return ((ulong)deviceIndex << 32) | (ulong)controlIndex;
  428. var shiftedDeviceIndex = (ulong)deviceIndex << 32;
  429. var unsignedControlIndex = (ulong)controlIndex;
  430. return shiftedDeviceIndex | unsignedControlIndex;
  431. }
  432. private static TControl FromIndex(ulong index)
  433. {
  434. if (index == kInvalidIndex)
  435. return null;
  436. var deviceIndex = (int)(index >> 32);
  437. var controlIndex = (int)(index & 0xFFFFFFFF);
  438. var device = InputSystem.devices[deviceIndex];
  439. if (controlIndex == 0)
  440. return (TControl)(InputControl)device;
  441. return (TControl)device.m_ChildrenForEachControl[controlIndex - 1];
  442. }
  443. private struct Enumerator : IEnumerator<TControl>
  444. {
  445. private readonly ulong* m_Indices;
  446. private readonly int m_Count;
  447. private int m_Current;
  448. public Enumerator(InputControlList<TControl> list)
  449. {
  450. m_Count = list.m_Count;
  451. m_Current = -1;
  452. m_Indices = m_Count > 0 ? (ulong*)list.m_Indices.GetUnsafeReadOnlyPtr() : null;
  453. }
  454. public bool MoveNext()
  455. {
  456. if (m_Current >= m_Count)
  457. return false;
  458. ++m_Current;
  459. return (m_Current != m_Count);
  460. }
  461. public void Reset()
  462. {
  463. m_Current = -1;
  464. }
  465. public TControl Current
  466. {
  467. get
  468. {
  469. if (m_Indices == null)
  470. throw new InvalidOperationException("Enumerator is not valid");
  471. return FromIndex(m_Indices[m_Current]);
  472. }
  473. }
  474. object IEnumerator.Current => Current;
  475. public void Dispose()
  476. {
  477. }
  478. }
  479. }
  480. #if UNITY_EDITOR || DEVELOPMENT_BUILD
  481. internal struct InputControlListDebugView<TControl>
  482. where TControl : InputControl
  483. {
  484. private readonly TControl[] m_Controls;
  485. public InputControlListDebugView(InputControlList<TControl> list)
  486. {
  487. m_Controls = list.ToArray();
  488. }
  489. public TControl[] controls => m_Controls;
  490. }
  491. #endif
  492. }