using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using UnityEngine.InputSystem; using UnityEngine.InputSystem.LowLevel; using UnityEngine.InputSystem.Utilities; ////REVIEW: should this enumerate *backwards* in time rather than *forwards*? ////TODO: allow correlating history to frames/updates ////TODO: add ability to grow such that you can set it to e.g. record up to 4 seconds of history and it will automatically keep the buffer size bounded ////REVIEW: should we align the extra memory on a 4 byte boundary? namespace UnityEngine.InputSystem.LowLevel { /// /// Record a history of state changes applied to one or more controls. /// /// /// This class makes it easy to track input values over time. It will automatically retain input state up to a given /// maximum history depth (). When the history is full, it will start overwriting the oldest /// entry each time a new history record is received. /// /// The class listens to changes on the given controls by adding change monitors () /// to each control. /// /// /// /// // Track all stick controls in the system. /// var history = new InputStateHistory<Vector2>("*/<Stick>"); /// foreach (var control in history.controls) /// Debug.Log("Capturing input on " + control); /// /// // Start capturing. /// history.StartRecording(); /// /// // Perform a couple artificial value changes. /// Gamepad.current.leftStick.QueueValueChange(new Vector2(0.123f, 0.234f)); /// Gamepad.current.leftStick.QueueValueChange(new Vector2(0.234f, 0.345f)); /// Gamepad.current.leftStick.QueueValueChange(new Vector2(0.345f, 0.456f)); /// InputSystem.Update(); /// /// // Every value change will be visible in the history. /// foreach (var record in history) /// Debug.Log($"{record.control} changed value to {record.ReadValue()}"); /// /// // Histories allocate unmanaged memory and must be disposed of in order to not leak. /// history.Dispose(); /// /// /// public class InputStateHistory : IDisposable, IEnumerable, IInputStateChangeMonitor { private const int kDefaultHistorySize = 128; /// /// Total number of state records currently captured in the history. /// /// /// Number of records in the collection. /// /// This will always be at most . /// To record a change use . /// public int Count => m_RecordCount; /// /// Current version stamp. Every time a record is stored in the history, /// this is incremented by one. /// /// /// Version stamp that indicates the number of mutations. /// To record a change use . /// public uint version => m_CurrentVersion; /// /// Maximum number of records that can be recorded in the history. /// /// is negative. /// /// Upper limit on number of records. /// A fixed size memory block of unmanaged memory will be allocated to store history /// records. /// When the history is full, it will start overwriting the oldest /// entry each time a new history record is received. /// public int historyDepth { get => m_HistoryDepth; set { if (value < 0) throw new ArgumentException("History depth cannot be negative", nameof(value)); if (m_RecordBuffer.IsCreated) throw new NotImplementedException(); m_HistoryDepth = value; } } /// /// Size of additional data storage to allocate per record. /// /// is negative. /// /// Additional custom data can be stored per record up to the size of this value. /// To retrieve a pointer to this memory use /// Used by /// public int extraMemoryPerRecord { get => m_ExtraMemoryPerRecord; set { if (value < 0) throw new ArgumentException("Memory size cannot be negative", nameof(value)); if (m_RecordBuffer.IsCreated) throw new NotImplementedException(); m_ExtraMemoryPerRecord = value; } } /// /// Specify which player loop positions the state history will be monitored for. /// /// When an invalid mask is provided (e.g. ). /// /// The state history will only be monitored for the specified player loop positions. /// is excluded from this list /// public InputUpdateType updateMask { get => m_UpdateMask ?? InputSystem.s_Manager.updateMask & ~InputUpdateType.Editor; set { if (value == InputUpdateType.None) throw new ArgumentException("'InputUpdateType.None' is not a valid update mask", nameof(value)); m_UpdateMask = value; } } /// /// List of input controls the state history will be recording for. /// /// /// The list of input controls the state history will be recording for is specified on construction of the /// public ReadOnlyArray controls => new ReadOnlyArray(m_Controls, 0, m_ControlCount); /// /// Returns an entry in the state history at the given index. /// /// Index into the array. /// /// Returns a entry from the state history at the given index. /// /// is less than 0 or greater than . public unsafe Record this[int index] { get { if (index < 0 || index >= m_RecordCount) throw new ArgumentOutOfRangeException( $"Index {index} is out of range for history with {m_RecordCount} entries", nameof(index)); var recordIndex = UserIndexToRecordIndex(index); return new Record(this, recordIndex, GetRecord(recordIndex)); } set { if (index < 0 || index >= m_RecordCount) throw new ArgumentOutOfRangeException( $"Index {index} is out of range for history with {m_RecordCount} entries", nameof(index)); var recordIndex = UserIndexToRecordIndex(index); new Record(this, recordIndex, GetRecord(recordIndex)).CopyFrom(value); } } /// /// Optional delegate to perform when a record is added to the history array. /// /// /// Can be used to fill in the extra memory with custom data using /// public Action onRecordAdded { get; set; } /// /// Optional delegate to decide whether the state change should be stored in the history. /// /// /// Can be used to filter out some events to focus on recording the ones you are most interested in. /// /// If the callback returns true, a record will be added to the history /// If the callback returns false, the event will be ignored and not recorded. /// public Func onShouldRecordStateChange { get; set; } /// /// Creates a new InputStateHistory class to record all control state changes. /// /// Maximum size of control state in the record entries. Controls with larger state will not be recorded. /// /// Creates a new InputStateHistory to record a history of control state changes. /// /// New controls are automatically added into the state history if their state is smaller than the threshold. /// public InputStateHistory(int maxStateSizeInBytes) { if (maxStateSizeInBytes <= 0) throw new ArgumentException("State size must be >= 0", nameof(maxStateSizeInBytes)); m_AddNewControls = true; m_StateSizeInBytes = maxStateSizeInBytes.AlignToMultipleOf(4); } /// /// Creates a new InputStateHistory class to record state changes for a specified control. /// /// Control path to identify which controls to monitor. /// /// Creates a new InputStateHistory to record a history of state changes for the specified controls. /// /// /// /// // Track all stick controls in the system. /// var history = new InputStateHistory("*/<Stick>"); /// /// public InputStateHistory(string path) { using (var controls = InputSystem.FindControls(path)) { m_Controls = controls.ToArray(); m_ControlCount = m_Controls.Length; } } /// /// Creates a new InputStateHistory class to record state changes for a specified control. /// /// Control to monitor. /// /// Creates a new InputStateHistory to record a history of state changes for the specified control. /// public InputStateHistory(InputControl control) { if (control == null) throw new ArgumentNullException(nameof(control)); m_Controls = new[] {control}; m_ControlCount = 1; } /// /// Creates a new InputStateHistory class to record state changes for a specified controls. /// /// Controls to monitor. /// /// Creates a new InputStateHistory to record a history of state changes for the specified controls. /// public InputStateHistory(IEnumerable controls) { if (controls != null) { m_Controls = controls.ToArray(); m_ControlCount = m_Controls.Length; } } /// /// InputStateHistory destructor. /// ~InputStateHistory() { Dispose(); } /// /// Clear the history record. /// /// /// Clear the history record. Resetting the list to empty. /// /// This won't clear controls that have been added on the fly. /// public void Clear() { m_HeadIndex = 0; m_RecordCount = 0; ++m_CurrentVersion; // NOTE: Won't clear controls that have been added on the fly. } /// /// Add a record to the input state history. /// /// Record to add. /// The newly added record from the history array (as a copy is made). /// /// Add a record to the input state history. /// Allocates an entry in the history array and returns this copy of the original data passed to the function. /// public unsafe Record AddRecord(Record record) { var recordPtr = AllocateRecord(out var index); var newRecord = new Record(this, index, recordPtr); newRecord.CopyFrom(record); return newRecord; } /// /// Start recording state history for the specified controls. /// /// /// Start recording state history for the controls specified in the constructor. /// /// /// /// using (var allTouchTaps = new InputStateHistory("<Touchscreen>/touch*/tap")) /// { /// allTouchTaps.StartRecording(); /// allTouchTaps.StopRecording(); /// } /// /// public void StartRecording() { // We defer allocation until we actually get values on a control. foreach (var control in controls) InputState.AddChangeMonitor(control, this); } /// /// Stop recording state history for the specified controls. /// /// /// Stop recording state history for the controls specified in the constructor. /// /// /// /// using (var allTouchTaps = new InputStateHistory("<Touchscreen>/touch*/tap")) /// { /// allTouchTaps.StartRecording(); /// allTouchTaps.StopRecording(); /// } /// /// public void StopRecording() { foreach (var control in controls) InputState.RemoveChangeMonitor(control, this); } /// /// Record a state change for a specific control. /// /// The control to record the state change for. /// The current event data to record. /// The newly added record. /// /// Record a state change for a specific control. /// Will call the delegate after adding the record. /// Note this does not call the delegate. /// public unsafe Record RecordStateChange(InputControl control, InputEventPtr eventPtr) { if (eventPtr.IsA()) throw new NotImplementedException(); if (!eventPtr.IsA()) throw new ArgumentException($"Event must be a state event but is '{eventPtr}' instead", nameof(eventPtr)); var statePtr = (byte*)StateEvent.From(eventPtr)->state - control.device.stateBlock.byteOffset; return RecordStateChange(control, statePtr, eventPtr.time); } /// /// Record a state change for a specific control. /// /// The control to record the state change for. /// The current state data to record. /// Time stamp to apply (overriding the event timestamp) /// The newly added record. /// /// Record a state change for a specific control. /// Will call the delegate after adding the record. /// Note this does not call the delegate. /// public unsafe Record RecordStateChange(InputControl control, void* statePtr, double time) { var controlIndex = ArrayHelpers.IndexOfReference(m_Controls, control, m_ControlCount); if (controlIndex == -1) { if (m_AddNewControls) { if (control.stateBlock.alignedSizeInBytes > m_StateSizeInBytes) throw new InvalidOperationException( $"Cannot add control '{control}' with state larger than {m_StateSizeInBytes} bytes"); controlIndex = ArrayHelpers.AppendWithCapacity(ref m_Controls, ref m_ControlCount, control); } else throw new ArgumentException($"Control '{control}' is not part of InputStateHistory", nameof(control)); } var recordPtr = AllocateRecord(out var index); recordPtr->time = time; recordPtr->version = ++m_CurrentVersion; var stateBufferPtr = recordPtr->statePtrWithoutControlIndex; if (m_ControlCount > 1 || m_AddNewControls) { // If there's multiple controls, write index of control to which the state change // pertains as an int before the state memory contents following it. recordPtr->controlIndex = controlIndex; stateBufferPtr = recordPtr->statePtrWithControlIndex; } var stateSize = control.stateBlock.alignedSizeInBytes; var stateOffset = control.stateBlock.byteOffset; UnsafeUtility.MemCpy(stateBufferPtr, (byte*)statePtr + stateOffset, stateSize); // Trigger callback. var record = new Record(this, index, recordPtr); onRecordAdded?.Invoke(record); return record; } /// /// Enumerate all state history records. /// /// An enumerator going over the state history records. /// /// Enumerate all state history records. /// /// public IEnumerator GetEnumerator() { return new Enumerator(this); } /// /// Enumerate all state history records. /// /// An enumerator going over the state history records. /// /// Enumerate all state history records. /// IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } /// /// Dispose of the state history records. /// /// /// Stops recording and cleans up the state history /// public void Dispose() { StopRecording(); Destroy(); GC.SuppressFinalize(this); } /// /// Destroy the state history records. /// /// /// Deletes the state history records. /// protected void Destroy() { if (m_RecordBuffer.IsCreated) { m_RecordBuffer.Dispose(); m_RecordBuffer = new NativeArray(); } } private void Allocate() { // Find max size of state. if (!m_AddNewControls) { m_StateSizeInBytes = 0; foreach (var control in controls) m_StateSizeInBytes = (int)Math.Max((uint)m_StateSizeInBytes, control.stateBlock.alignedSizeInBytes); } else { Debug.Assert(m_StateSizeInBytes > 0, "State size must be have initialized!"); } // Allocate historyDepth times state blocks of the given max size. For each one // add space for the RecordHeader header. // NOTE: If we only have a single control, we omit storing the integer control index. var totalSizeOfBuffer = bytesPerRecord * m_HistoryDepth; m_RecordBuffer = new NativeArray(totalSizeOfBuffer, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); } /// /// Remap a records internal index to an index from the start of the recording in the circular buffer. /// /// /// Remap a records internal index, which is relative to the start of the record buffer, /// to an index relative to the start of the recording in the circular buffer. /// /// Record index (from the start of the record array). /// An index relative to the start of the recording in the circular buffer. protected internal int RecordIndexToUserIndex(int index) { if (index < m_HeadIndex) return m_HistoryDepth - m_HeadIndex + index; return index - m_HeadIndex; } /// /// Remap an index from the start of the recording in the circular buffer to a records internal index. /// /// /// Remap an index relative to the start of the recording in the circular buffer, /// to a records internal index, which is relative to the start of the record buffer. /// /// An index relative to the start of the recording in the circular buffer. /// Record index (from the start of the record array). protected internal int UserIndexToRecordIndex(int index) { return (m_HeadIndex + index) % m_HistoryDepth; } /// /// Retrieve a record from the input state history. /// /// /// Retrieve a record from the input state history by Record index. /// /// Record index into the input state history records buffer. /// The record header for the specified index /// When the buffer is no longer valid as it has been disposed. /// If the index is out of range of the history depth. protected internal unsafe RecordHeader* GetRecord(int index) { if (!m_RecordBuffer.IsCreated) throw new InvalidOperationException("History buffer has been disposed"); if (index < 0 || index >= m_HistoryDepth) throw new ArgumentOutOfRangeException(nameof(index)); return GetRecordUnchecked(index); } /// /// Retrieve a record from the input state history, without any bounds check. /// /// /// Retrieve a record from the input state history by record index, without any bounds check /// /// Record index into the input state history records buffer. /// The record header for the specified index internal unsafe RecordHeader* GetRecordUnchecked(int index) { return (RecordHeader*)((byte*)m_RecordBuffer.GetUnsafePtr() + index * bytesPerRecord); } /// /// Allocate a new record in the input state history. /// /// /// Allocate a new record in the input state history. /// /// The index of the newly created record /// The header of the newly created record protected internal unsafe RecordHeader* AllocateRecord(out int index) { if (!m_RecordBuffer.IsCreated) Allocate(); index = (m_HeadIndex + m_RecordCount) % m_HistoryDepth; // If we're full, advance head to make room. if (m_RecordCount == m_HistoryDepth) m_HeadIndex = (m_HeadIndex + 1) % m_HistoryDepth; else { // We have a fixed max size given by the history depth and will start overwriting // older entries once we reached max size. ++m_RecordCount; } return (RecordHeader*)((byte*)m_RecordBuffer.GetUnsafePtr() + bytesPerRecord * index); } /// /// Returns value from the control in the specified record header. /// /// The record header to query. /// The type of the value being read /// The value from the record. /// When the record is no longer value or the specified type is not present. protected unsafe TValue ReadValue(RecordHeader* data) where TValue : struct { // Get control. If we only have a single one, the index isn't stored on the data. var haveSingleControl = m_ControlCount == 1 && !m_AddNewControls; var control = haveSingleControl ? controls[0] : controls[data->controlIndex]; if (!(control is InputControl controlOfType)) throw new InvalidOperationException( $"Cannot read value of type '{TypeHelpers.GetNiceTypeName(typeof(TValue))}' from control '{control}' with value type '{TypeHelpers.GetNiceTypeName(control.valueType)}'"); // Grab state memory. var statePtr = haveSingleControl ? data->statePtrWithoutControlIndex : data->statePtrWithControlIndex; statePtr -= control.stateBlock.byteOffset; return controlOfType.ReadValueFromState(statePtr); } /// /// Read the control's final, processed value from the given state and return the value as an object. /// /// The record header to query. /// The value of the control associated with the record header. /// /// This method allocates GC memory and should not be used during normal gameplay operation. /// /// When the specified value is not present. protected unsafe object ReadValueAsObject(RecordHeader* data) { // Get control. If we only have a single one, the index isn't stored on the data. var haveSingleControl = m_ControlCount == 1 && !m_AddNewControls; var control = haveSingleControl ? controls[0] : controls[data->controlIndex]; // Grab state memory. var statePtr = haveSingleControl ? data->statePtrWithoutControlIndex : data->statePtrWithControlIndex; statePtr -= control.stateBlock.byteOffset; return control.ReadValueFromStateAsObject(statePtr); } /// /// Delegate to list to control state change notifications. /// /// Control that is being monitored by the state change monitor and that had its state memory changed. /// Time on the timeline at which the control state change was received. /// If the state change was initiated by a state event (either a /// or ), this is the pointer to that event. Otherwise, it is pointer that is still /// , but refers a "dummy" event that is not a or . /// Index of the monitor as passed to /// /// Records a state change after checking the and the callback. /// unsafe void IInputStateChangeMonitor.NotifyControlStateChanged(InputControl control, double time, InputEventPtr eventPtr, long monitorIndex) { // Ignore state change if it's in an input update we're not interested in. var currentUpdateType = InputState.currentUpdateType; var updateTypeMask = updateMask; if ((currentUpdateType & updateTypeMask) == 0) return; // Ignore state change if we have a filter and the state change doesn't pass the check. if (onShouldRecordStateChange != null && !onShouldRecordStateChange(control, time, eventPtr)) return; RecordStateChange(control, control.currentStatePtr, time); } // Unused. /// /// Called when a timeout set on a state change monitor has expired. /// /// Control on which the timeout expired. /// Input time at which the timer expired. This is the time at which an is being /// run whose is past the time of expiration. /// Index of the monitor as given to . /// Index of the timer as given to . /// void IInputStateChangeMonitor.NotifyTimerExpired(InputControl control, double time, long monitorIndex, int timerIndex) { } internal InputControl[] m_Controls; internal int m_ControlCount; private NativeArray m_RecordBuffer; private int m_StateSizeInBytes; private int m_RecordCount; private int m_HistoryDepth = kDefaultHistorySize; private int m_ExtraMemoryPerRecord; internal int m_HeadIndex; internal uint m_CurrentVersion; private InputUpdateType? m_UpdateMask; internal readonly bool m_AddNewControls; internal int bytesPerRecord => (m_StateSizeInBytes + m_ExtraMemoryPerRecord + (m_ControlCount == 1 && !m_AddNewControls ? RecordHeader.kSizeWithoutControlIndex : RecordHeader.kSizeWithControlIndex)).AlignToMultipleOf(4); private struct Enumerator : IEnumerator { private readonly InputStateHistory m_History; private int m_Index; public Enumerator(InputStateHistory history) { m_History = history; m_Index = -1; } public bool MoveNext() { if (m_Index + 1 >= m_History.Count) return false; ++m_Index; return true; } public void Reset() { m_Index = -1; } public Record Current => m_History[m_Index]; object IEnumerator.Current => Current; public void Dispose() { } } /// State change record header /// /// Input State change record header containing the timestamp and other common record data. /// Stored in the . /// /// [StructLayout(LayoutKind.Explicit)] protected internal unsafe struct RecordHeader { /// /// The time stamp of the input state record. /// /// /// The time stamp of the input state record in the owning container. /// /// [FieldOffset(0)] public double time; /// /// The version of the input state record. /// /// /// Current version stamp. See . /// [FieldOffset(8)] public uint version; /// /// The index of the record. /// /// /// The index of the record relative to the start of the buffer. /// See to remap this record index to a user index. /// [FieldOffset(12)] public int controlIndex; [FieldOffset(12)] private fixed byte m_StateWithoutControlIndex[1]; [FieldOffset(16)] private fixed byte m_StateWithControlIndex[1]; /// /// The state data including the control index. /// /// /// The state data including the control index. /// public byte* statePtrWithControlIndex { get { fixed(byte* ptr = m_StateWithControlIndex) return ptr; } } /// /// The state data excluding the control index. /// /// /// The state data excluding the control index. /// public byte* statePtrWithoutControlIndex { get { fixed(byte* ptr = m_StateWithoutControlIndex) return ptr; } } /// /// Size of the state data including the control index. /// /// /// Size of the data including the control index. /// public const int kSizeWithControlIndex = 16; /// /// Size of the state data excluding the control index. /// /// /// Size of the data excluding the control index. /// public const int kSizeWithoutControlIndex = 12; } /// State change record /// Input State change record stored in the . /// public unsafe struct Record : IEquatable { // We store an index rather than a direct pointer to make this struct safer to use. private readonly InputStateHistory m_Owner; private readonly int m_IndexPlusOne; // Plus one so that default(int) works for us. private uint m_Version; internal RecordHeader* header => m_Owner.GetRecord(recordIndex); internal int recordIndex => m_IndexPlusOne - 1; internal uint version => m_Version; /// /// Identifies if the record is valid. /// /// True if the record is a valid entry. False if invalid. /// /// When the history is cleared with the entries become invalid. /// public bool valid => m_Owner != default && m_IndexPlusOne != default && header->version == m_Version; /// /// Identifies the owning container for the record. /// /// The owning container for the record. /// /// Identifies the owning container for the record. /// public InputStateHistory owner => m_Owner; /// /// The index of the input state record in the owning container. /// /// /// The index of the input state record in the owning container. /// /// When the record is no longer value. public int index { get { CheckValid(); return m_Owner.RecordIndexToUserIndex(recordIndex); } } /// /// The time stamp of the input state record. /// /// /// The time stamp of the input state record in the owning container. /// /// /// When the record is no longer value. public double time { get { CheckValid(); return header->time; } } /// /// The control associated with the input state record. /// /// /// The control associated with the input state record. /// /// When the record is no longer value. public InputControl control { get { CheckValid(); var controls = m_Owner.controls; if (controls.Count == 1 && !m_Owner.m_AddNewControls) return controls[0]; return controls[header->controlIndex]; } } /// /// The next input state record in the owning container. /// /// /// The next input state record in the owning container. /// /// When the record is no longer value. public Record next { get { CheckValid(); var userIndex = m_Owner.RecordIndexToUserIndex(this.recordIndex); if (userIndex + 1 >= m_Owner.Count) return default; var recordIndex = m_Owner.UserIndexToRecordIndex(userIndex + 1); return new Record(m_Owner, recordIndex, m_Owner.GetRecord(recordIndex)); } } /// /// The previous input state record in the owning container. /// /// /// The previous input state record in the owning container. /// /// When the record is no longer value. public Record previous { get { CheckValid(); var userIndex = m_Owner.RecordIndexToUserIndex(this.recordIndex); if (userIndex - 1 < 0) return default; var recordIndex = m_Owner.UserIndexToRecordIndex(userIndex - 1); return new Record(m_Owner, recordIndex, m_Owner.GetRecord(recordIndex)); } } internal Record(InputStateHistory owner, int index, RecordHeader* header) { m_Owner = owner; m_IndexPlusOne = index + 1; m_Version = header->version; } /// /// Returns value from the control in the record. /// /// The type of the value being read /// Returns the value from the record. /// When the record is no longer value or the specified type is not present. public TValue ReadValue() where TValue : struct { CheckValid(); return m_Owner.ReadValue(header); } /// /// Read the control's final, processed value from the given state and return the value as an object. /// /// The value of the control associated with the record. /// /// This method allocates GC memory and should not be used during normal gameplay operation. /// /// When the specified value is not present. public object ReadValueAsObject() { CheckValid(); return m_Owner.ReadValueAsObject(header); } /// /// Read the state memory for the record. /// /// The state memory for the record. /// /// Read the state memory for the record. /// public void* GetUnsafeMemoryPtr() { CheckValid(); return GetUnsafeMemoryPtrUnchecked(); } internal void* GetUnsafeMemoryPtrUnchecked() { if (m_Owner.controls.Count == 1 && !m_Owner.m_AddNewControls) return header->statePtrWithoutControlIndex; return header->statePtrWithControlIndex; } /// /// Read the extra memory for the record. /// /// The extra memory for the record. /// /// Additional date can be stored in a record in the extra memory section. /// /// public void* GetUnsafeExtraMemoryPtr() { CheckValid(); return GetUnsafeExtraMemoryPtrUnchecked(); } internal void* GetUnsafeExtraMemoryPtrUnchecked() { if (m_Owner.extraMemoryPerRecord == 0) throw new InvalidOperationException("No extra memory has been set up for history records; set extraMemoryPerRecord"); return (byte*)header + m_Owner.bytesPerRecord - m_Owner.extraMemoryPerRecord; } /// Copy data from one record to another. /// Source record to copy from. /// /// Copy data from one record to another. /// /// When the source record history is not valid. /// When the control is not tracked by the owning container. public void CopyFrom(Record record) { if (!record.valid) throw new ArgumentException("Given history record is not valid", nameof(record)); CheckValid(); // Find control. var control = record.control; var controlIndex = m_Owner.controls.IndexOfReference(control); if (controlIndex == -1) { // We haven't found it. Throw if we can't add it. if (!m_Owner.m_AddNewControls) throw new InvalidOperationException($"Control '{record.control}' is not tracked by target history"); controlIndex = ArrayHelpers.AppendWithCapacity(ref m_Owner.m_Controls, ref m_Owner.m_ControlCount, control); } // Make sure memory sizes match. var numBytesForState = m_Owner.m_StateSizeInBytes; if (numBytesForState != record.m_Owner.m_StateSizeInBytes) throw new InvalidOperationException( $"Cannot copy record from owner with state size '{record.m_Owner.m_StateSizeInBytes}' to owner with state size '{numBytesForState}'"); // Copy and update header. var thisRecordPtr = header; var otherRecordPtr = record.header; UnsafeUtility.MemCpy(thisRecordPtr, otherRecordPtr, RecordHeader.kSizeWithoutControlIndex); thisRecordPtr->version = ++m_Owner.m_CurrentVersion; m_Version = thisRecordPtr->version; // Copy state. var dstPtr = thisRecordPtr->statePtrWithoutControlIndex; if (m_Owner.controls.Count > 1 || m_Owner.m_AddNewControls) { thisRecordPtr->controlIndex = controlIndex; dstPtr = thisRecordPtr->statePtrWithControlIndex; } var srcPtr = record.m_Owner.m_ControlCount > 1 || record.m_Owner.m_AddNewControls ? otherRecordPtr->statePtrWithControlIndex : otherRecordPtr->statePtrWithoutControlIndex; UnsafeUtility.MemCpy(dstPtr, srcPtr, numBytesForState); // Copy extra memory, but only if the size in the source and target // history are identical. var numBytesExtraMemory = m_Owner.m_ExtraMemoryPerRecord; if (numBytesExtraMemory > 0 && numBytesExtraMemory == record.m_Owner.m_ExtraMemoryPerRecord) UnsafeUtility.MemCpy(GetUnsafeExtraMemoryPtr(), record.GetUnsafeExtraMemoryPtr(), numBytesExtraMemory); // Notify. m_Owner.onRecordAdded?.Invoke(this); } internal void CheckValid() { if (m_Owner == default || m_IndexPlusOne == default) throw new InvalidOperationException("Value not initialized"); ////TODO: need to check whether memory has been disposed if (header->version != m_Version) throw new InvalidOperationException("Record is no longer valid"); } /// Compare two records. /// Compare two records. /// The record to compare with. /// True if the records are the same, False if they differ. public bool Equals(Record other) { return ReferenceEquals(m_Owner, other.m_Owner) && m_IndexPlusOne == other.m_IndexPlusOne && m_Version == other.m_Version; } /// Compare two records. /// Compare two records. /// The record to compare with. /// True if the records are the same, False if they differ. public override bool Equals(object obj) { return obj is Record other && Equals(other); } /// Return the hash code of the record. /// Return the hash code of the record. /// The hash code of the record. public override int GetHashCode() { unchecked { var hashCode = m_Owner != null ? m_Owner.GetHashCode() : 0; hashCode = (hashCode * 397) ^ m_IndexPlusOne; hashCode = (hashCode * 397) ^ (int)m_Version; return hashCode; } } /// Return the string representation of the record. /// Includes the control, value and time of the record (or <Invalid> if not valid). /// The string representation of the record. public override string ToString() { if (!valid) return ""; return $"{{ control={control} value={ReadValueAsObject()} time={time} }}"; } } } /// /// Records value changes of a given control over time. /// /// The type of the record being stored /// /// This class makes it easy to track input values over time. It will automatically retain input state up to a given /// maximum history depth (). When the history is full, it will start overwriting the oldest /// entry each time a new history record is received. /// /// The class listens to changes on the given controls by adding change monitors () /// to each control. /// public class InputStateHistory : InputStateHistory, IReadOnlyList.Record> where TValue : struct { /// /// Creates a new InputStateHistory class to record all control state changes. /// /// Maximum size of control state in the record entries. Controls with larger state will not be recorded. /// /// Creates a new InputStateHistory to record a history of control state changes. /// /// New controls are automatically added into the state history if there state is smaller than the threshold. /// public InputStateHistory(int? maxStateSizeInBytes = null) // Using the size of the value here isn't quite correct but the value is used as an upper // bound on stored state size for which the size of the value should be a reasonable guess. : base(maxStateSizeInBytes ?? UnsafeUtility.SizeOf()) { if (maxStateSizeInBytes < UnsafeUtility.SizeOf()) throw new ArgumentException("Max state size cannot be smaller than sizeof(TValue)", nameof(maxStateSizeInBytes)); } /// /// Creates a new InputStateHistory class to record state changes for a specified control. /// /// Control to monitor. /// /// Creates a new InputStateHistory to record a history of state changes for the specified control. /// public InputStateHistory(InputControl control) : base(control) { } /// /// Creates a new InputStateHistory class to record state changes for a specified control. /// /// Control path to identify which controls to monitor. /// /// Creates a new InputStateHistory to record a history of state changes for the specified controls. /// /// /// /// // Track all stick controls in the system. /// var history = new InputStateHistory<Vector2>("*/<Stick>"); /// /// public InputStateHistory(string path) : base(path) { // Make sure that the value type of all matched controls is compatible with TValue. foreach (var control in controls) if (!typeof(TValue).IsAssignableFrom(control.valueType)) throw new ArgumentException( $"Control '{control}' matched by '{path}' has value type '{TypeHelpers.GetNiceTypeName(control.valueType)}' which is incompatible with '{TypeHelpers.GetNiceTypeName(typeof(TValue))}'"); } /// /// InputStateHistory destructor. /// ~InputStateHistory() { Destroy(); } /// /// Add a record to the input state history. /// /// Record to add. /// The newly added record from the history array (as a copy is made). /// /// Add a record to the input state history. /// Allocates an entry in the history array and returns this copy of the original data passed to the function. /// public unsafe Record AddRecord(Record record) { var recordPtr = AllocateRecord(out var index); var newRecord = new Record(this, index, recordPtr); newRecord.CopyFrom(record); return newRecord; } /// /// Record a state change for a specific control. /// /// The control to record the state change for. /// The value to record. /// Time stamp to apply (overriding the event timestamp) /// The newly added record. /// /// Record a state change for a specific control. /// Will call the delegate after adding the record. /// Note this does not call the delegate. /// /// /// /// using (var allTouchTaps = new InputStateHistory<float>(Gamepad.current.leftTrigger)) /// { /// history.RecordStateChange(Gamepad.current.leftTrigger, 0.234f); /// } /// /// public unsafe Record RecordStateChange(InputControl control, TValue value, double time = -1) { using (StateEvent.From(control.device, out var eventPtr)) { var statePtr = (byte*)StateEvent.From(eventPtr)->state - control.device.stateBlock.byteOffset; control.WriteValueIntoState(value, statePtr); if (time >= 0) eventPtr.time = time; var record = RecordStateChange(control, eventPtr); return new Record(this, record.recordIndex, record.header); } } /// /// Enumerate all state history records. /// /// An enumerator going over the state history records. /// /// Enumerate all state history records. /// /// public new IEnumerator GetEnumerator() { return new Enumerator(this); } /// /// Enumerate all state history records. /// /// An enumerator going over the state history records. /// /// Enumerate all state history records. /// IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } /// /// Returns an entry in the state history at the given index. /// /// Index into the array. /// /// Returns a entry from the state history at the given index. /// /// is less than 0 or greater than . public new unsafe Record this[int index] { get { if (index < 0 || index >= Count) throw new ArgumentOutOfRangeException( $"Index {index} is out of range for history with {Count} entries", nameof(index)); var recordIndex = UserIndexToRecordIndex(index); return new Record(this, recordIndex, GetRecord(recordIndex)); } set { if (index < 0 || index >= Count) throw new ArgumentOutOfRangeException( $"Index {index} is out of range for history with {Count} entries", nameof(index)); var recordIndex = UserIndexToRecordIndex(index); new Record(this, recordIndex, GetRecord(recordIndex)).CopyFrom(value); } } private struct Enumerator : IEnumerator { private readonly InputStateHistory m_History; private int m_Index; public Enumerator(InputStateHistory history) { m_History = history; m_Index = -1; } public bool MoveNext() { if (m_Index + 1 >= m_History.Count) return false; ++m_Index; return true; } public void Reset() { m_Index = -1; } public Record Current => m_History[m_Index]; object IEnumerator.Current => Current; public void Dispose() { } } /// State change record /// Input State change record stored in the /// public new unsafe struct Record : IEquatable { private readonly InputStateHistory m_Owner; private readonly int m_IndexPlusOne; private uint m_Version; internal RecordHeader* header => m_Owner.GetRecord(recordIndex); internal int recordIndex => m_IndexPlusOne - 1; /// /// Identifies if the record is valid. /// /// True if the record is a valid entry. False if invalid. /// /// When the history is cleared with the entries become invalid. /// public bool valid => m_Owner != default && m_IndexPlusOne != default && header->version == m_Version; /// /// Identifies the owning container for the record. /// /// The owning container for the record. /// /// Identifies the owning container for the record. /// public InputStateHistory owner => m_Owner; /// /// The index of the input state record in the owning container. /// /// /// The index of the input state record in the owning container. /// /// When the record is no longer value. public int index { get { CheckValid(); return m_Owner.RecordIndexToUserIndex(recordIndex); } } /// /// The time stamp of the input state record. /// /// /// The time stamp of the input state record in the owning container. /// /// /// When the record is no longer value. public double time { get { CheckValid(); return header->time; } } /// /// The control associated with the input state record. /// /// /// The control associated with the input state record. /// /// When the record is no longer value. public InputControl control { get { CheckValid(); var controls = m_Owner.controls; if (controls.Count == 1 && !m_Owner.m_AddNewControls) return (InputControl)controls[0]; return (InputControl)controls[header->controlIndex]; } } /// /// The next input state record in the owning container. /// /// /// The next input state record in the owning container. /// /// When the record is no longer value. public Record next { get { CheckValid(); var userIndex = m_Owner.RecordIndexToUserIndex(this.recordIndex); if (userIndex + 1 >= m_Owner.Count) return default; var recordIndex = m_Owner.UserIndexToRecordIndex(userIndex + 1); return new Record(m_Owner, recordIndex, m_Owner.GetRecord(recordIndex)); } } /// /// The previous input state record in the owning container. /// /// /// The previous input state record in the owning container. /// /// When the record is no longer value. public Record previous { get { CheckValid(); var userIndex = m_Owner.RecordIndexToUserIndex(this.recordIndex); if (userIndex - 1 < 0) return default; var recordIndex = m_Owner.UserIndexToRecordIndex(userIndex - 1); return new Record(m_Owner, recordIndex, m_Owner.GetRecord(recordIndex)); } } internal Record(InputStateHistory owner, int index, RecordHeader* header) { m_Owner = owner; m_IndexPlusOne = index + 1; m_Version = header->version; } internal Record(InputStateHistory owner, int index) { m_Owner = owner; m_IndexPlusOne = index + 1; m_Version = default; } /// /// Returns value from the control in the Record. /// /// Returns value from the Record. /// When the record is no longer value or the specified type is not present. public TValue ReadValue() { CheckValid(); return m_Owner.ReadValue(header); } /// /// Read the state memory for the record. /// /// The state memory for the record. /// /// Read the state memory for the record. /// public void* GetUnsafeMemoryPtr() { CheckValid(); return GetUnsafeMemoryPtrUnchecked(); } internal void* GetUnsafeMemoryPtrUnchecked() { if (m_Owner.controls.Count == 1 && !m_Owner.m_AddNewControls) return header->statePtrWithoutControlIndex; return header->statePtrWithControlIndex; } /// /// Read the extra memory for the record. /// /// The extra memory for the record. /// /// Additional date can be stored in a record in the extra memory section. /// /// public void* GetUnsafeExtraMemoryPtr() { CheckValid(); return GetUnsafeExtraMemoryPtrUnchecked(); } internal void* GetUnsafeExtraMemoryPtrUnchecked() { if (m_Owner.extraMemoryPerRecord == 0) throw new InvalidOperationException("No extra memory has been set up for history records; set extraMemoryPerRecord"); return (byte*)header + m_Owner.bytesPerRecord - m_Owner.extraMemoryPerRecord; } /// Copy data from one record to another. /// Source Record to copy from. /// /// Copy data from one record to another. /// /// When the source record history is not valid. /// When the control is not tracked by the owning container. public void CopyFrom(Record record) { CheckValid(); if (!record.valid) throw new ArgumentException("Given history record is not valid", nameof(record)); var temp = new InputStateHistory.Record(m_Owner, recordIndex, header); temp.CopyFrom(new InputStateHistory.Record(record.m_Owner, record.recordIndex, record.header)); m_Version = temp.version; } private void CheckValid() { if (m_Owner == default || m_IndexPlusOne == default) throw new InvalidOperationException("Value not initialized"); if (header->version != m_Version) throw new InvalidOperationException("Record is no longer valid"); } /// Compare two records. /// Compare two records. /// The record to compare with. /// True if the records are the same, False if they differ. public bool Equals(Record other) { return ReferenceEquals(m_Owner, other.m_Owner) && m_IndexPlusOne == other.m_IndexPlusOne && m_Version == other.m_Version; } /// Compare two records. /// Compare two records. /// The record to compare with. /// True if the records are the same, False if they differ. public override bool Equals(object obj) { return obj is Record other && Equals(other); } /// Return the hash code of the record. /// Return the hash code of the record. /// The hash code of the record. public override int GetHashCode() { unchecked { var hashCode = m_Owner != null ? m_Owner.GetHashCode() : 0; hashCode = (hashCode * 397) ^ m_IndexPlusOne; hashCode = (hashCode * 397) ^ (int)m_Version; return hashCode; } } /// Return the string representation of the record. /// Includes the control, value and time of the record (or <Invalid> if not valid). /// The string representation of the record. public override string ToString() { if (!valid) return ""; return $"{{ control={control} value={ReadValue()} time={time} }}"; } } } }