slimecing

a fighting game featuring slimes and swords
Log | Files | Refs | README

InputRecorder.cs (17203B)


      1 using System;
      2 using UnityEngine.Events;
      3 using UnityEngine.InputSystem.Layouts;
      4 using 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 
     12 namespace 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 }