A game about forced loneliness, made by TACStudios
1using System;
2using UnityEngine.Events;
3using UnityEngine.InputSystem.Layouts;
4using UnityEngine.InputSystem.LowLevel;
5
6////TODO: allow multiple device paths
7
8////TODO: streaming support
9
10////REVIEW: consider this for inclusion directly in the input system
11
12namespace UnityEngine.InputSystem
13{
14 /// <summary>
15 /// A wrapper component around <see cref="InputEventTrace"/> that provides an easy interface for recording input
16 /// from a GameObject.
17 /// </summary>
18 /// <remarks>
19 /// This component comes with a custom inspector that provides an easy recording and playback interface and also
20 /// gives feedback about what has been recorded in the trace. The interface also allows saving and loading event
21 /// traces.
22 ///
23 /// Capturing can either be constrained by a <see cref="devicePath"/> or capture all input occuring in the system.
24 ///
25 /// Replay by default will happen frame by frame (see <see cref="InputEventTrace.ReplayController.PlayAllFramesOneByOne"/>).
26 /// If frame markers are disabled (see <see cref="recordFrames"/>), all events are queued right away in the first
27 /// frame and replay completes immediately.
28 ///
29 /// Other than frame-by-frame, replay can be made to happen in a way that tries to simulate the original input
30 /// timing. To do so, enable <see cref="simulateOriginalTimingOnReplay"/>. This will make use of <see
31 /// cref="InputEventTrace.ReplayController.PlayAllEventsAccordingToTimestamps"/>
32 /// </remarks>
33 public class InputRecorder : MonoBehaviour
34 {
35 /// <summary>
36 /// Whether a capture is currently in progress.
37 /// </summary>
38 /// <value>True if a capture is in progress.</value>
39 public bool captureIsRunning => m_EventTrace != null && m_EventTrace.enabled;
40
41 /// <summary>
42 /// Whether a replay is currently being run by the component.
43 /// </summary>
44 /// <value>True if replay is running.</value>
45 /// <seealso cref="replay"/>
46 /// <seealso cref="StartReplay"/>
47 /// <seealso cref="StopReplay"/>
48 public bool replayIsRunning => m_ReplayController != null && !m_ReplayController.finished;
49
50 /// <summary>
51 /// If true, input recording is started immediately when the component is enabled. Disabled by default.
52 /// Call <see cref="StartCapture"/> to manually start capturing.
53 /// </summary>
54 /// <value>True if component will start recording automatically in <see cref="OnEnable"/>.</value>
55 /// <seealso cref="StartCapture"/>
56 public bool startRecordingWhenEnabled
57 {
58 get => m_StartRecordingWhenEnabled;
59 set
60 {
61 m_StartRecordingWhenEnabled = value;
62 if (value && enabled && !captureIsRunning)
63 StartCapture();
64 }
65 }
66
67 /// <summary>
68 /// Total number of events captured.
69 /// </summary>
70 /// <value>Number of captured events.</value>
71 public long eventCount => m_EventTrace?.eventCount ?? 0;
72
73 /// <summary>
74 /// Total size of captured events.
75 /// </summary>
76 /// <value>Size of captured events in bytes.</value>
77 public long totalEventSizeInBytes => m_EventTrace?.totalEventSizeInBytes ?? 0;
78
79 /// <summary>
80 /// Total size of capture memory currently allocated.
81 /// </summary>
82 /// <value>Size of memory allocated for capture.</value>
83 public long allocatedSizeInBytes => m_EventTrace?.allocatedSizeInBytes ?? 0;
84
85 /// <summary>
86 /// Whether to record frame marker events when capturing input. Enabled by default.
87 /// </summary>
88 /// <value>True if frame marker events will be recorded.</value>
89 /// <seealso cref="InputEventTrace.recordFrameMarkers"/>
90 public bool recordFrames
91 {
92 get => m_RecordFrames;
93 set
94 {
95 if (m_RecordFrames == value)
96 return;
97 m_RecordFrames = value;
98 if (m_EventTrace != null)
99 m_EventTrace.recordFrameMarkers = m_RecordFrames;
100 }
101 }
102
103 /// <summary>
104 /// Whether to record only <see cref="StateEvent"/>s and <see cref="DeltaStateEvent"/>s. Disabled by
105 /// default.
106 /// </summary>
107 /// <value>True if anything but state events should be ignored.</value>
108 public bool recordStateEventsOnly
109 {
110 get => m_RecordStateEventsOnly;
111 set => m_RecordStateEventsOnly = value;
112 }
113
114 /// <summary>
115 /// Path that constrains the devices to record from.
116 /// </summary>
117 /// <value>Input control path to match devices or null/empty.</value>
118 /// <remarks>
119 /// By default, this is not set. Meaning that input will be recorded from all devices. By setting this property
120 /// to a path, only events for devices that match the given path (as dictated by <see cref="InputControlPath.Matches"/>)
121 /// will be recorded from.
122 ///
123 /// By setting this property to the exact path of a device at runtime, recording can be restricted to just that
124 /// device.
125 /// </remarks>
126 /// <seealso cref="InputControlPath"/>
127 /// <seealso cref="InputControlPath.Matches"/>
128 public string devicePath
129 {
130 get => m_DevicePath;
131 set => m_DevicePath = value;
132 }
133
134 public string recordButtonPath
135 {
136 get => m_RecordButtonPath;
137 set
138 {
139 m_RecordButtonPath = value;
140 HookOnInputEvent();
141 }
142 }
143
144 public string playButtonPath
145 {
146 get => m_PlayButtonPath;
147 set
148 {
149 m_PlayButtonPath = value;
150 HookOnInputEvent();
151 }
152 }
153
154 /// <summary>
155 /// The underlying event trace that contains the captured input events.
156 /// </summary>
157 /// <value>Underlying event trace.</value>
158 /// <remarks>
159 /// This will be null if no capture is currently associated with the recorder.
160 /// </remarks>
161 public InputEventTrace capture => m_EventTrace;
162
163 /// <summary>
164 /// The replay controller for when a replay is running.
165 /// </summary>
166 /// <value>Replay controller for the event trace while replay is running.</value>
167 /// <seealso cref="replayIsRunning"/>
168 /// <seealso cref="StartReplay"/>
169 public InputEventTrace.ReplayController replay => m_ReplayController;
170
171 public int replayPosition
172 {
173 get
174 {
175 if (m_ReplayController != null)
176 return m_ReplayController.position;
177 return 0;
178 }
179 ////TODO: allow setting replay position
180 }
181
182 /// <summary>
183 /// Whether a replay should create new devices or replay recorded events as is. Disabled by default.
184 /// </summary>
185 /// <value>True if replay should temporary create new devices.</value>
186 /// <seealso cref="InputEventTrace.ReplayController.WithAllDevicesMappedToNewInstances"/>
187 public bool replayOnNewDevices
188 {
189 get => m_ReplayOnNewDevices;
190 set => m_ReplayOnNewDevices = value;
191 }
192
193 /// <summary>
194 /// Whether to attempt to re-create the original event timing when replaying events. Disabled by default.
195 /// </summary>
196 /// <value>If true, events are queued based on their timestamp rather than based on their recorded frames (if any).</value>
197 /// <seealso cref="InputEventTrace.ReplayController.PlayAllEventsAccordingToTimestamps"/>
198 public bool simulateOriginalTimingOnReplay
199 {
200 get => m_SimulateOriginalTimingOnReplay;
201 set => m_SimulateOriginalTimingOnReplay = value;
202 }
203
204 public ChangeEvent changeEvent
205 {
206 get
207 {
208 if (m_ChangeEvent == null)
209 m_ChangeEvent = new ChangeEvent();
210 return m_ChangeEvent;
211 }
212 }
213
214 public void StartCapture()
215 {
216 if (m_EventTrace != null && m_EventTrace.enabled)
217 return;
218
219 CreateEventTrace();
220 m_EventTrace.Enable();
221 m_ChangeEvent?.Invoke(Change.CaptureStarted);
222 }
223
224 public void StopCapture()
225 {
226 if (m_EventTrace != null && m_EventTrace.enabled)
227 {
228 m_EventTrace.Disable();
229 m_ChangeEvent?.Invoke(Change.CaptureStopped);
230 }
231 }
232
233 public void StartReplay()
234 {
235 if (m_EventTrace == null)
236 return;
237
238 if (replayIsRunning && replay.paused)
239 {
240 replay.paused = false;
241 return;
242 }
243
244 StopCapture();
245
246 // Configure replay controller.
247 m_ReplayController = m_EventTrace.Replay()
248 .OnFinished(StopReplay)
249 .OnEvent(_ => m_ChangeEvent?.Invoke(Change.EventPlayed));
250 if (m_ReplayOnNewDevices)
251 m_ReplayController.WithAllDevicesMappedToNewInstances();
252
253 // Start replay.
254 if (m_SimulateOriginalTimingOnReplay)
255 m_ReplayController.PlayAllEventsAccordingToTimestamps();
256 else
257 m_ReplayController.PlayAllFramesOneByOne();
258
259 m_ChangeEvent?.Invoke(Change.ReplayStarted);
260 }
261
262 public void StopReplay()
263 {
264 if (m_ReplayController != null)
265 {
266 m_ReplayController.Dispose();
267 m_ReplayController = null;
268 m_ChangeEvent?.Invoke(Change.ReplayStopped);
269 }
270 }
271
272 public void PauseReplay()
273 {
274 if (m_ReplayController != null)
275 m_ReplayController.paused = true;
276 }
277
278 public void ClearCapture()
279 {
280 m_EventTrace?.Clear();
281 }
282
283 public void LoadCaptureFromFile(string fileName)
284 {
285 if (string.IsNullOrEmpty(fileName))
286 throw new ArgumentNullException(nameof(fileName));
287
288 CreateEventTrace();
289 m_EventTrace.ReadFrom(fileName);
290 }
291
292 public void SaveCaptureToFile(string fileName)
293 {
294 if (string.IsNullOrEmpty(fileName))
295 throw new ArgumentNullException(nameof(fileName));
296 m_EventTrace?.WriteTo(fileName);
297 }
298
299 protected void OnEnable()
300 {
301 // Hook InputSystem.onEvent before the event trace does.
302 HookOnInputEvent();
303
304 if (m_StartRecordingWhenEnabled)
305 StartCapture();
306 }
307
308 protected void OnDisable()
309 {
310 StopCapture();
311 StopReplay();
312 UnhookOnInputEvent();
313 }
314
315 protected void OnDestroy()
316 {
317 m_ReplayController?.Dispose();
318 m_ReplayController = null;
319 m_EventTrace?.Dispose();
320 m_EventTrace = null;
321 }
322
323 private bool OnFilterInputEvent(InputEventPtr eventPtr, InputDevice device)
324 {
325 // Filter out non-state events, if enabled.
326 if (m_RecordStateEventsOnly && !eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>())
327 return false;
328
329 // Match device path, if set.
330 if (string.IsNullOrEmpty(m_DevicePath) || device == null)
331 return true;
332 return InputControlPath.MatchesPrefix(m_DevicePath, device);
333 }
334
335 private void OnEventRecorded(InputEventPtr eventPtr)
336 {
337 m_ChangeEvent?.Invoke(Change.EventCaptured);
338 }
339
340 private void OnInputEvent(InputEventPtr eventPtr, InputDevice device)
341 {
342 if (!eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>())
343 return;
344
345 if (!string.IsNullOrEmpty(m_PlayButtonPath))
346 {
347 var playControl = InputControlPath.TryFindControl(device, m_PlayButtonPath) as InputControl<float>;
348 if (playControl != null && playControl.ReadValueFromEvent(eventPtr) >= InputSystem.settings.defaultButtonPressPoint)
349 {
350 if (replayIsRunning)
351 StopReplay();
352 else
353 StartReplay();
354
355 eventPtr.handled = true;
356 }
357 }
358
359 if (!string.IsNullOrEmpty(m_RecordButtonPath))
360 {
361 var recordControl = InputControlPath.TryFindControl(device, m_RecordButtonPath) as InputControl<float>;
362 if (recordControl != null && recordControl.ReadValueFromEvent(eventPtr) >= InputSystem.settings.defaultButtonPressPoint)
363 {
364 if (captureIsRunning)
365 StopCapture();
366 else
367 StartCapture();
368
369 eventPtr.handled = true;
370 }
371 }
372 }
373
374 #if UNITY_EDITOR
375 protected void OnValidate()
376 {
377 if (m_EventTrace != null)
378 m_EventTrace.recordFrameMarkers = m_RecordFrames;
379 }
380
381 #endif
382
383 [SerializeField] private bool m_StartRecordingWhenEnabled = false;
384
385 [Tooltip("If enabled, additional events will be recorded that demarcate frame boundaries. When replaying, this allows "
386 + "spacing out input events across frames corresponding to the original distribution across frames when input was "
387 + "recorded. If this is turned off, all input events will be queued in one block when replaying the trace.")]
388 [SerializeField] private bool m_RecordFrames = true;
389
390 [Tooltip("If enabled, new devices will be created for captured events when replaying them. If disabled (default), "
391 + "events will be queued as is and thus keep their original device ID.")]
392 [SerializeField] private bool m_ReplayOnNewDevices;
393
394 [Tooltip("If enabled, the system will try to simulate the original event timing on replay. This differs from replaying frame "
395 + "by frame in that replay will try to compensate for differences in frame timings and redistribute events to frames that "
396 + "more closely match the original timing. Note that this is not perfect and will not necessarily create a 1:1 match.")]
397 [SerializeField] private bool m_SimulateOriginalTimingOnReplay;
398
399 [Tooltip("If enabled, only StateEvents and DeltaStateEvents will be captured.")]
400 [SerializeField] private bool m_RecordStateEventsOnly;
401
402 [SerializeField] private int m_CaptureMemoryDefaultSize = 2 * 1024 * 1024;
403 [SerializeField] private int m_CaptureMemoryMaxSize = 10 * 1024 * 1024;
404
405 [SerializeField]
406 [InputControl(layout = "InputDevice")]
407 private string m_DevicePath;
408
409 [SerializeField]
410 [InputControl(layout = "Button")]
411 private string m_RecordButtonPath;
412
413 [SerializeField]
414 [InputControl(layout = "Button")]
415 private string m_PlayButtonPath;
416
417 [SerializeField] private ChangeEvent m_ChangeEvent;
418
419 private Action<InputEventPtr, InputDevice> m_OnInputEventDelegate;
420 private InputEventTrace m_EventTrace;
421 private InputEventTrace.ReplayController m_ReplayController;
422
423 private void CreateEventTrace()
424 {
425 ////FIXME: remaining configuration should come through, too, if changed after the fact
426 if (m_EventTrace == null || m_EventTrace.maxSizeInBytes == 0)
427 {
428 m_EventTrace?.Dispose();
429 m_EventTrace = new InputEventTrace(m_CaptureMemoryDefaultSize, growBuffer: true, maxBufferSizeInBytes: m_CaptureMemoryMaxSize);
430 }
431
432 m_EventTrace.recordFrameMarkers = m_RecordFrames;
433 m_EventTrace.onFilterEvent += OnFilterInputEvent;
434 m_EventTrace.onEvent += OnEventRecorded;
435 }
436
437 private void HookOnInputEvent()
438 {
439 if (string.IsNullOrEmpty(m_PlayButtonPath) && string.IsNullOrEmpty(m_RecordButtonPath))
440 {
441 UnhookOnInputEvent();
442 return;
443 }
444
445 if (m_OnInputEventDelegate == null)
446 m_OnInputEventDelegate = OnInputEvent;
447 InputSystem.onEvent += m_OnInputEventDelegate;
448 }
449
450 private void UnhookOnInputEvent()
451 {
452 if (m_OnInputEventDelegate != null)
453 InputSystem.onEvent -= m_OnInputEventDelegate;
454 }
455
456 public enum Change
457 {
458 None,
459 EventCaptured,
460 EventPlayed,
461 CaptureStarted,
462 CaptureStopped,
463 ReplayStarted,
464 ReplayStopped,
465 }
466
467 [Serializable]
468 public class ChangeEvent : UnityEvent<Change>
469 {
470 }
471 }
472}