File size: 15,161 Bytes
18a519f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 | 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);
}
}
}
}
|