Ei kuvausta
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.

InputControlList.cs 23KB

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