InputTestFixture.cs 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using UnityEngine.InputSystem.Controls;
  5. using NUnit.Framework;
  6. using NUnit.Framework.Constraints;
  7. using Unity.Collections;
  8. using UnityEngine.InputSystem.LowLevel;
  9. using UnityEngine.InputSystem.Utilities;
  10. using UnityEngine.SceneManagement;
  11. using UnityEngine.TestTools.Utils;
  12. #if UNITY_EDITOR
  13. using UnityEngine.InputSystem.Editor;
  14. #endif
  15. ////TODO: must allow running UnityTests which means we have to be able to get per-frame updates yet not receive input from native
  16. ////TODO: when running tests in players, make sure that remoting is turned off
  17. ////REVIEW: always enable event diagnostics in InputTestFixture?
  18. namespace UnityEngine.InputSystem
  19. {
  20. /// <summary>
  21. /// A test fixture for writing tests that use the input system. Can be derived from
  22. /// or simply instantiated from another test fixture.
  23. /// </summary>
  24. /// <remarks>
  25. /// The fixture will put the input system into a known state where it has only the
  26. /// built-in set of basic layouts and no devices. The state of the system before
  27. /// starting a test is recorded and restored when the test finishes.
  28. ///
  29. /// <example>
  30. /// <code>
  31. /// public class MyInputTests : InputTestFixture
  32. /// {
  33. /// public override void Setup()
  34. /// {
  35. /// base.Setup();
  36. ///
  37. /// InputSystem.RegisterLayout&lt;MyDevice&gt;();
  38. /// }
  39. ///
  40. /// [Test]
  41. /// public void CanCreateMyDevice()
  42. /// {
  43. /// InputSystem.AddDevice&lt;MyDevice&gt;();
  44. /// Assert.That(InputSystem.devices, Has.Exactly(1).TypeOf&lt;MyDevice&gt;());
  45. /// }
  46. /// }
  47. /// </code>
  48. /// </example>
  49. ///
  50. /// The test fixture will also sever the tie of the input system to the Unity runtime.
  51. /// This means that while the test fixture is active, the input system will not receive
  52. /// input and device discovery or removal notifications from platform code. This ensures
  53. /// that while the test is running, input that may be generated on the machine running
  54. /// the test will not infer with it.
  55. /// </remarks>
  56. public class InputTestFixture
  57. {
  58. /// <summary>
  59. /// Put <see cref="InputSystem"/> into a known state where it only has a basic set of
  60. /// layouts and does not have any input devices.
  61. /// </summary>
  62. /// <remarks>
  63. /// If you derive your own test fixture directly from InputTestFixture, this
  64. /// method will automatically be called. If you embed InputTestFixture into
  65. /// your fixture, you have to explicitly call this method yourself.
  66. /// </remarks>
  67. /// <seealso cref="TearDown"/>
  68. [SetUp]
  69. public virtual void Setup()
  70. {
  71. try
  72. {
  73. // Apparently, NUnit is reusing instances :(
  74. m_KeyInfos = default;
  75. // Disable input debugger so we don't waste time responding to all the
  76. // input system activity from the tests.
  77. #if UNITY_EDITOR
  78. InputDebuggerWindow.Disable();
  79. #endif
  80. runtime = new InputTestRuntime();
  81. // Push current input system state on stack.
  82. InputSystem.SaveAndReset(enableRemoting: false, runtime: runtime);
  83. #if UNITY_EDITOR
  84. // Make sure we're not affected by the user giving focus away from the
  85. // game view.
  86. InputEditorUserSettings.lockInputToGameView = true;
  87. #endif
  88. // We use native collections in a couple places. We when leak them, we want to know where exactly
  89. // the allocation came from so enable full leak detection in tests.
  90. NativeLeakDetection.Mode = NativeLeakDetectionMode.EnabledWithStackTrace;
  91. }
  92. catch (Exception exception)
  93. {
  94. Debug.LogError("Failed to set up input system for test " + TestContext.CurrentContext.Test.Name);
  95. Debug.LogException(exception);
  96. throw;
  97. }
  98. if (InputSystem.devices.Count > 0)
  99. Assert.Fail("Input system should not have devices after reset");
  100. }
  101. /// <summary>
  102. /// Restore the state of the input system it had when the test was started.
  103. /// </summary>
  104. /// <seealso cref="Setup"/>
  105. [TearDown]
  106. public virtual void TearDown()
  107. {
  108. try
  109. {
  110. // Destroy any GameObject in the current scene that isn't hidden and isn't the
  111. // test runner object. Do this first so that any cleanup finds the system in the
  112. // state it expects.
  113. var scene = SceneManager.GetActiveScene();
  114. foreach (var go in scene.GetRootGameObjects())
  115. {
  116. if (go.hideFlags != 0 || go.name.Contains("tests runner"))
  117. continue;
  118. Object.DestroyImmediate(go);
  119. }
  120. InputSystem.Restore();
  121. runtime.Dispose();
  122. // Re-enable input debugger.
  123. #if UNITY_EDITOR
  124. InputDebuggerWindow.Enable();
  125. #endif
  126. }
  127. catch (Exception exception)
  128. {
  129. Debug.LogError("Failed to shut down and restore input system after test " + TestContext.CurrentContext.Test.Name);
  130. Debug.LogException(exception);
  131. throw;
  132. }
  133. }
  134. // ReSharper disable once MemberCanBeProtected.Global
  135. public static void AssertButtonPress<TState>(InputDevice device, TState state, params ButtonControl[] buttons)
  136. where TState : struct, IInputStateTypeInfo
  137. {
  138. // Update state.
  139. InputSystem.QueueStateEvent(device, state);
  140. InputSystem.Update();
  141. // Now verify that only the buttons we expect to be pressed are pressed.
  142. foreach (var control in device.allControls)
  143. {
  144. if (!(control is ButtonControl controlAsButton))
  145. continue;
  146. var isInList = buttons.Contains(controlAsButton);
  147. if (!isInList)
  148. Assert.That(controlAsButton.isPressed, Is.False,
  149. $"Expected button {controlAsButton} to NOT be pressed");
  150. else
  151. Assert.That(controlAsButton.isPressed, Is.True,
  152. $"Expected button {controlAsButton} to be pressed");
  153. }
  154. }
  155. private Dictionary<Key, Tuple<string, int>> m_KeyInfos;
  156. /// <summary>
  157. /// Set <see cref="Keyboard.keyboardLayout"/> of the given keyboard.
  158. /// </summary>
  159. /// <param name="name">Name of the keyboard layout to switch to.</param>
  160. /// <param name="keyboard">Keyboard to switch layout on. If <c>null</c>, <see cref="Keyboard.current"/> is used.</param>
  161. /// <exception cref="ArgumentException"><paramref name="keyboard"/> and <see cref="Keyboard.current"/> are both <c>null</c>.</exception>
  162. /// <remarks>
  163. /// Also queues and immediately processes an <see cref="DeviceConfigurationEvent"/> for the keyboard.
  164. /// </remarks>
  165. public unsafe void SetKeyboardLayout(string name, Keyboard keyboard = null)
  166. {
  167. if (keyboard == null)
  168. {
  169. keyboard = Keyboard.current;
  170. if (keyboard == null)
  171. throw new ArgumentException("No keyboard has been created and no keyboard has been given", nameof(keyboard));
  172. }
  173. runtime.SetDeviceCommandCallback(keyboard, (id, command) =>
  174. {
  175. if (id == QueryKeyboardLayoutCommand.Type)
  176. {
  177. var commandPtr = (QueryKeyboardLayoutCommand*)command;
  178. commandPtr->WriteLayoutName(name);
  179. return InputDeviceCommand.GenericSuccess;
  180. }
  181. return InputDeviceCommand.GenericFailure;
  182. });
  183. // Make sure caches on keys are flushed.
  184. InputSystem.QueueConfigChangeEvent(Keyboard.current);
  185. InputSystem.Update();
  186. }
  187. /// <summary>
  188. /// Set the <see cref="InputControl.displayName"/> of <paramref name="key"/> on the current
  189. /// <see cref="Keyboard"/> to be <paramref name="displayName"/>.
  190. /// </summary>
  191. /// <param name="key">Key to set the display name for.</param>
  192. /// <param name="displayName">Display name for the key.</param>
  193. /// <param name="scanCode">Optional <see cref="KeyControl.scanCode"/> to report for the key.</param>
  194. /// <remarks>
  195. /// Automatically adds a <see cref="Keyboard"/> if none has been added yet.
  196. /// </remarks>
  197. public unsafe void SetKeyInfo(Key key, string displayName, int scanCode = 0)
  198. {
  199. if (Keyboard.current == null)
  200. InputSystem.AddDevice<Keyboard>();
  201. if (m_KeyInfos == null)
  202. {
  203. m_KeyInfos = new Dictionary<Key, Tuple<string, int>>();
  204. runtime.SetDeviceCommandCallback(Keyboard.current,
  205. (id, commandPtr) =>
  206. {
  207. if (commandPtr->type == QueryKeyNameCommand.Type)
  208. {
  209. var keyNameCommand = (QueryKeyNameCommand*)commandPtr;
  210. if (m_KeyInfos.TryGetValue((Key)keyNameCommand->scanOrKeyCode, out var info))
  211. {
  212. keyNameCommand->scanOrKeyCode = info.Item2;
  213. StringHelpers.WriteStringToBuffer(info.Item1, (IntPtr)keyNameCommand->nameBuffer,
  214. QueryKeyNameCommand.kMaxNameLength);
  215. }
  216. return QueryKeyNameCommand.kSize;
  217. }
  218. return InputDeviceCommand.GenericFailure;
  219. });
  220. }
  221. m_KeyInfos[key] = new Tuple<string, int>(displayName, scanCode);
  222. // Make sure caches on keys are flushed.
  223. InputSystem.QueueConfigChangeEvent(Keyboard.current);
  224. InputSystem.Update();
  225. }
  226. /// <summary>
  227. /// Add support for <see cref="QueryCanRunInBackground"/> to <paramref name="device"/> and return
  228. /// <paramref name="value"/> as <see cref="QueryCanRunInBackground.canRunInBackground"/>.
  229. /// </summary>
  230. /// <param name="device"></param>
  231. internal unsafe void SetCanRunInBackground(InputDevice device, bool canRunInBackground = true)
  232. {
  233. runtime.SetDeviceCommandCallback(device, (id, command) =>
  234. {
  235. if (command->type == QueryCanRunInBackground.Type)
  236. {
  237. ((QueryCanRunInBackground*)command)->canRunInBackground = canRunInBackground;
  238. return InputDeviceCommand.GenericSuccess;
  239. }
  240. return InputDeviceCommand.GenericFailure;
  241. });
  242. }
  243. public ActionConstraint Started(InputAction action, InputControl control = null, double? time = null)
  244. {
  245. return new ActionConstraint(InputActionPhase.Started, action, control, time: time, duration: 0);
  246. }
  247. public ActionConstraint Started<TValue>(InputAction action, InputControl<TValue> control, TValue value, double? time = null)
  248. where TValue : struct
  249. {
  250. return new ActionConstraint(InputActionPhase.Started, action, control, value, time: time, duration: 0);
  251. }
  252. public ActionConstraint Performed(InputAction action, InputControl control = null, double? time = null, double? duration = null)
  253. {
  254. return new ActionConstraint(InputActionPhase.Performed, action, control, time: time, duration: duration);
  255. }
  256. public ActionConstraint Performed<TValue>(InputAction action, InputControl<TValue> control, TValue value, double? time = null, double? duration = null)
  257. where TValue : struct
  258. {
  259. return new ActionConstraint(InputActionPhase.Performed, action, control, value, time: time, duration: duration);
  260. }
  261. public ActionConstraint Canceled(InputAction action, InputControl control = null, double? time = null, double? duration = null)
  262. {
  263. return new ActionConstraint(InputActionPhase.Canceled, action, control, time: time, duration: duration);
  264. }
  265. public ActionConstraint Canceled<TValue>(InputAction action, InputControl<TValue> control, TValue value, double? time = null, double? duration = null)
  266. where TValue : struct
  267. {
  268. return new ActionConstraint(InputActionPhase.Canceled, action, control, value, time: time, duration: duration);
  269. }
  270. public ActionConstraint Started<TInteraction>(InputAction action, InputControl control = null, object value = null, double? time = null)
  271. where TInteraction : IInputInteraction
  272. {
  273. return new ActionConstraint(InputActionPhase.Started, action, control, interaction: typeof(TInteraction), time: time,
  274. duration: 0, value: value);
  275. }
  276. public ActionConstraint Performed<TInteraction>(InputAction action, InputControl control = null, object value = null, double? time = null, double? duration = null)
  277. where TInteraction : IInputInteraction
  278. {
  279. return new ActionConstraint(InputActionPhase.Performed, action, control, interaction: typeof(TInteraction), time: time,
  280. duration: duration, value: value);
  281. }
  282. public ActionConstraint Canceled<TInteraction>(InputAction action, InputControl control = null, object value = null, double? time = null, double? duration = null)
  283. where TInteraction : IInputInteraction
  284. {
  285. return new ActionConstraint(InputActionPhase.Canceled, action, control, interaction: typeof(TInteraction), time: time,
  286. duration: duration, value: value);
  287. }
  288. // ReSharper disable once MemberCanBeProtected.Global
  289. public void Press(ButtonControl button, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
  290. {
  291. Set(button, 1, time, timeOffset, queueEventOnly: queueEventOnly);
  292. }
  293. // ReSharper disable once MemberCanBeProtected.Global
  294. public void Release(ButtonControl button, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
  295. {
  296. Set(button, 0, time, timeOffset, queueEventOnly: queueEventOnly);
  297. }
  298. // ReSharper disable once MemberCanBePrivate.Global
  299. public void PressAndRelease(ButtonControl button, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
  300. {
  301. Press(button, time, timeOffset, queueEventOnly: true); // This one is always just a queue.
  302. Release(button, time, timeOffset, queueEventOnly: queueEventOnly);
  303. }
  304. // ReSharper disable once MemberCanBeProtected.Global
  305. public void Click(ButtonControl button, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
  306. {
  307. PressAndRelease(button, time, timeOffset, queueEventOnly: queueEventOnly);
  308. }
  309. /// <summary>
  310. /// Set the control with the given <paramref name="path"/> on <paramref name="device"/> to the given <paramref name="state"/>
  311. /// by sending a state event with the value to the device.
  312. /// </summary>
  313. /// <param name="device">Device on which to find a control.</param>
  314. /// <param name="path">Path of the control on the device.</param>
  315. /// <param name="state">New state for the control.</param>
  316. /// <param name="time">Timestamp to use for the state event. If -1 (default), current time is used (see <see cref="InputTestFixture.currentTime"/>).</param>
  317. /// <param name="timeOffset">Offset to apply to the current time. This is an alternative to <paramref name="time"/>. By default, no offset is applied.</param>
  318. /// <param name="queueEventOnly">If true, no <see cref="InputSystem.Update"/> will be performed after queueing the event. This will only put
  319. /// the state event on the event queue and not do anything else. The default is to call <see cref="InputSystem.Update"/> after queuing the event.
  320. /// Note that not issuing an update means the state of the device will not change yet. This may affect subsequent Set/Press/Release/etc calls
  321. /// as they will not yet see the state change.</param>
  322. /// <typeparam name="TValue">Value type of the control.</typeparam>
  323. /// <example>
  324. /// <code>
  325. /// var device = InputSystem.AddDevice("TestDevice");
  326. /// Set&lt;ButtonControl&gt;(device, "button", 1);
  327. /// Set&lt;AxisControl&gt;(device, "{Primary2DMotion}/x", 123.456f);
  328. /// </code>
  329. /// </example>
  330. public void Set<TValue>(InputDevice device, string path, TValue state, double time = -1, double timeOffset = 0,
  331. bool queueEventOnly = false)
  332. where TValue : struct
  333. {
  334. if (device == null)
  335. throw new ArgumentNullException(nameof(device));
  336. if (string.IsNullOrEmpty(path))
  337. throw new ArgumentNullException(nameof(path));
  338. var control = (InputControl<TValue>)device[path];
  339. Set(control, state, time, timeOffset, queueEventOnly);
  340. }
  341. /// <summary>
  342. /// Set the control to the given value by sending a state event with the value to the
  343. /// control's device.
  344. /// </summary>
  345. /// <param name="control">An input control on a device that has been added to the system.</param>
  346. /// <param name="state">New value for the input control.</param>
  347. /// <param name="time">Timestamp to use for the state event. If -1 (default), current time is used (see <see cref="InputTestFixture.currentTime"/>).</param>
  348. /// <param name="timeOffset">Offset to apply to the current time. This is an alternative to <paramref name="time"/>. By default, no offset is applied.</param>
  349. /// <param name="queueEventOnly">If true, no <see cref="InputSystem.Update"/> will be performed after queueing the event. This will only put
  350. /// the state event on the event queue and not do anything else. The default is to call <see cref="InputSystem.Update"/> after queuing the event.
  351. /// Note that not issuing an update means the state of the device will not change yet. This may affect subsequent Set/Press/Release/etc calls
  352. /// as they will not yet see the state change.</param>
  353. /// <typeparam name="TValue">Value type of the given control.</typeparam>
  354. /// <example>
  355. /// <code>
  356. /// var gamepad = InputSystem.AddDevice&lt;Gamepad&gt;();
  357. /// Set(gamepad.leftButton, 1);
  358. /// </code>
  359. /// </example>
  360. public void Set<TValue>(InputControl<TValue> control, TValue state, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
  361. where TValue : struct
  362. {
  363. if (control == null)
  364. throw new ArgumentNullException(nameof(control));
  365. if (!control.device.added)
  366. throw new ArgumentException(
  367. $"Device of control '{control}' has not been added to the system", nameof(control));
  368. void SetUpAndQueueEvent(InputEventPtr eventPtr)
  369. {
  370. ////REVIEW: should we by default take the time from the device here?
  371. if (time >= 0)
  372. eventPtr.time = time;
  373. eventPtr.time += timeOffset;
  374. control.WriteValueIntoEvent(state, eventPtr);
  375. InputSystem.QueueEvent(eventPtr);
  376. }
  377. // Touchscreen does not support delta events involving TouchState.
  378. if (control is TouchControl)
  379. {
  380. using (StateEvent.From(control.device, out var eventPtr))
  381. SetUpAndQueueEvent(eventPtr);
  382. }
  383. else
  384. {
  385. // We use delta state events rather than full state events here to mitigate the following problem:
  386. // Grabbing state from the device will preserve the current values of controls covered in the state.
  387. // However, running an update may alter the value of one or more of those controls. So with a full
  388. // state event, we may be writing outdated data back into the device. For example, in the case of delta
  389. // controls which will reset in OnBeforeUpdate().
  390. //
  391. // Using delta events, we may still grab state outside of just the one control in case we're looking at
  392. // bit-addressed controls but at least we can avoid the problem for the majority of controls.
  393. using (DeltaStateEvent.From(control, out var eventPtr))
  394. SetUpAndQueueEvent(eventPtr);
  395. }
  396. if (!queueEventOnly)
  397. InputSystem.Update();
  398. }
  399. public void Move(InputControl<Vector2> positionControl, Vector2 position, Vector2? delta = null, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
  400. {
  401. Set(positionControl, position, time: time, timeOffset: timeOffset, queueEventOnly: true);
  402. var deltaControl = (Vector2Control)positionControl.device.TryGetChildControl("delta");
  403. if (deltaControl != null)
  404. Set(deltaControl, delta ?? position - positionControl.ReadValue(), time: time, timeOffset: timeOffset, queueEventOnly: true);
  405. if (!queueEventOnly)
  406. InputSystem.Update();
  407. }
  408. public void BeginTouch(int touchId, Vector2 position, bool queueEventOnly = false, Touchscreen screen = null,
  409. double time = -1, double timeOffset = 0)
  410. {
  411. SetTouch(touchId, TouchPhase.Began, position, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
  412. }
  413. public void MoveTouch(int touchId, Vector2 position, Vector2 delta = default, bool queueEventOnly = false,
  414. Touchscreen screen = null, double time = -1, double timeOffset = 0)
  415. {
  416. SetTouch(touchId, TouchPhase.Moved, position, delta, queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
  417. }
  418. public void EndTouch(int touchId, Vector2 position, Vector2 delta = default, bool queueEventOnly = false,
  419. Touchscreen screen = null, double time = -1, double timeOffset = 0)
  420. {
  421. SetTouch(touchId, TouchPhase.Ended, position, delta, queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
  422. }
  423. public void CancelTouch(int touchId, Vector2 position, Vector2 delta = default, bool queueEventOnly = false,
  424. Touchscreen screen = null, double time = -1, double timeOffset = 0)
  425. {
  426. SetTouch(touchId, TouchPhase.Canceled, position, delta, queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
  427. }
  428. public void SetTouch(int touchId, TouchPhase phase, Vector2 position, Vector2 delta = default, bool queueEventOnly = true,
  429. Touchscreen screen = null, double time = -1, double timeOffset = 0)
  430. {
  431. if (screen == null)
  432. {
  433. screen = Touchscreen.current;
  434. if (screen == null)
  435. throw new InvalidOperationException("No touchscreen has been added");
  436. }
  437. InputSystem.QueueStateEvent(screen, new TouchState
  438. {
  439. touchId = touchId,
  440. phase = phase,
  441. position = position,
  442. delta = delta,
  443. }, (time >= 0 ? time : InputRuntime.s_Instance.currentTime) + timeOffset);
  444. if (!queueEventOnly)
  445. InputSystem.Update();
  446. }
  447. public void Trigger<TValue>(InputAction action, InputControl<TValue> control, TValue value)
  448. where TValue : struct
  449. {
  450. throw new NotImplementedException();
  451. }
  452. /// <summary>
  453. /// Perform the input action without having to know what it is bound to.
  454. /// </summary>
  455. /// <param name="action">An input action that is currently enabled and has controls it is bound to.</param>
  456. /// <remarks>
  457. /// Blindly triggering an action requires making a few assumptions. Actions are not built to be able to trigger
  458. /// without any input. This means that this method has to generate input on a control that the action is bound to.
  459. ///
  460. /// Note that this method has no understanding of the interactions that may be present on the action and thus
  461. /// does not know how they may affect the triggering of the action.
  462. /// </remarks>
  463. public void Trigger(InputAction action)
  464. {
  465. if (action == null)
  466. throw new ArgumentNullException(nameof(action));
  467. if (!action.enabled)
  468. throw new ArgumentException(
  469. $"Action '{action}' must be enabled in order to be able to trigger it", nameof(action));
  470. var controls = action.controls;
  471. if (controls.Count == 0)
  472. throw new ArgumentException(
  473. $"Action '{action}' must be bound to controls in order to be able to trigger it", nameof(action));
  474. // See if we have a button we can trigger.
  475. for (var i = 0; i < controls.Count; ++i)
  476. {
  477. if (!(controls[i] is ButtonControl button))
  478. continue;
  479. // Press and release button.
  480. Set(button, 1);
  481. Set(button, 0);
  482. return;
  483. }
  484. // See if we have an axis we can slide a bit.
  485. for (var i = 0; i < controls.Count; ++i)
  486. {
  487. if (!(controls[i] is AxisControl axis))
  488. continue;
  489. // We do, so nudge its value a bit.
  490. Set(axis, axis.ReadValue() + 0.01f);
  491. return;
  492. }
  493. ////TODO: support a wider range of controls
  494. throw new NotImplementedException();
  495. }
  496. /// <summary>
  497. /// The input runtime used during testing.
  498. /// </summary>
  499. internal InputTestRuntime runtime { get; private set; }
  500. /// <summary>
  501. /// Get or set the current time used by the input system.
  502. /// </summary>
  503. /// <value>Current time used by the input system.</value>
  504. public double currentTime
  505. {
  506. get => runtime.currentTime;
  507. set
  508. {
  509. runtime.currentTime = value;
  510. runtime.dontAdvanceTimeNextDynamicUpdate = true;
  511. }
  512. }
  513. public class ActionConstraint : Constraint
  514. {
  515. public InputActionPhase phase { get; set; }
  516. public double? time { get; set; }
  517. public double? duration { get; set; }
  518. public InputAction action { get; set; }
  519. public InputControl control { get; set; }
  520. public object value { get; set; }
  521. public Type interaction { get; set; }
  522. private readonly List<ActionConstraint> m_AndThen = new List<ActionConstraint>();
  523. public ActionConstraint(InputActionPhase phase, InputAction action, InputControl control, object value = null, Type interaction = null, double? time = null, double? duration = null)
  524. {
  525. this.phase = phase;
  526. this.time = time;
  527. this.duration = duration;
  528. this.action = action;
  529. this.control = control;
  530. this.value = value;
  531. this.interaction = interaction;
  532. var interactionText = string.Empty;
  533. if (interaction != null)
  534. interactionText = InputInteraction.GetDisplayName(interaction);
  535. var actionName = action.actionMap != null ? $"{action.actionMap}/{action.name}" : action.name;
  536. // Use same text format as InputActionTrace for easier comparison.
  537. var description = $"{{ action={actionName} phase={phase}";
  538. if (time != null)
  539. description += $" time={time}";
  540. if (control != null)
  541. description += $" control={control}";
  542. if (value != null)
  543. description += $" value={value}";
  544. if (interaction != null)
  545. description += $" interaction={interactionText}";
  546. if (duration != null)
  547. description += $" duration={duration}";
  548. description += " }";
  549. Description = description;
  550. }
  551. public override ConstraintResult ApplyTo(object actual)
  552. {
  553. var trace = (InputActionTrace)actual;
  554. var actions = trace.ToArray();
  555. if (actions.Length == 0)
  556. return new ConstraintResult(this, actual, false);
  557. if (!Verify(actions[0]))
  558. return new ConstraintResult(this, actual, false);
  559. var i = 1;
  560. foreach (var constraint in m_AndThen)
  561. {
  562. if (i >= actions.Length || !constraint.Verify(actions[i]))
  563. return new ConstraintResult(this, actual, false);
  564. ++i;
  565. }
  566. if (i != actions.Length)
  567. return new ConstraintResult(this, actual, false);
  568. return new ConstraintResult(this, actual, true);
  569. }
  570. private bool Verify(InputActionTrace.ActionEventPtr eventPtr)
  571. {
  572. // NOTE: Using explicit "return false" branches everywhere for easier setting of breakpoints.
  573. if (eventPtr.action != action ||
  574. eventPtr.phase != phase)
  575. return false;
  576. // Check time.
  577. if (time != null && !Mathf.Approximately((float)time.Value, (float)eventPtr.time))
  578. return false;
  579. // Check duration.
  580. if (duration != null && !Mathf.Approximately((float)duration.Value, (float)eventPtr.duration))
  581. return false;
  582. // Check control.
  583. if (control != null && eventPtr.control != control)
  584. return false;
  585. // Check interaction.
  586. if (interaction != null && (eventPtr.interaction == null ||
  587. !interaction.IsInstanceOfType(eventPtr.interaction)))
  588. return false;
  589. // Check value.
  590. if (value != null)
  591. {
  592. var val = eventPtr.ReadValueAsObject();
  593. if (value is float f)
  594. {
  595. if (!Mathf.Approximately(f, Convert.ToSingle(val)))
  596. return false;
  597. }
  598. else if (value is double d)
  599. {
  600. if (!Mathf.Approximately((float)d, (float)Convert.ToDouble(val)))
  601. return false;
  602. }
  603. else if (value is Vector2 v2)
  604. {
  605. if (!Vector2EqualityComparer.Instance.Equals(v2, (Vector2)val))
  606. return false;
  607. }
  608. else if (value is Vector3 v3)
  609. {
  610. if (!Vector3EqualityComparer.Instance.Equals(v3, (Vector3)val))
  611. return false;
  612. }
  613. else if (!value.Equals(val))
  614. return false;
  615. }
  616. return true;
  617. }
  618. public ActionConstraint AndThen(ActionConstraint constraint)
  619. {
  620. m_AndThen.Add(constraint);
  621. Description += " and\n";
  622. Description += constraint.Description;
  623. return this;
  624. }
  625. }
  626. #if UNITY_EDITOR
  627. internal void SimulateDomainReload()
  628. {
  629. // This quite invasively goes into InputSystem internals. Unfortunately, we
  630. // have no proper way of simulating domain reloads ATM. So we directly call various
  631. // internal methods here in a sequence similar to what we'd get during a domain reload.
  632. InputSystem.s_SystemObject.OnBeforeSerialize();
  633. InputSystem.s_SystemObject = null;
  634. InputSystem.InitializeInEditor(runtime);
  635. }
  636. #endif
  637. }
  638. }