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); } } } }