Files
MMORPG/Packages/com.unity.inputsystem/Samples~/InputRecorder/InputRecorder.cs
cooney ce83f21c93 UI 자동화를 위해 바인딩 기능 구현
- 유니티 에셋 인증 오류로 meta 재생성
2026-01-25 01:31:34 +09:00

473 lines
17 KiB
C#

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>
{
}
}
}