123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472 |
- using System;
- using UnityEngine.Events;
- using UnityEngine.InputSystem.Layouts;
- using UnityEngine.InputSystem.LowLevel;
- ////TODO: allow multiple device paths
- ////TODO: streaming support
- ////REVIEW: consider this for inclusion directly in the input system
- namespace UnityEngine.InputSystem
- {
- /// <summary>
- /// A wrapper component around <see cref="InputEventTrace"/> that provides an easy interface for recording input
- /// from a GameObject.
- /// </summary>
- /// <remarks>
- /// This component comes with a custom inspector that provides an easy recording and playback interface and also
- /// gives feedback about what has been recorded in the trace. The interface also allows saving and loading event
- /// traces.
- ///
- /// Capturing can either be constrained by a <see cref="devicePath"/> or capture all input occuring in the system.
- ///
- /// Replay by default will happen frame by frame (see <see cref="InputEventTrace.ReplayController.PlayAllFramesOneByOne"/>).
- /// If frame markers are disabled (see <see cref="recordFrames"/>), all events are queued right away in the first
- /// frame and replay completes immediately.
- ///
- /// Other than frame-by-frame, replay can be made to happen in a way that tries to simulate the original input
- /// timing. To do so, enable <see cref="simulateOriginalTimingOnReplay"/>. This will make use of <see
- /// cref="InputEventTrace.ReplayController.PlayAllEventsAccordingToTimestamps"/>
- /// </remarks>
- public class InputRecorder : MonoBehaviour
- {
- /// <summary>
- /// Whether a capture is currently in progress.
- /// </summary>
- /// <value>True if a capture is in progress.</value>
- public bool captureIsRunning => m_EventTrace != null && m_EventTrace.enabled;
- /// <summary>
- /// Whether a replay is currently being run by the component.
- /// </summary>
- /// <value>True if replay is running.</value>
- /// <seealso cref="replay"/>
- /// <seealso cref="StartReplay"/>
- /// <seealso cref="StopReplay"/>
- public bool replayIsRunning => m_ReplayController != null && !m_ReplayController.finished;
- /// <summary>
- /// If true, input recording is started immediately when the component is enabled. Disabled by default.
- /// Call <see cref="StartCapture"/> to manually start capturing.
- /// </summary>
- /// <value>True if component will start recording automatically in <see cref="OnEnable"/>.</value>
- /// <seealso cref="StartCapture"/>
- public bool startRecordingWhenEnabled
- {
- get => m_StartRecordingWhenEnabled;
- set
- {
- m_StartRecordingWhenEnabled = value;
- if (value && enabled && !captureIsRunning)
- StartCapture();
- }
- }
- /// <summary>
- /// Total number of events captured.
- /// </summary>
- /// <value>Number of captured events.</value>
- public long eventCount => m_EventTrace?.eventCount ?? 0;
- /// <summary>
- /// Total size of captured events.
- /// </summary>
- /// <value>Size of captured events in bytes.</value>
- public long totalEventSizeInBytes => m_EventTrace?.totalEventSizeInBytes ?? 0;
- /// <summary>
- /// Total size of capture memory currently allocated.
- /// </summary>
- /// <value>Size of memory allocated for capture.</value>
- public long allocatedSizeInBytes => m_EventTrace?.allocatedSizeInBytes ?? 0;
- /// <summary>
- /// Whether to record frame marker events when capturing input. Enabled by default.
- /// </summary>
- /// <value>True if frame marker events will be recorded.</value>
- /// <seealso cref="InputEventTrace.recordFrameMarkers"/>
- public bool recordFrames
- {
- get => m_RecordFrames;
- set
- {
- if (m_RecordFrames == value)
- return;
- m_RecordFrames = value;
- if (m_EventTrace != null)
- m_EventTrace.recordFrameMarkers = m_RecordFrames;
- }
- }
- /// <summary>
- /// Whether to record only <see cref="StateEvent"/>s and <see cref="DeltaStateEvent"/>s. Disabled by
- /// default.
- /// </summary>
- /// <value>True if anything but state events should be ignored.</value>
- public bool recordStateEventsOnly
- {
- get => m_RecordStateEventsOnly;
- set => m_RecordStateEventsOnly = value;
- }
- /// <summary>
- /// Path that constrains the devices to record from.
- /// </summary>
- /// <value>Input control path to match devices or null/empty.</value>
- /// <remarks>
- /// By default, this is not set. Meaning that input will be recorded from all devices. By setting this property
- /// to a path, only events for devices that match the given path (as dictated by <see cref="InputControlPath.Matches"/>)
- /// will be recorded from.
- ///
- /// By setting this property to the exact path of a device at runtime, recording can be restricted to just that
- /// device.
- /// </remarks>
- /// <seealso cref="InputControlPath"/>
- /// <seealso cref="InputControlPath.Matches"/>
- public string devicePath
- {
- get => m_DevicePath;
- set => m_DevicePath = value;
- }
- public string recordButtonPath
- {
- get => m_RecordButtonPath;
- set
- {
- m_RecordButtonPath = value;
- HookOnInputEvent();
- }
- }
- public string playButtonPath
- {
- get => m_PlayButtonPath;
- set
- {
- m_PlayButtonPath = value;
- HookOnInputEvent();
- }
- }
- /// <summary>
- /// The underlying event trace that contains the captured input events.
- /// </summary>
- /// <value>Underlying event trace.</value>
- /// <remarks>
- /// This will be null if no capture is currently associated with the recorder.
- /// </remarks>
- public InputEventTrace capture => m_EventTrace;
- /// <summary>
- /// The replay controller for when a replay is running.
- /// </summary>
- /// <value>Replay controller for the event trace while replay is running.</value>
- /// <seealso cref="replayIsRunning"/>
- /// <seealso cref="StartReplay"/>
- public InputEventTrace.ReplayController replay => m_ReplayController;
- public int replayPosition
- {
- get
- {
- if (m_ReplayController != null)
- return m_ReplayController.position;
- return 0;
- }
- ////TODO: allow setting replay position
- }
- /// <summary>
- /// Whether a replay should create new devices or replay recorded events as is. Disabled by default.
- /// </summary>
- /// <value>True if replay should temporary create new devices.</value>
- /// <seealso cref="InputEventTrace.ReplayController.WithAllDevicesMappedToNewInstances"/>
- public bool replayOnNewDevices
- {
- get => m_ReplayOnNewDevices;
- set => m_ReplayOnNewDevices = value;
- }
- /// <summary>
- /// Whether to attempt to re-create the original event timing when replaying events. Disabled by default.
- /// </summary>
- /// <value>If true, events are queued based on their timestamp rather than based on their recorded frames (if any).</value>
- /// <seealso cref="InputEventTrace.ReplayController.PlayAllEventsAccordingToTimestamps"/>
- public bool simulateOriginalTimingOnReplay
- {
- get => m_SimulateOriginalTimingOnReplay;
- set => m_SimulateOriginalTimingOnReplay = value;
- }
- public ChangeEvent changeEvent
- {
- get
- {
- if (m_ChangeEvent == null)
- m_ChangeEvent = new ChangeEvent();
- return m_ChangeEvent;
- }
- }
- public void StartCapture()
- {
- if (m_EventTrace != null && m_EventTrace.enabled)
- return;
- CreateEventTrace();
- m_EventTrace.Enable();
- m_ChangeEvent?.Invoke(Change.CaptureStarted);
- }
- public void StopCapture()
- {
- if (m_EventTrace != null && m_EventTrace.enabled)
- {
- m_EventTrace.Disable();
- m_ChangeEvent?.Invoke(Change.CaptureStopped);
- }
- }
- public void StartReplay()
- {
- if (m_EventTrace == null)
- return;
- if (replayIsRunning && replay.paused)
- {
- replay.paused = false;
- return;
- }
- StopCapture();
- // Configure replay controller.
- m_ReplayController = m_EventTrace.Replay()
- .OnFinished(StopReplay)
- .OnEvent(_ => m_ChangeEvent?.Invoke(Change.EventPlayed));
- if (m_ReplayOnNewDevices)
- m_ReplayController.WithAllDevicesMappedToNewInstances();
- // Start replay.
- if (m_SimulateOriginalTimingOnReplay)
- m_ReplayController.PlayAllEventsAccordingToTimestamps();
- else
- m_ReplayController.PlayAllFramesOneByOne();
- m_ChangeEvent?.Invoke(Change.ReplayStarted);
- }
- public void StopReplay()
- {
- if (m_ReplayController != null)
- {
- m_ReplayController.Dispose();
- m_ReplayController = null;
- m_ChangeEvent?.Invoke(Change.ReplayStopped);
- }
- }
- public void PauseReplay()
- {
- if (m_ReplayController != null)
- m_ReplayController.paused = true;
- }
- public void ClearCapture()
- {
- m_EventTrace?.Clear();
- }
- public void LoadCaptureFromFile(string fileName)
- {
- if (string.IsNullOrEmpty(fileName))
- throw new ArgumentNullException(nameof(fileName));
- CreateEventTrace();
- m_EventTrace.ReadFrom(fileName);
- }
- public void SaveCaptureToFile(string fileName)
- {
- if (string.IsNullOrEmpty(fileName))
- throw new ArgumentNullException(nameof(fileName));
- m_EventTrace?.WriteTo(fileName);
- }
- protected void OnEnable()
- {
- // Hook InputSystem.onEvent before the event trace does.
- HookOnInputEvent();
- if (m_StartRecordingWhenEnabled)
- StartCapture();
- }
- protected void OnDisable()
- {
- StopCapture();
- StopReplay();
- UnhookOnInputEvent();
- }
- protected void OnDestroy()
- {
- m_ReplayController?.Dispose();
- m_ReplayController = null;
- m_EventTrace?.Dispose();
- m_EventTrace = null;
- }
- private bool OnFilterInputEvent(InputEventPtr eventPtr, InputDevice device)
- {
- // Filter out non-state events, if enabled.
- if (m_RecordStateEventsOnly && !eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>())
- return false;
- // Match device path, if set.
- if (string.IsNullOrEmpty(m_DevicePath) || device == null)
- return true;
- return InputControlPath.MatchesPrefix(m_DevicePath, device);
- }
- private void OnEventRecorded(InputEventPtr eventPtr)
- {
- m_ChangeEvent?.Invoke(Change.EventCaptured);
- }
- private void OnInputEvent(InputEventPtr eventPtr, InputDevice device)
- {
- if (!eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>())
- return;
- if (!string.IsNullOrEmpty(m_PlayButtonPath))
- {
- var playControl = InputControlPath.TryFindControl(device, m_PlayButtonPath) as InputControl<float>;
- if (playControl != null && playControl.ReadValueFromEvent(eventPtr) >= InputSystem.settings.defaultButtonPressPoint)
- {
- if (replayIsRunning)
- StopReplay();
- else
- StartReplay();
- eventPtr.handled = true;
- }
- }
- if (!string.IsNullOrEmpty(m_RecordButtonPath))
- {
- var recordControl = InputControlPath.TryFindControl(device, m_RecordButtonPath) as InputControl<float>;
- if (recordControl != null && recordControl.ReadValueFromEvent(eventPtr) >= InputSystem.settings.defaultButtonPressPoint)
- {
- if (captureIsRunning)
- StopCapture();
- else
- StartCapture();
- eventPtr.handled = true;
- }
- }
- }
- #if UNITY_EDITOR
- protected void OnValidate()
- {
- if (m_EventTrace != null)
- m_EventTrace.recordFrameMarkers = m_RecordFrames;
- }
- #endif
- [SerializeField] private bool m_StartRecordingWhenEnabled = false;
- [Tooltip("If enabled, additional events will be recorded that demarcate frame boundaries. When replaying, this allows "
- + "spacing out input events across frames corresponding to the original distribution across frames when input was "
- + "recorded. If this is turned off, all input events will be queued in one block when replaying the trace.")]
- [SerializeField] private bool m_RecordFrames = true;
- [Tooltip("If enabled, new devices will be created for captured events when replaying them. If disabled (default), "
- + "events will be queued as is and thus keep their original device ID.")]
- [SerializeField] private bool m_ReplayOnNewDevices;
- [Tooltip("If enabled, the system will try to simulate the original event timing on replay. This differs from replaying frame "
- + "by frame in that replay will try to compensate for differences in frame timings and redistribute events to frames that "
- + "more closely match the original timing. Note that this is not perfect and will not necessarily create a 1:1 match.")]
- [SerializeField] private bool m_SimulateOriginalTimingOnReplay;
- [Tooltip("If enabled, only StateEvents and DeltaStateEvents will be captured.")]
- [SerializeField] private bool m_RecordStateEventsOnly;
- [SerializeField] private int m_CaptureMemoryDefaultSize = 2 * 1024 * 1024;
- [SerializeField] private int m_CaptureMemoryMaxSize = 10 * 1024 * 1024;
- [SerializeField]
- [InputControl(layout = "InputDevice")]
- private string m_DevicePath;
- [SerializeField]
- [InputControl(layout = "Button")]
- private string m_RecordButtonPath;
- [SerializeField]
- [InputControl(layout = "Button")]
- private string m_PlayButtonPath;
- [SerializeField] private ChangeEvent m_ChangeEvent;
- private Action<InputEventPtr, InputDevice> m_OnInputEventDelegate;
- private InputEventTrace m_EventTrace;
- private InputEventTrace.ReplayController m_ReplayController;
- private void CreateEventTrace()
- {
- ////FIXME: remaining configuration should come through, too, if changed after the fact
- if (m_EventTrace == null || m_EventTrace.maxSizeInBytes == 0)
- {
- m_EventTrace?.Dispose();
- m_EventTrace = new InputEventTrace(m_CaptureMemoryDefaultSize, growBuffer: true, maxBufferSizeInBytes: m_CaptureMemoryMaxSize);
- }
- m_EventTrace.recordFrameMarkers = m_RecordFrames;
- m_EventTrace.onFilterEvent += OnFilterInputEvent;
- m_EventTrace.onEvent += OnEventRecorded;
- }
- private void HookOnInputEvent()
- {
- if (string.IsNullOrEmpty(m_PlayButtonPath) && string.IsNullOrEmpty(m_RecordButtonPath))
- {
- UnhookOnInputEvent();
- return;
- }
- if (m_OnInputEventDelegate == null)
- m_OnInputEventDelegate = OnInputEvent;
- InputSystem.onEvent += m_OnInputEventDelegate;
- }
- private void UnhookOnInputEvent()
- {
- if (m_OnInputEventDelegate != null)
- InputSystem.onEvent -= m_OnInputEventDelegate;
- }
- public enum Change
- {
- None,
- EventCaptured,
- EventPlayed,
- CaptureStarted,
- CaptureStopped,
- ReplayStarted,
- ReplayStopped,
- }
- [Serializable]
- public class ChangeEvent : UnityEvent<Change>
- {
- }
- }
- }
|