InputRecorder.cs 17 KB

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