287 lines
15 KiB
C#
287 lines
15 KiB
C#
using System;
|
|
using Unity.Collections.LowLevel.Unsafe;
|
|
using UnityEngine.InputSystem.Utilities;
|
|
|
|
////TODO: method to get raw state pointer for device/control
|
|
|
|
////REVIEW: allow to restrict state change monitors to specific updates?
|
|
|
|
namespace UnityEngine.InputSystem.LowLevel
|
|
{
|
|
using NotifyControlValueChangeAction = Action<InputControl, double, InputEventPtr, long>;
|
|
using NotifyTimerExpiredAction = Action<InputControl, double, long, int>;
|
|
|
|
/// <summary>
|
|
/// Low-level APIs for working with input state memory.
|
|
/// </summary>
|
|
public static class InputState
|
|
{
|
|
/// <summary>
|
|
/// The type of update that was last run or is currently being run on the input state.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This determines which set of buffers are currently active and thus determines which view code
|
|
/// that queries input state will receive. For example, during editor updates, this will be
|
|
/// <see cref="InputUpdateType.Editor"/> and the state buffers for the editor will be active.
|
|
/// </remarks>
|
|
public static InputUpdateType currentUpdateType => InputUpdate.s_LatestUpdateType;
|
|
|
|
////FIXME: ATM this does not work for editor updates
|
|
/// <summary>
|
|
/// The number of times the current input state has been updated.
|
|
/// </summary>
|
|
public static uint updateCount => InputUpdate.s_UpdateStepCount;
|
|
|
|
public static double currentTime => InputRuntime.s_Instance.currentTime - InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup;
|
|
|
|
/// <summary>
|
|
/// Callback that is triggered when the state of an input device changes.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The first parameter is the device whose state was changed the second parameter is the event
|
|
/// that triggered the change in state. Note that the latter may be <c>null</c> in case the
|
|
/// change was performed directly through <see cref="Change"/> rather than through an event.
|
|
/// </remarks>
|
|
public static event Action<InputDevice, InputEventPtr> onChange
|
|
{
|
|
add => InputSystem.s_Manager.onDeviceStateChange += value;
|
|
remove => InputSystem.s_Manager.onDeviceStateChange -= value;
|
|
}
|
|
|
|
public static unsafe void Change(InputDevice device, InputEventPtr eventPtr, InputUpdateType updateType = default)
|
|
{
|
|
if (device == null)
|
|
throw new ArgumentNullException(nameof(device));
|
|
if (!eventPtr.valid)
|
|
throw new ArgumentNullException(nameof(eventPtr));
|
|
|
|
// Make sure event is a StateEvent or DeltaStateEvent and has a format matching the device.
|
|
FourCC stateFormat;
|
|
var eventType = eventPtr.type;
|
|
if (eventType == StateEvent.Type)
|
|
stateFormat = StateEvent.FromUnchecked(eventPtr)->stateFormat;
|
|
else if (eventType == DeltaStateEvent.Type)
|
|
stateFormat = DeltaStateEvent.FromUnchecked(eventPtr)->stateFormat;
|
|
else
|
|
{
|
|
#if UNITY_EDITOR
|
|
InputSystem.s_Manager.m_Diagnostics?.OnEventFormatMismatch(eventPtr, device);
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
if (stateFormat != device.stateBlock.format)
|
|
throw new ArgumentException(
|
|
$"State format {stateFormat} from event does not match state format {device.stateBlock.format} of device {device}",
|
|
nameof(eventPtr));
|
|
|
|
InputSystem.s_Manager.UpdateState(device, eventPtr,
|
|
updateType != default ? updateType : InputSystem.s_Manager.defaultUpdateType);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Perform one update of input state.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Incorporates the given state and triggers all state change monitors as needed.
|
|
///
|
|
/// Note that input state changes performed with this method will not be visible on remotes as they will bypass
|
|
/// event processing. It is effectively equivalent to directly writing into input state memory except that it
|
|
/// also performs related tasks such as checking state change monitors, flipping buffers, or making the respective
|
|
/// device current.
|
|
/// </remarks>
|
|
public static void Change<TState>(InputControl control, TState state, InputUpdateType updateType = default,
|
|
InputEventPtr eventPtr = default)
|
|
where TState : struct
|
|
{
|
|
Change(control, ref state, updateType, eventPtr);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Perform one update of input state.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Incorporates the given state and triggers all state change monitors as needed.
|
|
///
|
|
/// Note that input state changes performed with this method will not be visible on remotes as they will bypass
|
|
/// event processing. It is effectively equivalent to directly writing into input state memory except that it
|
|
/// also performs related tasks such as checking state change monitors, flipping buffers, or making the respective
|
|
/// device current.
|
|
/// </remarks>
|
|
public static unsafe void Change<TState>(InputControl control, ref TState state, InputUpdateType updateType = default,
|
|
InputEventPtr eventPtr = default)
|
|
where TState : struct
|
|
{
|
|
if (control == null)
|
|
throw new ArgumentNullException(nameof(control));
|
|
if (control.stateBlock.bitOffset != 0 || control.stateBlock.sizeInBits % 8 != 0)
|
|
throw new ArgumentException($"Cannot change state of bitfield control '{control}' using this method", nameof(control));
|
|
|
|
var device = control.device;
|
|
var stateSize = Math.Min(UnsafeUtility.SizeOf<TState>(), control.m_StateBlock.alignedSizeInBytes);
|
|
var statePtr = UnsafeUtility.AddressOf(ref state);
|
|
var stateOffset = control.stateBlock.byteOffset - device.stateBlock.byteOffset;
|
|
|
|
InputSystem.s_Manager.UpdateState(device,
|
|
updateType != default ? updateType : InputSystem.s_Manager.defaultUpdateType, statePtr, stateOffset,
|
|
(uint)stateSize,
|
|
eventPtr.valid
|
|
? eventPtr.internalTime
|
|
: InputRuntime.s_Instance.currentTime,
|
|
eventPtr: eventPtr);
|
|
}
|
|
|
|
public static bool IsIntegerFormat(this FourCC format)
|
|
{
|
|
return format == InputStateBlock.FormatBit ||
|
|
format == InputStateBlock.FormatInt ||
|
|
format == InputStateBlock.FormatByte ||
|
|
format == InputStateBlock.FormatShort ||
|
|
format == InputStateBlock.FormatSBit ||
|
|
format == InputStateBlock.FormatUInt ||
|
|
format == InputStateBlock.FormatUShort ||
|
|
format == InputStateBlock.FormatLong ||
|
|
format == InputStateBlock.FormatULong;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a monitor that gets triggered every time the state of <paramref name="control"/> changes.
|
|
/// </summary>
|
|
/// <param name="control">A control sitting on an <see cref="InputDevice"/> that has been <see cref="InputDevice.added"/>.</param>
|
|
/// <param name="monitor">Instance of the monitor that should be notified when state changes occur.</param>
|
|
/// <param name="monitorIndex">Numeric index of the monitors. Monitors on a device are ordered by <em>decreasing</em> monitor index
|
|
/// and invoked in that order.</param>
|
|
/// <param name="groupIndex">Numeric group of the monitor. See remarks.</param>
|
|
/// <exception cref="ArgumentNullException"><paramref name="control"/> is <c>null</c> -or- <paramref name="monitor"/> is <c>null</c>.</exception>
|
|
/// <exception cref="ArgumentException">The <see cref="InputDevice"/> of <paramref name="control"/> has not been <see cref="InputDevice.added"/>.</exception>
|
|
/// <remarks>
|
|
/// All monitors on an <see cref="InputDevice"/> are sorted by the complexity specified in their <paramref name="monitorIndex"/> (in decreasing order) and invoked
|
|
/// in that order.
|
|
///
|
|
/// Every handler gets an opportunity to set <see cref="InputEventPtr.handled"/> to <c>true</c>. When doing so, all remaining pending monitors
|
|
/// from the same <paramref name="monitor"/> instance that have the same <paramref name="groupIndex"/> will be silenced and skipped over.
|
|
/// This can be used to establish an order of event "consumption" where one change monitor may prevent another change monitor from triggering.
|
|
///
|
|
/// Monitors are invoked <em>after</em> a state change has been written to the device. If, for example, a <see cref="StateEvent"/> is
|
|
/// received that sets <see cref="Gamepad.leftTrigger"/> to <c>0.5</c>, the value is first applied to the control and then any state
|
|
/// monitors that may be listening to the change are invoked (thus getting <c>0.5</c> if calling <see cref="InputControl{TValue}.ReadValue()"/>).
|
|
///
|
|
/// <example>
|
|
/// <code>
|
|
/// class InputMonitor : IInputStateChangeMonitor
|
|
/// {
|
|
/// public InputMonitor()
|
|
/// {
|
|
/// // Watch the left and right mouse button.
|
|
/// // By supplying monitor indices here, we not only receive the indices in NotifyControlStateChanged,
|
|
/// // we also create an ordering between the two monitors. The one on RMB will fire *before* the one
|
|
/// // on LMB in case there is a single event that changes both buttons.
|
|
/// InputState.AddChangeMonitor(Mouse.current.leftButton, this, monitorIndex: 1);
|
|
/// InputState.AddChangeMonitor(Mouse.current.rightButton, this, monitorIndex: 2);
|
|
/// }
|
|
///
|
|
/// public void NotifyControlStateChanged(InputControl control, double time, InputEventPtr eventPtr, long monitorIndex)
|
|
/// {
|
|
/// Debug.Log($"{control} changed");
|
|
///
|
|
/// // We can add a monitor timeout that will trigger in case the state of the
|
|
/// // given control is not changed within the given time. Let's watch the control
|
|
/// // for 2 seconds. If nothing happens, we will get a call to NotifyTimerExpired.
|
|
/// // If, however, there is a state change, the timeout is automatically removed
|
|
/// // and we will see a call to NotifyControlStateChanged instead.
|
|
/// InputState.AddChangeMonitorTimeout(control, this, 2);
|
|
/// }
|
|
///
|
|
/// public void NotifyTimerExpired(InputControl control, double time, long monitorIndex, int timerIndex)
|
|
/// {
|
|
/// Debug.Log($"{control} was not changed within 2 seconds");
|
|
/// }
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
/// </remarks>
|
|
public static void AddChangeMonitor(InputControl control, IInputStateChangeMonitor monitor, long monitorIndex = -1, uint groupIndex = default)
|
|
{
|
|
if (control == null)
|
|
throw new ArgumentNullException(nameof(control));
|
|
if (monitor == null)
|
|
throw new ArgumentNullException(nameof(monitor));
|
|
if (!control.device.added)
|
|
throw new ArgumentException($"Device for control '{control}' has not been added to system");
|
|
|
|
InputSystem.s_Manager.AddStateChangeMonitor(control, monitor, monitorIndex, groupIndex);
|
|
}
|
|
|
|
public static IInputStateChangeMonitor AddChangeMonitor(InputControl control,
|
|
NotifyControlValueChangeAction valueChangeCallback, int monitorIndex = -1,
|
|
NotifyTimerExpiredAction timerExpiredCallback = null)
|
|
{
|
|
if (valueChangeCallback == null)
|
|
throw new ArgumentNullException(nameof(valueChangeCallback));
|
|
var monitor = new StateChangeMonitorDelegate
|
|
{
|
|
valueChangeCallback = valueChangeCallback,
|
|
timerExpiredCallback = timerExpiredCallback
|
|
};
|
|
AddChangeMonitor(control, monitor, monitorIndex);
|
|
return monitor;
|
|
}
|
|
|
|
public static void RemoveChangeMonitor(InputControl control, IInputStateChangeMonitor monitor, long monitorIndex = -1)
|
|
{
|
|
if (control == null)
|
|
throw new ArgumentNullException(nameof(control));
|
|
if (monitor == null)
|
|
throw new ArgumentNullException(nameof(monitor));
|
|
|
|
InputSystem.s_Manager.RemoveStateChangeMonitor(control, monitor, monitorIndex);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Put a timeout on a previously registered state change monitor.
|
|
/// </summary>
|
|
/// <param name="control"></param>
|
|
/// <param name="monitor"></param>
|
|
/// <param name="time"></param>
|
|
/// <param name="monitorIndex"></param>
|
|
/// <param name="timerIndex"></param>
|
|
/// <remarks>
|
|
/// If by the given <paramref name="time"/>, no state change has been registered on the control monitored
|
|
/// by the given <paramref name="monitor">state change monitor</paramref>, <see cref="IInputStateChangeMonitor.NotifyTimerExpired"/>
|
|
/// will be called on <paramref name="monitor"/>. If a state change happens by the given <paramref name="time"/>,
|
|
/// the monitor is notified as usual and the timer is automatically removed.
|
|
/// </remarks>
|
|
public static void AddChangeMonitorTimeout(InputControl control, IInputStateChangeMonitor monitor, double time, long monitorIndex = -1, int timerIndex = -1)
|
|
{
|
|
if (monitor == null)
|
|
throw new ArgumentNullException(nameof(monitor));
|
|
|
|
InputSystem.s_Manager.AddStateChangeMonitorTimeout(control, monitor, time, monitorIndex, timerIndex);
|
|
}
|
|
|
|
public static void RemoveChangeMonitorTimeout(IInputStateChangeMonitor monitor, long monitorIndex = -1, int timerIndex = -1)
|
|
{
|
|
if (monitor == null)
|
|
throw new ArgumentNullException(nameof(monitor));
|
|
|
|
InputSystem.s_Manager.RemoveStateChangeMonitorTimeout(monitor, monitorIndex, timerIndex);
|
|
}
|
|
|
|
private class StateChangeMonitorDelegate : IInputStateChangeMonitor
|
|
{
|
|
public NotifyControlValueChangeAction valueChangeCallback;
|
|
public NotifyTimerExpiredAction timerExpiredCallback;
|
|
|
|
public void NotifyControlStateChanged(InputControl control, double time, InputEventPtr eventPtr, long monitorIndex)
|
|
{
|
|
valueChangeCallback(control, time, eventPtr, monitorIndex);
|
|
}
|
|
|
|
public void NotifyTimerExpired(InputControl control, double time, long monitorIndex, int timerIndex)
|
|
{
|
|
timerExpiredCallback?.Invoke(control, time, monitorIndex, timerIndex);
|
|
}
|
|
}
|
|
}
|
|
}
|