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} }}";
}
}
}
}