2023-03-28 13:24:16 -04:00
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.
InputSystem . SaveAndReset ( enableRemoting : false , runtime : runtime ) ;
// 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 ;
2023-05-07 18:43:11 -04:00
// Turn on all optimizations and checks
InputSystem . settings . SetInternalFeatureFlag ( InputFeatureNames . kUseOptimizedControls , true ) ;
InputSystem . settings . SetInternalFeatureFlag ( InputFeatureNames . kUseReadValueCaching , true ) ;
InputSystem . settings . SetInternalFeatureFlag ( InputFeatureNames . kParanoidReadValueCachingChecks , true ) ;
2023-03-28 13:24:16 -04:00
}
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
{
InputSystem . Restore ( ) ;
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
}
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 ,
2023-05-07 18:43:11 -04:00
double time = - 1 , double timeOffset = 0 , byte displayIndex = 0 )
2023-03-28 13:24:16 -04:00
{
2023-05-07 18:43:11 -04:00
SetTouch ( touchId , TouchPhase . Began , position , 1 , queueEventOnly : queueEventOnly , screen : screen , time : time , timeOffset : timeOffset , displayIndex : displayIndex ) ;
2023-03-28 13:24:16 -04:00
}
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 ,
2023-05-07 18:43:11 -04:00
Touchscreen screen = null , double time = - 1 , double timeOffset = 0 , byte displayIndex = 0 )
2023-03-28 13:24:16 -04:00
{
2023-05-07 18:43:11 -04:00
SetTouch ( touchId , TouchPhase . Ended , position , 1 , delta , queueEventOnly : queueEventOnly , screen : screen , time : time , timeOffset : timeOffset , displayIndex : displayIndex ) ;
2023-03-28 13:24:16 -04:00
}
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 ,
2023-05-07 18:43:11 -04:00
Touchscreen screen = null , double time = - 1 , double timeOffset = 0 , byte displayIndex = 0 )
2023-03-28 13:24:16 -04:00
{
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 ,
2023-05-07 18:43:11 -04:00
displayIndex = displayIndex ,
2023-03-28 13:24:16 -04:00
} , ( 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
}
}