123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916 |
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Reflection;
- using UnityEngine.InputSystem.Controls;
- using NUnit.Framework;
- using NUnit.Framework.Constraints;
- using NUnit.Framework.Internal;
- using Unity.Collections;
- using UnityEngine.InputSystem.LowLevel;
- using UnityEngine.InputSystem.Utilities;
- using UnityEngine.TestTools;
- using UnityEngine.TestTools.Utils;
- #if UNITY_EDITOR
- using UnityEditor;
- using UnityEngine.InputSystem.Editor;
- #endif
-
- ////TODO: must allow running UnityTests which means we have to be able to get per-frame updates yet not receive input from native
-
- ////TODO: when running tests in players, make sure that remoting is turned off
-
- ////REVIEW: always enable event diagnostics in InputTestFixture?
-
- namespace UnityEngine.InputSystem
- {
- /// <summary>
- /// A test fixture for writing tests that use the input system. Can be derived from
- /// or simply instantiated from another test fixture.
- /// </summary>
- /// <remarks>
- /// The fixture will put the input system into a known state where it has only the
- /// built-in set of basic layouts and no devices. The state of the system before
- /// starting a test is recorded and restored when the test finishes.
- ///
- /// <example>
- /// <code>
- /// public class MyInputTests : InputTestFixture
- /// {
- /// public override void Setup()
- /// {
- /// base.Setup();
- ///
- /// InputSystem.RegisterLayout<MyDevice>();
- /// }
- ///
- /// [Test]
- /// public void CanCreateMyDevice()
- /// {
- /// InputSystem.AddDevice<MyDevice>();
- /// Assert.That(InputSystem.devices, Has.Exactly(1).TypeOf<MyDevice>());
- /// }
- /// }
- /// </code>
- /// </example>
- ///
- /// The test fixture will also sever the tie of the input system to the Unity runtime.
- /// This means that while the test fixture is active, the input system will not receive
- /// input and device discovery or removal notifications from platform code. This ensures
- /// that while the test is running, input that may be generated on the machine running
- /// the test will not infer with it.
- /// </remarks>
- public class InputTestFixture
- {
- /// <summary>
- /// Put <see cref="InputSystem"/> into a known state where it only has a basic set of
- /// layouts and does not have any input devices.
- /// </summary>
- /// <remarks>
- /// If you derive your own test fixture directly from InputTestFixture, this
- /// method will automatically be called. If you embed InputTestFixture into
- /// your fixture, you have to explicitly call this method yourself.
- /// </remarks>
- /// <seealso cref="TearDown"/>
- [SetUp]
- public virtual void Setup()
- {
- try
- {
- // Apparently, NUnit is reusing instances :(
- m_KeyInfos = default;
- m_IsUnityTest = default;
- m_CurrentTest = default;
-
- // Disable input debugger so we don't waste time responding to all the
- // input system activity from the tests.
- #if UNITY_EDITOR
- InputDebuggerWindow.Disable();
- #endif
-
- runtime = new InputTestRuntime();
-
- // Push current input system state on stack.
- #if DEVELOPMENT_BUILD || UNITY_EDITOR
- InputSystem.SaveAndReset(enableRemoting: false, runtime: runtime);
- #endif
- // Override the editor messing with logic like canRunInBackground and focus and
- // make it behave like in the player.
- #if UNITY_EDITOR
- InputSystem.settings.editorInputBehaviorInPlayMode = InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView;
- #endif
-
- // For a [UnityTest] play mode test, we don't want editor updates interfering with the test,
- // so turn them off.
- #if UNITY_EDITOR
- if (Application.isPlaying && IsUnityTest())
- InputSystem.s_Manager.m_UpdateMask &= ~InputUpdateType.Editor;
- #endif
-
- // We use native collections in a couple places. We when leak them, we want to know where exactly
- // the allocation came from so enable full leak detection in tests.
- NativeLeakDetection.Mode = NativeLeakDetectionMode.EnabledWithStackTrace;
-
- // For [UnityTest]s, we need to process input in sync with the player loop. However, InputTestRuntime
- // is divorced from the player loop by virtue of not being tied into NativeInputSystem. Listen
- // for NativeInputSystem.Update here and trigger input processing in our isolated InputSystem.
- // This is irrelevant for normal [Test]s but for [UnityTest]s that run over several frames, it's crucial.
- // NOTE: We're severing the tie the previous InputManager had to NativeInputRuntime here. This means that
- // device removal events that happen to occur while tests are running will get lost.
- NativeInputRuntime.instance.onUpdate =
- (InputUpdateType updateType, ref InputEventBuffer buffer) =>
- {
- if (InputSystem.s_Manager.ShouldRunUpdate(updateType))
- InputSystem.Update(updateType);
- // We ignore any input coming from native.
- buffer.Reset();
- };
- NativeInputRuntime.instance.onShouldRunUpdate =
- updateType => true;
-
- #if UNITY_EDITOR
- m_OnPlayModeStateChange = OnPlayModeStateChange;
- EditorApplication.playModeStateChanged += m_OnPlayModeStateChange;
- #endif
-
- // Always want to merge by default
- InputSystem.settings.disableRedundantEventsMerging = false;
-
- // Turn on all optimizations and checks
- InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseOptimizedControls, true);
- InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseReadValueCaching, true);
- InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kParanoidReadValueCachingChecks, true);
-
- #if UNITY_EDITOR
- // Default mock dialogs to avoid unexpected cancellation of standard flows
- Dialog.InputActionAsset.SetSaveChanges((_) => Dialog.Result.Discard);
- Dialog.InputActionAsset.SetDiscardUnsavedChanges((_) => Dialog.Result.Discard);
- Dialog.InputActionAsset.SetCreateAndOverwriteExistingAsset((_) => Dialog.Result.Discard);
- Dialog.ControlScheme.SetDeleteControlScheme((_) => Dialog.Result.Delete);
- #endif
- }
- catch (Exception exception)
- {
- Debug.LogError("Failed to set up input system for test " + TestContext.CurrentContext.Test.Name);
- Debug.LogException(exception);
- throw;
- }
-
- m_Initialized = true;
-
- if (InputSystem.devices.Count > 0)
- Assert.Fail("Input system should not have devices after reset");
- }
-
- /// <summary>
- /// Restore the state of the input system it had when the test was started.
- /// </summary>
- /// <seealso cref="Setup"/>
- [TearDown]
- public virtual void TearDown()
- {
- if (!m_Initialized)
- return;
-
- try
- {
- #if DEVELOPMENT_BUILD || UNITY_EDITOR
- InputSystem.Restore();
- #endif
- runtime.Dispose();
-
- // Unhook from play mode state changes.
- #if UNITY_EDITOR
- if (m_OnPlayModeStateChange != null)
- EditorApplication.playModeStateChanged -= m_OnPlayModeStateChange;
- #endif
-
- // Re-enable input debugger.
- #if UNITY_EDITOR
- InputDebuggerWindow.Enable();
- #endif
-
- #if UNITY_EDITOR
- // Re-enable dialogs.
- Dialog.InputActionAsset.SetSaveChanges(null);
- Dialog.InputActionAsset.SetDiscardUnsavedChanges(null);
- Dialog.InputActionAsset.SetCreateAndOverwriteExistingAsset(null);
- Dialog.ControlScheme.SetDeleteControlScheme(null);
- #endif
- }
- catch (Exception exception)
- {
- Debug.LogError("Failed to shut down and restore input system after test " + TestContext.CurrentContext.Test.Name);
- Debug.LogException(exception);
- throw;
- }
-
- m_Initialized = false;
- }
-
- private bool? m_IsUnityTest;
- private Test m_CurrentTest;
-
- // True if the current test is a [UnityTest].
- private bool IsUnityTest()
- {
- // We cache this value so that any call after the first in a test no
- // longer allocates GC memory. Otherwise we'll run into trouble with
- // DoesNotAllocate tests.
- var test = TestContext.CurrentTestExecutionContext.CurrentTest;
- if (m_IsUnityTest.HasValue && m_CurrentTest == test)
- return m_IsUnityTest.Value;
-
- var className = test.ClassName;
- var methodName = test.MethodName;
-
- // Doesn't seem like there's a proper way to get the current test method based on
- // the information provided by NUnit (see https://github.com/nunit/nunit/issues/3354).
-
- var type = Type.GetType(className);
- if (type == null)
- {
- foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
- {
- type = assembly.GetType(className);
- if (type != null)
- break;
- }
- }
-
- if (type == null)
- {
- m_IsUnityTest = false;
- }
- else
- {
- var method = type.GetMethod(methodName);
- m_IsUnityTest = method?.GetCustomAttribute<UnityTestAttribute>() != null;
- }
-
- m_CurrentTest = test;
- return m_IsUnityTest.Value;
- }
-
- #if UNITY_EDITOR
- private Action<PlayModeStateChange> m_OnPlayModeStateChange;
- private void OnPlayModeStateChange(PlayModeStateChange change)
- {
- if (change == PlayModeStateChange.ExitingPlayMode && m_Initialized)
- TearDown();
- }
-
- #endif
-
- // ReSharper disable once MemberCanBeProtected.Global
- public static void AssertButtonPress<TState>(InputDevice device, TState state, params ButtonControl[] buttons)
- where TState : struct, IInputStateTypeInfo
- {
- // Update state.
- InputSystem.QueueStateEvent(device, state);
- InputSystem.Update();
-
- // Now verify that only the buttons we expect to be pressed are pressed.
- foreach (var control in device.allControls)
- {
- if (!(control is ButtonControl controlAsButton))
- continue;
-
- var isInList = buttons.Contains(controlAsButton);
- if (!isInList)
- Assert.That(controlAsButton.isPressed, Is.False,
- $"Expected button {controlAsButton} to NOT be pressed");
- else
- Assert.That(controlAsButton.isPressed, Is.True,
- $"Expected button {controlAsButton} to be pressed");
- }
- }
-
- public static void AssertStickValues(StickControl stick, Vector2 stickValue, float up, float down, float left,
- float right)
- {
- Assert.That(stick.ReadUnprocessedValue(), Is.EqualTo(stickValue));
-
- Assert.That(stick.up.ReadUnprocessedValue(), Is.EqualTo(up).Within(0.0001), "Incorrect 'up' value");
- Assert.That(stick.down.ReadUnprocessedValue(), Is.EqualTo(down).Within(0.0001), "Incorrect 'down' value");
- Assert.That(stick.left.ReadUnprocessedValue(), Is.EqualTo(left).Within(0.0001), "Incorrect 'left' value");
- Assert.That(stick.right.ReadUnprocessedValue(), Is.EqualTo(right).Within(0.0001), "Incorrect 'right' value");
- }
-
- private Dictionary<Key, Tuple<string, int>> m_KeyInfos;
- private bool m_Initialized;
-
- /// <summary>
- /// Set <see cref="Keyboard.keyboardLayout"/> of the given keyboard.
- /// </summary>
- /// <param name="name">Name of the keyboard layout to switch to.</param>
- /// <param name="keyboard">Keyboard to switch layout on. If <c>null</c>, <see cref="Keyboard.current"/> is used.</param>
- /// <exception cref="ArgumentException"><paramref name="keyboard"/> and <see cref="Keyboard.current"/> are both <c>null</c>.</exception>
- /// <remarks>
- /// Also queues and immediately processes an <see cref="DeviceConfigurationEvent"/> for the keyboard.
- /// </remarks>
- public unsafe void SetKeyboardLayout(string name, Keyboard keyboard = null)
- {
- if (keyboard == null)
- {
- keyboard = Keyboard.current;
- if (keyboard == null)
- throw new ArgumentException("No keyboard has been created and no keyboard has been given", nameof(keyboard));
- }
-
- runtime.SetDeviceCommandCallback(keyboard, (id, command) =>
- {
- if (id == QueryKeyboardLayoutCommand.Type)
- {
- var commandPtr = (QueryKeyboardLayoutCommand*)command;
- commandPtr->WriteLayoutName(name);
- return InputDeviceCommand.GenericSuccess;
- }
- return InputDeviceCommand.GenericFailure;
- });
-
- // Make sure caches on keys are flushed.
- InputSystem.QueueConfigChangeEvent(Keyboard.current);
- InputSystem.Update();
- }
-
- /// <summary>
- /// Set the <see cref="InputControl.displayName"/> of <paramref name="key"/> on the current
- /// <see cref="Keyboard"/> to be <paramref name="displayName"/>.
- /// </summary>
- /// <param name="key">Key to set the display name for.</param>
- /// <param name="displayName">Display name for the key.</param>
- /// <param name="scanCode">Optional <see cref="KeyControl.scanCode"/> to report for the key.</param>
- /// <remarks>
- /// Automatically adds a <see cref="Keyboard"/> if none has been added yet.
- /// </remarks>
- public unsafe void SetKeyInfo(Key key, string displayName, int scanCode = 0)
- {
- if (Keyboard.current == null)
- InputSystem.AddDevice<Keyboard>();
-
- if (m_KeyInfos == null)
- {
- m_KeyInfos = new Dictionary<Key, Tuple<string, int>>();
-
- runtime.SetDeviceCommandCallback(Keyboard.current,
- (id, commandPtr) =>
- {
- if (commandPtr->type == QueryKeyNameCommand.Type)
- {
- var keyNameCommand = (QueryKeyNameCommand*)commandPtr;
-
- if (m_KeyInfos.TryGetValue((Key)keyNameCommand->scanOrKeyCode, out var info))
- {
- keyNameCommand->scanOrKeyCode = info.Item2;
- StringHelpers.WriteStringToBuffer(info.Item1, (IntPtr)keyNameCommand->nameBuffer,
- QueryKeyNameCommand.kMaxNameLength);
- }
-
- return QueryKeyNameCommand.kSize;
- }
-
- return InputDeviceCommand.GenericFailure;
- });
- }
-
- m_KeyInfos[key] = new Tuple<string, int>(displayName, scanCode);
-
- // Make sure caches on keys are flushed.
- InputSystem.QueueConfigChangeEvent(Keyboard.current);
- InputSystem.Update();
- }
-
- /// <summary>
- /// Add support for <see cref="QueryCanRunInBackground"/> to <paramref name="device"/> and return
- /// <paramref name="value"/> as <see cref="QueryCanRunInBackground.canRunInBackground"/>.
- /// </summary>
- /// <param name="device"></param>
- internal unsafe void SetCanRunInBackground(InputDevice device, bool canRunInBackground = true)
- {
- runtime.SetDeviceCommandCallback(device, (id, command) =>
- {
- if (command->type == QueryCanRunInBackground.Type)
- {
- ((QueryCanRunInBackground*)command)->canRunInBackground = canRunInBackground;
- return InputDeviceCommand.GenericSuccess;
- }
- return InputDeviceCommand.GenericFailure;
- });
- }
-
- public ActionConstraint Started(InputAction action, InputControl control = null, double? time = null, object value = null)
- {
- return new ActionConstraint(InputActionPhase.Started, action, control, time: time, duration: 0, value: value);
- }
-
- public ActionConstraint Started<TValue>(InputAction action, InputControl<TValue> control, TValue value, double? time = null)
- where TValue : struct
- {
- return new ActionConstraint(InputActionPhase.Started, action, control, value, time: time, duration: 0);
- }
-
- public ActionConstraint Performed(InputAction action, InputControl control = null, double? time = null, double? duration = null, object value = null)
- {
- return new ActionConstraint(InputActionPhase.Performed, action, control, time: time, duration: duration, value: value);
- }
-
- public ActionConstraint Performed<TValue>(InputAction action, InputControl<TValue> control, TValue value, double? time = null, double? duration = null)
- where TValue : struct
- {
- return new ActionConstraint(InputActionPhase.Performed, action, control, value, time: time, duration: duration);
- }
-
- public ActionConstraint Canceled(InputAction action, InputControl control = null, double? time = null, double? duration = null, object value = null)
- {
- return new ActionConstraint(InputActionPhase.Canceled, action, control, time: time, duration: duration, value: value);
- }
-
- public ActionConstraint Canceled<TValue>(InputAction action, InputControl<TValue> control, TValue value, double? time = null, double? duration = null)
- where TValue : struct
- {
- return new ActionConstraint(InputActionPhase.Canceled, action, control, value, time: time, duration: duration);
- }
-
- public ActionConstraint Started<TInteraction>(InputAction action, InputControl control = null, object value = null, double? time = null)
- where TInteraction : IInputInteraction
- {
- return new ActionConstraint(InputActionPhase.Started, action, control, interaction: typeof(TInteraction), time: time,
- duration: 0, value: value);
- }
-
- public ActionConstraint Performed<TInteraction>(InputAction action, InputControl control = null, object value = null, double? time = null, double? duration = null)
- where TInteraction : IInputInteraction
- {
- return new ActionConstraint(InputActionPhase.Performed, action, control, interaction: typeof(TInteraction), time: time,
- duration: duration, value: value);
- }
-
- public ActionConstraint Canceled<TInteraction>(InputAction action, InputControl control = null, object value = null, double? time = null, double? duration = null)
- where TInteraction : IInputInteraction
- {
- return new ActionConstraint(InputActionPhase.Canceled, action, control, interaction: typeof(TInteraction), time: time,
- duration: duration, value: value);
- }
-
- ////REVIEW: Should we determine queueEventOnly automatically from whether we're in a UnityTest?
-
- // ReSharper disable once MemberCanBeProtected.Global
- public void Press(ButtonControl button, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
- {
- Set(button, 1, time, timeOffset, queueEventOnly: queueEventOnly);
- }
-
- // ReSharper disable once MemberCanBeProtected.Global
- public void Release(ButtonControl button, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
- {
- Set(button, 0, time, timeOffset, queueEventOnly: queueEventOnly);
- }
-
- // ReSharper disable once MemberCanBePrivate.Global
- public void PressAndRelease(ButtonControl button, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
- {
- Press(button, time, timeOffset, queueEventOnly: true); // This one is always just a queue.
- Release(button, time, timeOffset, queueEventOnly: queueEventOnly);
- }
-
- // ReSharper disable once MemberCanBeProtected.Global
- public void Click(ButtonControl button, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
- {
- PressAndRelease(button, time, timeOffset, queueEventOnly: queueEventOnly);
- }
-
- /// <summary>
- /// Set the control with the given <paramref name="path"/> on <paramref name="device"/> to the given <paramref name="state"/>
- /// by sending a state event with the value to the device.
- /// </summary>
- /// <param name="device">Device on which to find a control.</param>
- /// <param name="path">Path of the control on the device.</param>
- /// <param name="state">New state for the control.</param>
- /// <param name="time">Timestamp to use for the state event. If -1 (default), current time is used (see <see cref="InputTestFixture.currentTime"/>).</param>
- /// <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>
- /// <param name="queueEventOnly">If true, no <see cref="InputSystem.Update"/> will be performed after queueing the event. This will only put
- /// 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.
- /// 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
- /// as they will not yet see the state change.
- ///
- /// Note that this parameter will be ignored if the test is a <c>[UnityTest]</c>. Multi-frame
- /// playmode tests will automatically process input as part of the Unity player loop.</param>
- /// <typeparam name="TValue">Value type of the control.</typeparam>
- /// <example>
- /// <code>
- /// var device = InputSystem.AddDevice("TestDevice");
- /// Set<ButtonControl>(device, "button", 1);
- /// Set<AxisControl>(device, "{Primary2DMotion}/x", 123.456f);
- /// </code>
- /// </example>
- public void Set<TValue>(InputDevice device, string path, TValue state, double time = -1, double timeOffset = 0,
- bool queueEventOnly = false)
- where TValue : struct
- {
- if (device == null)
- throw new ArgumentNullException(nameof(device));
- if (string.IsNullOrEmpty(path))
- throw new ArgumentNullException(nameof(path));
-
- var control = (InputControl<TValue>)device[path];
- Set(control, state, time, timeOffset, queueEventOnly);
- }
-
- /// <summary>
- /// Set the control to the given value by sending a state event with the value to the
- /// control's device.
- /// </summary>
- /// <param name="control">An input control on a device that has been added to the system.</param>
- /// <param name="state">New value for the input control.</param>
- /// <param name="time">Timestamp to use for the state event. If -1 (default), current time is used (see <see cref="InputTestFixture.currentTime"/>).</param>
- /// <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>
- /// <param name="queueEventOnly">If true, no <see cref="InputSystem.Update"/> will be performed after queueing the event. This will only put
- /// 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.
- /// 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
- /// as they will not yet see the state change.
- ///
- /// Note that this parameter will be ignored if the test is a <c>[UnityTest]</c>. Multi-frame
- /// playmode tests will automatically process input as part of the Unity player loop.</param>
- /// <typeparam name="TValue">Value type of the given control.</typeparam>
- /// <example>
- /// <code>
- /// var gamepad = InputSystem.AddDevice<Gamepad>();
- /// Set(gamepad.leftButton, 1);
- /// </code>
- /// </example>
- public void Set<TValue>(InputControl<TValue> control, TValue state, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
- where TValue : struct
- {
- if (control == null)
- throw new ArgumentNullException(nameof(control));
- if (!control.device.added)
- throw new ArgumentException(
- $"Device of control '{control}' has not been added to the system", nameof(control));
-
- if (IsUnityTest())
- queueEventOnly = true;
-
- void SetUpAndQueueEvent(InputEventPtr eventPtr)
- {
- eventPtr.time = (time >= 0 ? time : InputState.currentTime) + timeOffset;
- control.WriteValueIntoEvent(state, eventPtr);
- InputSystem.QueueEvent(eventPtr);
- }
-
- // Touchscreen does not support delta events involving TouchState.
- if (control is TouchControl)
- {
- using (StateEvent.From(control.device, out var eventPtr))
- SetUpAndQueueEvent(eventPtr);
- }
- else
- {
- // We use delta state events rather than full state events here to mitigate the following problem:
- // Grabbing state from the device will preserve the current values of controls covered in the state.
- // However, running an update may alter the value of one or more of those controls. So with a full
- // state event, we may be writing outdated data back into the device. For example, in the case of delta
- // controls which will reset in OnBeforeUpdate().
- //
- // Using delta events, we may still grab state outside of just the one control in case we're looking at
- // bit-addressed controls but at least we can avoid the problem for the majority of controls.
- using (DeltaStateEvent.From(control, out var eventPtr))
- SetUpAndQueueEvent(eventPtr);
- }
-
- if (!queueEventOnly)
- InputSystem.Update();
- }
-
- public void Move(InputControl<Vector2> positionControl, Vector2 position, Vector2? delta = null, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
- {
- Set(positionControl, position, time: time, timeOffset: timeOffset, queueEventOnly: true);
-
- var deltaControl = (Vector2Control)positionControl.device.TryGetChildControl("delta");
- if (deltaControl != null)
- Set(deltaControl, delta ?? position - positionControl.ReadValue(), time: time, timeOffset: timeOffset, queueEventOnly: true);
-
- if (!queueEventOnly)
- InputSystem.Update();
- }
-
- ////TODO: obsolete this one in 2.0 and use pressure=1 default value
- public void BeginTouch(int touchId, Vector2 position, bool queueEventOnly = false, Touchscreen screen = null,
- double time = -1, double timeOffset = 0, byte displayIndex = 0)
- {
- SetTouch(touchId, TouchPhase.Began, position, 1, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset, displayIndex: displayIndex);
- }
-
- public void BeginTouch(int touchId, Vector2 position, float pressure, bool queueEventOnly = false, Touchscreen screen = null,
- double time = -1, double timeOffset = 0)
- {
- SetTouch(touchId, TouchPhase.Began, position, pressure, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
- }
-
- ////TODO: obsolete this one in 2.0 and use pressure=1 default value
- public void MoveTouch(int touchId, Vector2 position, Vector2 delta = default, bool queueEventOnly = false,
- Touchscreen screen = null, double time = -1, double timeOffset = 0)
- {
- SetTouch(touchId, TouchPhase.Moved, position, 1, delta, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
- }
-
- public void MoveTouch(int touchId, Vector2 position, float pressure, Vector2 delta = default, bool queueEventOnly = false,
- Touchscreen screen = null, double time = -1, double timeOffset = 0)
- {
- SetTouch(touchId, TouchPhase.Moved, position, pressure, delta, queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
- }
-
- ////TODO: obsolete this one in 2.0 and use pressure=1 default value
- public void EndTouch(int touchId, Vector2 position, Vector2 delta = default, bool queueEventOnly = false,
- Touchscreen screen = null, double time = -1, double timeOffset = 0, byte displayIndex = 0)
- {
- SetTouch(touchId, TouchPhase.Ended, position, 1, delta, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset, displayIndex: displayIndex);
- }
-
- public void EndTouch(int touchId, Vector2 position, float pressure, Vector2 delta = default, bool queueEventOnly = false,
- Touchscreen screen = null, double time = -1, double timeOffset = 0)
- {
- SetTouch(touchId, TouchPhase.Ended, position, pressure, delta, queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
- }
-
- ////TODO: obsolete this one in 2.0 and use pressure=1 default value
- public void CancelTouch(int touchId, Vector2 position, Vector2 delta = default, bool queueEventOnly = false,
- Touchscreen screen = null, double time = -1, double timeOffset = 0)
- {
- SetTouch(touchId, TouchPhase.Canceled, position, delta, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
- }
-
- public void CancelTouch(int touchId, Vector2 position, float pressure, Vector2 delta = default, bool queueEventOnly = false,
- Touchscreen screen = null, double time = -1, double timeOffset = 0)
- {
- SetTouch(touchId, TouchPhase.Canceled, position, pressure, delta, queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
- }
-
- ////TODO: obsolete this one in 2.0 and use pressure=1 default value
- public void SetTouch(int touchId, TouchPhase phase, Vector2 position, Vector2 delta = default,
- bool queueEventOnly = true, Touchscreen screen = null, double time = -1, double timeOffset = 0)
- {
- SetTouch(touchId, phase, position, 1, delta: delta, queueEventOnly: queueEventOnly, screen: screen, time: time,
- timeOffset: timeOffset);
- }
-
- public void SetTouch(int touchId, TouchPhase phase, Vector2 position, float pressure, Vector2 delta = default, bool queueEventOnly = true,
- Touchscreen screen = null, double time = -1, double timeOffset = 0, byte displayIndex = 0)
- {
- if (screen == null)
- {
- screen = Touchscreen.current;
- if (screen == null)
- screen = InputSystem.AddDevice<Touchscreen>();
- }
-
- InputSystem.QueueStateEvent(screen, new TouchState
- {
- touchId = touchId,
- phase = phase,
- position = position,
- delta = delta,
- pressure = pressure,
- displayIndex = displayIndex,
- }, (time >= 0 ? time : InputState.currentTime) + timeOffset);
- if (!queueEventOnly)
- InputSystem.Update();
- }
-
- public void Trigger<TValue>(InputAction action, InputControl<TValue> control, TValue value)
- where TValue : struct
- {
- throw new NotImplementedException();
- }
-
- /// <summary>
- /// Perform the input action without having to know what it is bound to.
- /// </summary>
- /// <param name="action">An input action that is currently enabled and has controls it is bound to.</param>
- /// <remarks>
- /// Blindly triggering an action requires making a few assumptions. Actions are not built to be able to trigger
- /// without any input. This means that this method has to generate input on a control that the action is bound to.
- ///
- /// Note that this method has no understanding of the interactions that may be present on the action and thus
- /// does not know how they may affect the triggering of the action.
- /// </remarks>
- public void Trigger(InputAction action)
- {
- if (action == null)
- throw new ArgumentNullException(nameof(action));
-
- if (!action.enabled)
- throw new ArgumentException(
- $"Action '{action}' must be enabled in order to be able to trigger it", nameof(action));
-
- var controls = action.controls;
- if (controls.Count == 0)
- throw new ArgumentException(
- $"Action '{action}' must be bound to controls in order to be able to trigger it", nameof(action));
-
- // See if we have a button we can trigger.
- for (var i = 0; i < controls.Count; ++i)
- {
- if (!(controls[i] is ButtonControl button))
- continue;
-
- // Press and release button.
- Set(button, 1);
- Set(button, 0);
-
- return;
- }
-
- // See if we have an axis we can slide a bit.
- for (var i = 0; i < controls.Count; ++i)
- {
- if (!(controls[i] is AxisControl axis))
- continue;
-
- // We do, so nudge its value a bit.
- Set(axis, axis.ReadValue() + 0.01f);
-
- return;
- }
-
- ////TODO: support a wider range of controls
- throw new NotImplementedException();
- }
-
- /// <summary>
- /// The input runtime used during testing.
- /// </summary>
- internal InputTestRuntime runtime { get; private set; }
-
- /// <summary>
- /// Get or set the current time used by the input system.
- /// </summary>
- /// <value>Current time used by the input system.</value>
- public double currentTime
- {
- get => runtime.currentTime - runtime.currentTimeOffsetToRealtimeSinceStartup;
- set
- {
- runtime.currentTime = value + runtime.currentTimeOffsetToRealtimeSinceStartup;
- runtime.dontAdvanceTimeNextDynamicUpdate = true;
- }
- }
-
- internal float unscaledGameTime
- {
- get => runtime.unscaledGameTime;
- set
- {
- runtime.unscaledGameTime = value;
- runtime.dontAdvanceUnscaledGameTimeNextDynamicUpdate = true;
- }
- }
-
- public class ActionConstraint : Constraint
- {
- public InputActionPhase phase { get; set; }
- public double? time { get; set; }
- public double? duration { get; set; }
- public InputAction action { get; set; }
- public InputControl control { get; set; }
- public object value { get; set; }
- public Type interaction { get; set; }
-
- private readonly List<ActionConstraint> m_AndThen = new List<ActionConstraint>();
-
- public ActionConstraint(InputActionPhase phase, InputAction action, InputControl control, object value = null, Type interaction = null, double? time = null, double? duration = null)
- {
- this.phase = phase;
- this.time = time;
- this.duration = duration;
- this.action = action;
- this.control = control;
- this.value = value;
- this.interaction = interaction;
-
- var interactionText = string.Empty;
- if (interaction != null)
- interactionText = InputInteraction.GetDisplayName(interaction);
-
- var actionName = action.actionMap != null ? $"{action.actionMap}/{action.name}" : action.name;
- // Use same text format as InputActionTrace for easier comparison.
- var description = $"{{ action={actionName} phase={phase}";
- if (time != null)
- description += $" time={time}";
- if (control != null)
- description += $" control={control}";
- if (value != null)
- description += $" value={value}";
- if (interaction != null)
- description += $" interaction={interactionText}";
- if (duration != null)
- description += $" duration={duration}";
- description += " }";
- Description = description;
- }
-
- public override ConstraintResult ApplyTo(object actual)
- {
- var trace = (InputActionTrace)actual;
- var actions = trace.ToArray();
-
- if (actions.Length == 0)
- return new ConstraintResult(this, actual, false);
-
- if (!Verify(actions[0]))
- return new ConstraintResult(this, actual, false);
-
- var i = 1;
- foreach (var constraint in m_AndThen)
- {
- if (i >= actions.Length || !constraint.Verify(actions[i]))
- return new ConstraintResult(this, actual, false);
- ++i;
- }
-
- if (i != actions.Length)
- return new ConstraintResult(this, actual, false);
-
- return new ConstraintResult(this, actual, true);
- }
-
- private bool Verify(InputActionTrace.ActionEventPtr eventPtr)
- {
- // NOTE: Using explicit "return false" branches everywhere for easier setting of breakpoints.
-
- if (eventPtr.action != action ||
- eventPtr.phase != phase)
- return false;
-
- // Check time.
- if (time != null && !Mathf.Approximately((float)time.Value, (float)eventPtr.time))
- return false;
-
- // Check duration.
- if (duration != null && !Mathf.Approximately((float)duration.Value, (float)eventPtr.duration))
- return false;
-
- // Check control.
- if (control != null && eventPtr.control != control)
- return false;
-
- // Check interaction.
- if (interaction != null && (eventPtr.interaction == null ||
- !interaction.IsInstanceOfType(eventPtr.interaction)))
- return false;
-
- // Check value.
- if (value != null)
- {
- var val = eventPtr.ReadValueAsObject();
- if (val is float f)
- {
- if (!Mathf.Approximately(f, Convert.ToSingle(value)))
- return false;
- }
- else if (val is double d)
- {
- if (!Mathf.Approximately((float)d, (float)Convert.ToDouble(value)))
- return false;
- }
- else if (val is Vector2 v2)
- {
- if (!Vector2EqualityComparer.Instance.Equals(v2, value.As<Vector2>()))
- return false;
- }
- else if (val is Vector3 v3)
- {
- if (!Vector3EqualityComparer.Instance.Equals(v3, value.As<Vector3>()))
- return false;
- }
- else if (!val.Equals(value))
- return false;
- }
-
- return true;
- }
-
- public ActionConstraint AndThen(ActionConstraint constraint)
- {
- m_AndThen.Add(constraint);
- Description += " and\n";
- Description += constraint.Description;
- return this;
- }
- }
-
- #if UNITY_EDITOR
- internal void SimulateDomainReload()
- {
- // This quite invasively goes into InputSystem internals. Unfortunately, we
- // have no proper way of simulating domain reloads ATM. So we directly call various
- // internal methods here in a sequence similar to what we'd get during a domain reload.
-
- InputSystem.s_SystemObject.OnBeforeSerialize();
- InputSystem.s_SystemObject = null;
- InputSystem.InitializeInEditor(runtime);
- }
-
- #endif
- }
- }
|