Started project

This commit is contained in:
Caleb Sandford deQuincey
2025-11-14 17:30:41 +00:00
parent 00b55a5b1e
commit 334021d04e
36526 changed files with 4940113 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
////REVIEW: generalize this to AnyButton and add to more devices?
namespace UnityEngine.InputSystem.Controls
{
/// <summary>
/// A control that simply checks the entire state it's been assigned
/// for whether there's any non-zero bytes. If there are, the control
/// returns 1.0; otherwise it returns 0.0.
/// </summary>
/// <remarks>
/// This control is used by <see cref="Keyboard.anyKey"/> to create a button
/// that is toggled on as long as any of the keys on the keyboard is pressed.
/// </remarks>
/// <seealso cref="Keyboard.anyKey"/>
[InputControlLayout(hideInUI = true)]
public class AnyKeyControl : ButtonControl
{
////TODO: wasPressedThisFrame and wasReleasedThisFrame
/// <summary>
/// Default initialization. Sets state size to 1 bit and format to
/// <see cref="InputStateBlock.FormatBit"/>.
/// </summary>
public AnyKeyControl()
{
m_StateBlock.sizeInBits = 1; // Should be overridden by whoever uses the control.
m_StateBlock.format = InputStateBlock.FormatBit;
}
/// <inheritdoc />
public override unsafe float ReadUnprocessedValueFromState(void* statePtr)
{
return this.CheckStateIsAtDefault(statePtr) ? 0.0f : 1.0f;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6e444fe6609243258e0a7746c163373a
timeCreated: 1508369900

View File

@@ -0,0 +1,334 @@
using System.Runtime.CompilerServices;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Processors;
using UnityEngine.InputSystem.Utilities;
////REVIEW: change 'clampToConstant' to simply 'clampToMin'?
////TODO: if AxisControl fields where properties, we wouldn't need ApplyParameterChanges, maybe it's ok breaking change?
namespace UnityEngine.InputSystem.Controls
{
/// <summary>
/// A floating-point axis control.
/// </summary>
/// <remarks>
/// Can optionally be configured to perform normalization.
/// Stored as either a float, a short, a byte, or a single bit.
/// </remarks>
public class AxisControl : InputControl<float>
{
/// <summary>
/// Clamping behavior for an axis control.
/// </summary>
public enum Clamp
{
/// <summary>
/// Do not clamp values.
/// </summary>
None = 0,
/// <summary>
/// Clamp values to <see cref="clampMin"/> and <see cref="clampMax"/>
/// before normalizing the value.
/// </summary>
BeforeNormalize = 1,
/// <summary>
/// Clamp values to <see cref="clampMin"/> and <see cref="clampMax"/>
/// after normalizing the value.
/// </summary>
AfterNormalize = 2,
/// <summary>
/// Clamp values any value below <see cref="clampMin"/> or above <see cref="clampMax"/>
/// to <see cref="clampConstant"/> before normalizing the value.
/// </summary>
ToConstantBeforeNormalize = 3,
}
// These can be added as processors but they are so common that we
// build the functionality right into AxisControl to save us an
// additional object and an additional virtual call.
// NOTE: A number of the parameters here can be expressed in much simpler form.
// E.g. 'scale', 'scaleFactor' and 'invert' could all be rolled into a single
// multiplier. And maybe that's what we should do. However, the one advantage
// of the current setup is that it allows to set these operations up individually.
// For example, a given layout may want to have a very specific scale factor but
// then a derived layout needs the value to be inverted. If it was a single setting,
// the derived layout would have to know the specific scale factor in order to come
// up with a valid multiplier.
/// <summary>
/// Clamping behavior when reading values. <see cref="Clamp.None"/> by default.
/// </summary>
/// <value>Clamping behavior.</value>
/// <remarks>
/// When a value is read from the control's state, it is first converted
/// to a floating-point number.
/// </remarks>
/// <seealso cref="clampMin"/>
/// <seealso cref="clampMax"/>
/// <seealso cref="clampConstant"/>
public Clamp clamp;
/// <summary>
/// Lower end of the clamping range when <see cref="clamp"/> is not
/// <see cref="Clamp.None"/>.
/// </summary>
/// <value>Lower bound of clamping range. Inclusive.</value>
public float clampMin;
/// <summary>
/// Upper end of the clamping range when <see cref="clamp"/> is not
/// <see cref="Clamp.None"/>.
/// </summary>
/// <value>Upper bound of clamping range. Inclusive.</value>
public float clampMax;
/// <summary>
/// When <see cref="clamp"/> is set to <see cref="Clamp.ToConstantBeforeNormalize"/>
/// and the value is outside of the range defined by <see cref="clampMin"/> and
/// <see cref="clampMax"/>, this value is returned.
/// </summary>
/// <value>Constant value to return when value is outside of clamping range.</value>
public float clampConstant;
////REVIEW: why not just roll this into scaleFactor?
/// <summary>
/// If true, the input value will be inverted, i.e. multiplied by -1. Off by default.
/// </summary>
/// <value>Whether to invert the input value.</value>
public bool invert;
/// <summary>
/// If true, normalize the input value to [0..1] or [-1..1] (depending on the
/// value of <see cref="normalizeZero"/>. Off by default.
/// </summary>
/// <value>Whether to normalize input values or not.</value>
/// <seealso cref="normalizeMin"/>
/// <seealso cref="normalizeMax"/>
public bool normalize;
////REVIEW: shouldn't these come from the control min/max value by default?
/// <summary>
/// If <see cref="normalize"/> is on, this is the input value that corresponds
/// to 0 of the normalized [0..1] or [-1..1] range.
/// </summary>
/// <value>Input value that should become 0 or -1.</value>
/// <remarks>
/// In other words, with <see cref="normalize"/> on, input values are mapped from
/// the range of [normalizeMin..normalizeMax] to [0..1] or [-1..1] (depending on
/// <see cref="normalizeZero"/>).
/// </remarks>
public float normalizeMin;
/// <summary>
/// If <see cref="normalize"/> is on, this is the input value that corresponds
/// to 1 of the normalized [0..1] or [-1..1] range.
/// </summary>
/// <value>Input value that should become 1.</value>
/// <remarks>
/// In other words, with <see cref="normalize"/> on, input values are mapped from
/// the range of [normalizeMin..normalizeMax] to [0..1] or [-1..1] (depending on
/// <see cref="normalizeZero"/>).
/// </remarks>
public float normalizeMax;
/// <summary>
/// Where to put the zero point of the normalization range. Only relevant
/// if <see cref="normalize"/> is set to true. Defaults to 0.
/// </summary>
/// <value>Zero point of normalization range.</value>
/// <remarks>
/// The value of this property determines where the zero point is located in the
/// range established by <see cref="normalizeMin"/> and <see cref="normalizeMax"/>.
///
/// If <c>normalizeZero</c> is placed at <see cref="normalizeMin"/>, the normalization
/// returns a value in the [0..1] range mapped from the input value range of
/// <see cref="normalizeMin"/> and <see cref="normalizeMax"/>.
///
/// If <c>normalizeZero</c> is placed in-between <see cref="normalizeMin"/> and
/// <see cref="normalizeMax"/>, normalization returns a value in the [-1..1] mapped
/// from the input value range of <see cref="normalizeMin"/> and <see cref="normalizeMax"/>
/// and the zero point between the two established by <c>normalizeZero</c>.
/// </remarks>
public float normalizeZero;
////REVIEW: why not just have a default scaleFactor of 1?
/// <summary>
/// Whether the scale the input value by <see cref="scaleFactor"/>. Off by default.
/// </summary>
/// <value>True if inputs should be scaled by <see cref="scaleFactor"/>.</value>
public bool scale;
/// <summary>
/// Value to multiple input values with. Only applied if <see cref="scale"/> is <c>true</c>.
/// </summary>
/// <value>Multiplier for input values.</value>
public float scaleFactor;
/// <summary>
/// Apply modifications to the given value according to the parameters configured
/// on the control (<see cref="clamp"/>, <see cref="normalize"/>, etc).
/// </summary>
/// <param name="value">Input value.</param>
/// <returns>A processed value (clamped, normalized, etc).</returns>
/// <seealso cref="clamp"/>
/// <seealso cref="normalize"/>
/// <seealso cref="scale"/>
/// <seealso cref="invert"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected float Preprocess(float value)
{
if (scale)
value *= scaleFactor;
if (clamp == Clamp.ToConstantBeforeNormalize)
{
if (value < clampMin || value > clampMax)
value = clampConstant;
}
else if (clamp == Clamp.BeforeNormalize)
value = Mathf.Clamp(value, clampMin, clampMax);
if (normalize)
value = NormalizeProcessor.Normalize(value, normalizeMin, normalizeMax, normalizeZero);
if (clamp == Clamp.AfterNormalize)
value = Mathf.Clamp(value, clampMin, clampMax);
if (invert)
value *= -1.0f;
return value;
}
private float Unpreprocess(float value)
{
// Does not reverse the effect of clamping (we don't know what the unclamped value should be).
if (invert)
value *= -1f;
if (normalize)
value = NormalizeProcessor.Denormalize(value, normalizeMin, normalizeMax, normalizeZero);
if (scale)
value /= scaleFactor;
return value;
}
/// <summary>
/// Default-initialize the control.
/// </summary>
/// <remarks>
/// Defaults the format to <see cref="InputStateBlock.FormatFloat"/>.
/// </remarks>
public AxisControl()
{
m_StateBlock.format = InputStateBlock.FormatFloat;
}
protected override void FinishSetup()
{
base.FinishSetup();
// if we don't have any default state, and we are using normalizeZero, then the default value
// should not be zero. Generate it from normalizeZero.
if (!hasDefaultState && normalize && Mathf.Abs(normalizeZero) > Mathf.Epsilon)
m_DefaultState = stateBlock.FloatToPrimitiveValue(normalizeZero);
}
/// <inheritdoc />
public override unsafe float ReadUnprocessedValueFromState(void* statePtr)
{
switch (m_OptimizedControlDataType)
{
case InputStateBlock.kFormatFloat:
return *(float*)((byte*)statePtr + m_StateBlock.m_ByteOffset);
case InputStateBlock.kFormatByte:
return *((byte*)statePtr + m_StateBlock.m_ByteOffset) != 0 ? 1.0f : 0.0f;
default:
{
var value = stateBlock.ReadFloat(statePtr);
////REVIEW: this isn't very raw
return Preprocess(value);
}
}
}
/// <inheritdoc />
public override unsafe void WriteValueIntoState(float value, void* statePtr)
{
switch (m_OptimizedControlDataType)
{
case InputStateBlock.kFormatFloat:
*(float*)((byte*)statePtr + m_StateBlock.m_ByteOffset) = value;
break;
case InputStateBlock.kFormatByte:
*((byte*)statePtr + m_StateBlock.m_ByteOffset) = (byte)(value >= 0.5f ? 1 : 0);
break;
default:
value = Unpreprocess(value);
stateBlock.WriteFloat(statePtr, value);
break;
}
}
/// <inheritdoc />
public override unsafe bool CompareValue(void* firstStatePtr, void* secondStatePtr)
{
var currentValue = ReadValueFromState(firstStatePtr);
var valueInState = ReadValueFromState(secondStatePtr);
return !Mathf.Approximately(currentValue, valueInState);
}
/// <inheritdoc />
public override unsafe float EvaluateMagnitude(void* statePtr)
{
return EvaluateMagnitude(ReadValueFromStateWithCaching(statePtr));
}
private float EvaluateMagnitude(float value)
{
if (m_MinValue.isEmpty || m_MaxValue.isEmpty)
return Mathf.Abs(value);
var min = m_MinValue.ToSingle();
var max = m_MaxValue.ToSingle();
var clampedValue = Mathf.Clamp(value, min, max);
// If part of our range is in negative space, evaluate magnitude as two
// separate subspaces.
if (min < 0)
{
if (clampedValue < 0)
return NormalizeProcessor.Normalize(Mathf.Abs(clampedValue), 0, Mathf.Abs(min), 0);
return NormalizeProcessor.Normalize(clampedValue, 0, max, 0);
}
return NormalizeProcessor.Normalize(clampedValue, min, max, 0);
}
protected override FourCC CalculateOptimizedControlDataType()
{
var noProcessingNeeded =
clamp == Clamp.None &&
invert == false &&
normalize == false &&
scale == false;
if (noProcessingNeeded &&
m_StateBlock.format == InputStateBlock.FormatFloat &&
m_StateBlock.sizeInBits == 32 &&
m_StateBlock.bitOffset == 0)
return InputStateBlock.FormatFloat;
if (noProcessingNeeded &&
m_StateBlock.format == InputStateBlock.FormatBit &&
// has to be 8, otherwise we might be mapping to a state which only represents first bit in the byte, while other bits might map to some other controls
// like in the mouse where LMB/RMB map to the same byte, just LMB maps to first bit and RMB maps to second bit
m_StateBlock.sizeInBits == 8 &&
m_StateBlock.bitOffset == 0)
return InputStateBlock.FormatByte;
return InputStateBlock.FormatInvalid;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 551b733126e34f71a7294051cf2bfc0b
timeCreated: 1506741161

View File

@@ -0,0 +1,387 @@
using System.Runtime.CompilerServices;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.Scripting;
////REVIEW: introduce separate base class for ButtonControl and AxisControl instead of deriving ButtonControl from AxisControl?
namespace UnityEngine.InputSystem.Controls
{
/// <summary>
/// An axis that has a trigger point beyond which it is considered to be pressed.
/// </summary>
/// <remarks>
/// By default, stored as a single bit. In that format, buttons will only yield 0
/// and 1 as values. However, buttons return are <see cref="AxisControl"/>s and
/// yield full floating-point values and may thus have a range of values. See
/// <see cref="pressPoint"/> for how button presses on such buttons are handled.
/// </remarks>
public class ButtonControl : AxisControl
{
private bool m_NeedsToCheckFramePress = false;
private uint m_UpdateCountLastPressed = uint.MaxValue;
private uint m_UpdateCountLastReleased = uint.MaxValue;
private bool m_LastUpdateWasPress;
#if UNITY_EDITOR
// Editor input updates have a separate block of state memory, so must be checked separately
private uint m_UpdateCountLastPressedEditor = uint.MaxValue;
private uint m_UpdateCountLastReleasedEditor = uint.MaxValue;
private bool m_LastUpdateWasPressEditor;
#endif
internal bool needsToCheckFramePress { get; private set; }
////REVIEW: are per-control press points really necessary? can we just drop them?
/// <summary>
/// The minimum value the button has to reach for it to be considered pressed.
/// </summary>
/// <remarks>
/// The button is considered pressed, if it has a value equal to or greater than
/// this value.
///
/// By default, this property is set to -1. If the value of the property is negative,
/// <see cref="InputSettings.defaultButtonPressPoint"/> is used.
///
/// The value can be configured as a parameter in a layout.
///
/// <example>
/// <code>
/// using UnityEngine;
/// using UnityEngine.InputSystem.Controls;
///
/// public class MyDevice : InputDevice
/// {
/// [InputControl(parameters = "pressPoint=0.234")]
/// public ButtonControl button { get; private set; }
///
/// //...
/// }
/// </code>
/// </example>
/// </remarks>
public float pressPoint = -1;
/// <summary>
/// Return <see cref="pressPoint"/> if set, otherwise return <see cref="InputSettings.defaultButtonPressPoint"/>.
/// </summary>
public float pressPointOrDefault => pressPoint > 0 ? pressPoint : s_GlobalDefaultButtonPressPoint;
/// <summary>
/// Default-initialize the button control.
/// </summary>
/// <remarks>
/// The default format for the button control is <see cref="InputStateBlock.FormatBit"/>.
/// The button control's minimum value is set to 0 and the maximum value to 1.
/// </remarks>
/// <example>
/// <code>
/// using UnityEngine;
/// using UnityEngine.InputSystem.Controls;
///
/// public class ButtonControlExample : MonoBehaviour
/// {
/// void Start()
/// {
/// var myButton = new ButtonControl();
/// }
///
/// //...
/// }
/// </code>
/// </example>
/// <seealso cref="AxisControl"/>
public ButtonControl()
{
m_StateBlock.format = InputStateBlock.FormatBit;
m_MinValue = 0f;
m_MaxValue = 1f;
}
/// <summary>
/// Whether the given value would be considered pressed for this button.
/// </summary>
/// <param name="value">Value to check for if the button would be considered pressed or not.</param>
/// <returns>True if <paramref name="value"/> crosses the threshold to be considered pressed.</returns>
/// <remarks>
/// The default format for the control is <see cref="InputStateBlock.FormatBit"/>.
/// The control's minimum value is set to 0 and the maximum value to 1.
/// See <see cref="InputSettings.defaultButtonPressPoint"/> and <see cref="pressPoint"/>for the (default) press point.
/// </remarks>
/// <example>
/// <code>
/// using UnityEngine;
/// using UnityEngine.InputSystem.Controls;
///
/// public class IsValueConsideredPressedExample : MonoBehaviour
/// {
/// void Start()
/// {
/// var myButton = new ButtonControl();
/// var valueToTest = 0.5f;
///
/// if (myButton.IsValueConsideredPressed(valueToTest))
/// {
/// Debug.Log("myButton is considered pressed at: " + valueToTest.ToString());
/// }
/// else
/// {
/// Debug.Log("myButton is not considered pressed at: " + valueToTest.ToString());
/// }
/// }
///
/// //...
/// }
/// </code>
/// </example>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public new bool IsValueConsideredPressed(float value)
{
return value >= pressPointOrDefault;
}
/// <summary>
/// Whether the button is currently pressed.
/// </summary>
/// <remarks>
/// A button is considered pressed if its value is equal to or greater
/// than its button press threshold (<see cref="pressPointOrDefault"/>, <see cref="pressPoint"/>).
/// </remarks>
/// <example>
/// <para>You can use this to read whether specific keys are currently pressed by using isPressed on keys, as shown in the following examples:</para>
/// <code>
/// <![CDATA[
/// // Using KeyControl property directly.
/// Keyboard.current.spaceKey.isPressed
/// Keyboard.current.aKey.isPressed // etc.
///
/// // Using Key enum.
/// Keyboard.current[Key.Space].isPressed
///
/// // Using key name.
/// ((KeyControl)Keyboard.current["space"]).isPressed
/// ]]>
/// </code>
/// <para>Note: The Input System identifies keys by physical layout, not according to the current language mapping of the keyboard. To query the name of the key according to the language mapping, use <see cref="InputControl.displayName"/>.
///
/// You can also use this to read mouse buttons, as shown in the following examples:</para>
/// <code>
/// <![CDATA[
/// bool leftPressed = Mouse.current.leftButton.isPressed;
/// bool rightPressed = Mouse.current.rightButton.isPressed;
/// bool middlePressed = Mouse.current.middleButton.isPressed;
/// ]]>
/// </code>
/// <para>You can also check through all numbered buttons on the mouse: (this example does not cause allocations)</para>
/// <code>
/// <![CDATA[
/// var controls = Mouse.current.allControls;
/// for (var i = 0; i < controls.Count; ++i)
/// {
/// var button = controls[i] as ButtonControl;
/// if (button != null && button.isPressed)
/// {
/// // respond to mouse button press here...
/// }
/// }
/// ]]>
/// </code>
/// <para>Or you can look up controls by name, like this:</para>
/// <code>
/// <![CDATA[
/// bool leftPressed = ((ButtonControl)Mouse.current["leftButton"]).isPressed;
/// ]]>
/// </code>
/// </example>
public bool isPressed
{
get
{
// Take the old path if we don't have the speed gain from already testing wasPressedThisFrame/wasReleasedThisFrame.
if (!needsToCheckFramePress)
return IsValueConsideredPressed(value);
#if UNITY_EDITOR
if (InputUpdate.s_LatestUpdateType.IsEditorUpdate())
return m_LastUpdateWasPressEditor;
#endif
return m_LastUpdateWasPress;
}
}
// When we start caring about inter-frame presses, use the info we have to set up the alternate path.
// If we don't do this, users could call wasPressedThisFrame/wasReleasedThisFrame twice for the first time in
// a single frame, and the returned value may be incorrect until the next frame.
private void BeginTestingForFramePresses(bool currentlyPressed, bool pressedLastFrame)
{
needsToCheckFramePress = true;
device.m_ButtonControlsCheckingPressState.Add(this);
#if UNITY_EDITOR
if (InputUpdate.s_LatestUpdateType.IsEditorUpdate())
{
m_LastUpdateWasPressEditor = currentlyPressed;
if (currentlyPressed && !pressedLastFrame)
m_UpdateCountLastPressedEditor = device.m_CurrentUpdateStepCount;
else if (pressedLastFrame && !currentlyPressed)
m_UpdateCountLastReleasedEditor = device.m_CurrentUpdateStepCount;
}
else
#endif
{
m_LastUpdateWasPress = currentlyPressed;
if (currentlyPressed && !pressedLastFrame)
m_UpdateCountLastPressed = device.m_CurrentUpdateStepCount;
else if (pressedLastFrame && !currentlyPressed)
m_UpdateCountLastReleased = device.m_CurrentUpdateStepCount;
}
}
/// <summary>
/// Whether the press started this frame.
/// </summary>
/// <remarks>
/// The first time this function - or wasReleasedThisFrame - are called, it's possible that extremely fast
/// inputs (or very slow frame update times) will result in presses/releases being missed.
/// Following the next input system update after either have been called, and from then on until the device is
/// destroyed, this ceases to be an issue.
/// <example>
/// <code>
/// // An example showing the use of this property on a gamepad button and a keyboard key.
///
/// using UnityEngine;
/// using UnityEngine.InputSystem;
///
/// public class ExampleScript : MonoBehaviour
/// {
/// void Update()
/// {
/// bool buttonPressed = Gamepad.current.aButton.wasPressedThisFrame;
/// bool spaceKeyPressed = Keyboard.current.spaceKey.wasPressedThisFrame;
/// }
/// }
/// </code>
/// </example>
/// _Note_: The Input System identifies keys by physical layout, not according to the current language mapping of the keyboard. To query the name of the key according to the language mapping, use <see cref="InputControl.displayName"/>.
///
/// You can also use this property to read mouse buttons. For example:
///
/// <example>
/// <code>
/// Mouse.current.leftButton.wasPressedThisFrame
/// Mouse.current.rightButton.wasPressedThisFrame
/// Mouse.current.middleButton.wasPressedThisFrame
/// </code>
/// </example>
///
///
/// </remarks>
public bool wasPressedThisFrame
{
get
{
// Take the old path if this is the first time calling.
if (!needsToCheckFramePress)
{
var currentlyPressed = IsValueConsideredPressed(value);
var pressedLastFrame = IsValueConsideredPressed(ReadValueFromPreviousFrame());
BeginTestingForFramePresses(currentlyPressed, pressedLastFrame);
return device.wasUpdatedThisFrame && currentlyPressed && !pressedLastFrame;
}
#if UNITY_EDITOR
if (InputUpdate.s_LatestUpdateType.IsEditorUpdate())
return InputUpdate.s_UpdateStepCount == m_UpdateCountLastPressedEditor;
#endif
return InputUpdate.s_UpdateStepCount == m_UpdateCountLastPressed;
}
}
/// <summary>
/// Whether the press ended this frame.
/// </summary>
/// <remarks>
/// _Note_: The Input System identifies keys by physical layout, not according to the current language mapping of the keyboard. To query the name of the key according to the language mapping, use <see cref="InputControl.displayName"/>.
/// </remarks>
/// <example>
/// <para>An example showing the use of this property on a gamepad button and a keyboard key:</para>
///
/// <code>
/// using UnityEngine;
/// using UnityEngine.InputSystem;
///
/// public class ExampleScript : MonoBehaviour
/// {
/// void Update()
/// {
/// bool buttonReleased = Gamepad.current.aButton.wasReleasedThisFrame;
/// bool spaceKeyReleased = Keyboard.current.spaceKey.wasReleasedThisFrame;
/// }
/// }
/// </code>
/// </example>
public bool wasReleasedThisFrame
{
get
{
// Take the old path if this is the first time calling.
if (!needsToCheckFramePress)
{
var currentlyPressed = IsValueConsideredPressed(value);
var pressedLastFrame = IsValueConsideredPressed(ReadValueFromPreviousFrame());
BeginTestingForFramePresses(currentlyPressed, pressedLastFrame);
return device.wasUpdatedThisFrame && !currentlyPressed && pressedLastFrame;
}
#if UNITY_EDITOR
if (InputUpdate.s_LatestUpdateType.IsEditorUpdate())
return InputUpdate.s_UpdateStepCount == m_UpdateCountLastReleasedEditor;
#endif
return InputUpdate.s_UpdateStepCount == m_UpdateCountLastReleased;
}
}
internal void UpdateWasPressed()
{
var isNowPressed = IsValueConsideredPressed(value);
if (m_LastUpdateWasPress != isNowPressed)
{
if (isNowPressed)
m_UpdateCountLastPressed = device.m_CurrentUpdateStepCount;
else
m_UpdateCountLastReleased = device.m_CurrentUpdateStepCount;
m_LastUpdateWasPress = isNowPressed;
}
}
#if UNITY_EDITOR
internal void UpdateWasPressedEditor()
{
var isNowPressed = IsValueConsideredPressed(value);
if (m_LastUpdateWasPressEditor != isNowPressed)
{
if (isNowPressed)
m_UpdateCountLastPressedEditor = device.m_CurrentUpdateStepCount;
else
m_UpdateCountLastReleasedEditor = device.m_CurrentUpdateStepCount;
m_LastUpdateWasPressEditor = isNowPressed;
}
}
#endif // UNITY_EDITOR
// We make the current global default button press point available as a static so that we don't have to
// constantly make the hop from InputSystem.settings -> InputManager.m_Settings -> defaultButtonPressPoint.
internal static float s_GlobalDefaultButtonPressPoint;
internal static float s_GlobalDefaultButtonReleaseThreshold;
// We clamp button press points to this value as allowing 0 as the press point causes all buttons
// to implicitly be pressed all the time. Not useful.
internal const float kMinButtonPressPoint = 0.0001f;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d618f7e3781548c28836f39ae65d95e1
timeCreated: 1506741155

View File

@@ -0,0 +1,207 @@
using UnityEngine.InputSystem.Utilities;
namespace UnityEngine.InputSystem
{
/// <summary>
/// A collection of common usage string values as reported by <see cref="InputControl.usages"/>.
/// </summary>
public static class CommonUsages
{
/// <summary>
/// Primary 2D motion control.
/// </summary>
/// <remarks>
/// Example: Left stick on a gamepad.
/// </remarks>
public static readonly InternedString Primary2DMotion = new InternedString("Primary2DMotion");
/// <summary>
/// Secondary 2D motion control.
/// </summary>
/// <remarks>
/// Example: Right stick on a gamepad.
/// </remarks>
public static readonly InternedString Secondary2DMotion = new InternedString("Secondary2DMotion");
/// <summary>
/// The primary action control on any input device, such as a gamepad, mouse, or keyboard.
/// </summary>
/// <remarks>
/// Example: Primary mouse button (left button on right-handed configuration, right button on left-handed configuration),
/// south-button on a gamepad.
/// </remarks>
public static readonly InternedString PrimaryAction = new InternedString("PrimaryAction");
/// <summary>
/// Secondary action control on any input device, such as a gamepad, mouse, or keyboard.
/// </summary>
/// <remarks>
/// Example: Secondary mouse button (right button on right-handed configuration, left button on left-handed configuration),
/// east-button on a gamepad.
/// </remarks>
public static readonly InternedString SecondaryAction = new InternedString("SecondaryAction");
/// <summary>
/// The primary trigger control on input devices with triggers.
/// </summary>
/// <remarks>
/// Example: Right trigger-button on a gamepad.
/// </remarks>
public static readonly InternedString PrimaryTrigger = new InternedString("PrimaryTrigger");
/// <summary>
/// The secondary trigger control on input devices with triggers.
/// </summary>
/// <remarks>
/// Example: Left trigger-button on a gamepad.
/// </remarks>
public static readonly InternedString SecondaryTrigger = new InternedString("SecondaryTrigger");
/// <summary>
/// A modifier action control that modifies usage of other controls.
/// </summary>
/// <remarks>
/// Example: Keyboard modifier keys like CTRL, SHIFT, ALT, OPTION, etc.
/// </remarks>
public static readonly InternedString Modifier = new InternedString("Modifier");
/// <summary>
/// The spatial position control on input devices with spatial tracking.
/// </summary>
/// <remarks>
/// Example: User head position in tracking-space using e.g. a head-tracking system. This could for example be a VR tracking system or another user-facing tracking sensor.
/// </remarks>
public static readonly InternedString Position = new InternedString("Position");
/// <summary>
/// The spatial orientation control on input devices with spatial tracking.
/// </summary>
/// <remarks>
/// Example: User head-orientation in tracking-space using e.g. a head-tracking system. This could for example be a VR tracking system or another user-facing tracking sensor.
/// </remarks>
public static readonly InternedString Orientation = new InternedString("Orientation");
/// <summary>
/// The primary hat-switch control on input devices with hat-switches such as joysticks or gamepads.
/// </summary>
/// <remarks>
/// Example: Joystick or gamepad hat-switch.
/// </remarks>
public static readonly InternedString Hatswitch = new InternedString("Hatswitch");
/// <summary>
/// Button to navigate to previous location.
/// </summary>
/// <remarks>
/// Example: Escape on keyboard, B button on gamepad.
///
/// In general, the "Back" control is used for moving backwards in the navigation history
/// of a UI. This is used, for example, in hierarchical menu structures to move back to parent menus
/// (e.g. from the "Settings" menu back to the "Main" menu). Consoles generally have stringent requirements
/// as to which button has to fulfill this role.
/// </remarks>
public static readonly InternedString Back = new InternedString("Back");
/// <summary>
/// Button to navigate to next location.
/// </summary>
public static readonly InternedString Forward = new InternedString("Forward");
/// <summary>
/// Button to bring up menu.
/// </summary>
public static readonly InternedString Menu = new InternedString("Menu");
/// <summary>
/// Button to confirm the current choice.
/// </summary>
public static readonly InternedString Submit = new InternedString("Submit");
////REVIEW: isn't this the same as "Back"?
/// <summary>
/// Button to not accept the current choice.
/// </summary>
public static readonly InternedString Cancel = new InternedString("Cancel");
/// <summary>
/// Horizontal motion axis.
/// </summary>
/// <remarks>
/// Example: X axis on mouse.
/// </remarks>
public static readonly InternedString Horizontal = new InternedString("Horizontal");
/// <summary>
/// Vertical motion axis.
/// </summary>
/// <remarks>
/// Example: Y axis on mouse.
/// </remarks>
public static readonly InternedString Vertical = new InternedString("Vertical");
/// <summary>
/// Rotation around single, fixed axis.
/// </summary>
/// <remarks>
/// Example: twist on joystick or twist of pen (few pens support that).
/// </remarks>
public static readonly InternedString Twist = new InternedString("Twist");
/// <summary>
/// Pressure level axis.
/// </summary>
/// <remarks>
/// Example: pen pressure.
/// </remarks>
public static readonly InternedString Pressure = new InternedString("Pressure");
/// <summary>
/// Axis to scroll horizontally.
/// </summary>
public static readonly InternedString ScrollHorizontal = new InternedString("ScrollHorizontal");
/// <summary>
/// Axis to scroll vertically.
/// </summary>
public static readonly InternedString ScrollVertical = new InternedString("ScrollVertical");
/// <summary>
/// A screen-space point.
/// </summary>
/// <remarks>
/// Example: Touch contact point.
/// </remarks>
public static readonly InternedString Point = new InternedString("Point");
/// <summary>
/// Low-frequency haptic motor for force-feedback.
/// </summary>
public static readonly InternedString LowFreqMotor = new InternedString("LowFreqMotor");
/// <summary>
/// High-frequency haptic motor for force-feedback.
/// </summary>
public static readonly InternedString HighFreqMotor = new InternedString("HighFreqMotor");
/// <summary>
/// Device in left hand.
/// </summary>
/// <remarks>
/// Example: left hand XR controller.
/// </remarks>
public static readonly InternedString LeftHand = new InternedString("LeftHand");
/// <summary>
/// Device in right hand.
/// </summary>
/// <remarks>
/// Example: right hand XR controller.
/// </remarks>
public static readonly InternedString RightHand = new InternedString("RightHand");
/// <summary>
/// Axis representing charge of battery (1=full, 0=empty).
/// </summary>
public static readonly InternedString BatteryStrength = new InternedString("BatteryStrength");
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cf624ae4ff5e44cebdcba54ae1ff380a
timeCreated: 1507150918

View File

@@ -0,0 +1,70 @@
using UnityEngine.InputSystem.Layouts;
using UnityEngine.Scripting;
namespace UnityEngine.InputSystem.Controls
{
/// <summary>
/// Delta controls are a two-dimensional motion vector that accumulate within a frame
/// and reset at the beginning of a frame. You can read the values from a delta control
/// using the inherited members from Vector2Control or InputControl.
/// </summary>
/// <see cref="Pointer.delta"/>
/// <seealso cref="Mouse.scroll"/>
[Preserve]
public class DeltaControl : Vector2Control
{
/// <summary>
/// A synthetic axis representing the upper half of the Y axis value, i.e. the 0 to 1 range.
/// </summary>
/// <value>Control representing the control's upper half Y axis.</value>
/// <remarks>
/// The control is marked as <see cref="InputControl.synthetic"/>.
/// </remarks>
[InputControl(useStateFrom = "y", parameters = "clamp=1,clampMin=0,clampMax=3.402823E+38", synthetic = true, displayName = "Up")]
[Preserve]
public AxisControl up { get; set; }
/// <summary>
/// A synthetic axis representing the lower half of the Y axis value, i.e. the 0 to -1 range (inverted).
/// </summary>
/// <value>Control representing the control's lower half Y axis.</value>
/// <remarks>
/// The control is marked as <see cref="InputControl.synthetic"/>.
/// </remarks>
[InputControl(useStateFrom = "y", parameters = "clamp=1,clampMin=-3.402823E+38,clampMax=0,invert", synthetic = true, displayName = "Down")]
[Preserve]
public AxisControl down { get; set; }
/// <summary>
/// A synthetic axis representing the left half of the X axis value, i.e. the 0 to -1 range (inverted).
/// </summary>
/// <value>Control representing the control's left half X axis.</value>
/// <remarks>
/// The control is marked as <see cref="InputControl.synthetic"/>.
/// </remarks>
[InputControl(useStateFrom = "x", parameters = "clamp=1,clampMin=-3.402823E+38,clampMax=0,invert", synthetic = true, displayName = "Left")]
[Preserve]
public AxisControl left { get; set; }
/// <summary>
/// A synthetic axis representing the right half of the X axis value, i.e. the 0 to 1 range.
/// </summary>
/// <value>Control representing the control's right half X axis.</value>
/// <remarks>
/// The control is marked as <see cref="InputControl.synthetic"/>.
/// </remarks>
[InputControl(useStateFrom = "x", parameters = "clamp=1,clampMin=0,clampMax=3.402823E+38", synthetic = true, displayName = "Right")]
[Preserve]
public AxisControl right { get; set; }
protected override void FinishSetup()
{
base.FinishSetup();
up = GetChildControl<AxisControl>("up");
down = GetChildControl<AxisControl>("down");
left = GetChildControl<AxisControl>("left");
right = GetChildControl<AxisControl>("right");
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 339c9c6bc0554ad79681e127d417ef3d
timeCreated: 1630420986

View File

@@ -0,0 +1,149 @@
using System;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
////TODO: hide in UI
namespace UnityEngine.InputSystem.Controls
{
/// <summary>
/// A button that is considered pressed if the underlying state has a value in the specific range.
/// </summary>
/// <remarks>
/// This control is most useful for handling HID-style hat switches. Unlike <see cref="DpadControl"/>,
/// which by default is stored as a bitfield of four bits that each represent a direction on the pad,
/// these hat switches enumerate the possible directions that the switch can be moved in. For example,
/// the value 1 could indicate that the switch is moved to the left whereas 3 could indicate it is
/// moved up.
///
/// <example>
/// <code>
/// [StructLayout(LayoutKind.Explicit, Size = 32)]
/// internal struct DualShock4HIDInputReport : IInputStateTypeInfo
/// {
/// [FieldOffset(0)] public byte reportId;
///
/// [InputControl(name = "dpad", format = "BIT", layout = "Dpad", sizeInBits = 4, defaultState = 8)]
/// [InputControl(name = "dpad/up", format = "BIT", layout = "DiscreteButton", parameters = "minValue=7,maxValue=1,nullValue=8,wrapAtValue=7", bit = 0, sizeInBits = 4)]
/// [InputControl(name = "dpad/right", format = "BIT", layout = "DiscreteButton", parameters = "minValue=1,maxValue=3", bit = 0, sizeInBits = 4)]
/// [InputControl(name = "dpad/down", format = "BIT", layout = "DiscreteButton", parameters = "minValue=3,maxValue=5", bit = 0, sizeInBits = 4)]
/// [InputControl(name = "dpad/left", format = "BIT", layout = "DiscreteButton", parameters = "minValue=5, maxValue=7", bit = 0, sizeInBits = 4)]
/// [FieldOffset(5)] public byte buttons1;
/// }
/// </code>
/// </example>
/// </remarks>
public class DiscreteButtonControl : ButtonControl
{
/// <summary>
/// Value (inclusive) at which to start considering the button to be pressed.
/// </summary>
/// <remarks>
/// <see cref="minValue"/> is allowed to be larger than <see cref="maxValue"/>. This indicates
/// a setup where the value wraps around beyond <see cref="minValue"/>, skips <see cref="nullValue"/>,
/// and then goes all the way up to <see cref="maxValue"/>.
///
/// For example, if the underlying state represents a circular D-pad and enumerates its
/// 9 possible positions (including null state) going clock-wise from 0 to 8 and with 1
/// indicating that the D-pad is pressed to the left, then 1, 2, and 8 would indicate
/// that the "left" button is held on the D-pad. To set this up, set <see cref="minValue"/>
/// to 8, <see cref="maxValue"/> to 2, and <see cref="nullValue"/> to 0 (the default).
/// </remarks>
public int minValue;
/// <summary>
/// Value (inclusive) beyond which to stop considering the button to be pressed.
/// </summary>
public int maxValue;
/// <summary>
/// Value (inclusive) at which the values cut off and wrap back around. Considered to not be set if equal to <see cref="nullValue"/>.
/// </summary>
/// <remarks>
/// This is useful if, for example, an enumeration has more bits than are used for "on" states of controls. For example,
/// a bitfield of 4 bits where values 1-7 (i.e. 0001 to 0111) indicate "on" states of controls and value 8 indicating an
/// "off" state. In that case, set <see cref="nullValue"/> to 8 and <see cref="wrapAtValue"/> to 7.
/// </remarks>
public int wrapAtValue;
/// <summary>
/// Value at which the button is considered to not be pressed. Usually zero. Some devices, however,
/// use non-zero default states.
/// </summary>
public int nullValue;
/// <summary>
/// Determines the behavior of <see cref="WriteValueIntoState"/>. By default, attempting to write a <see cref="DiscreteButtonControl"/>
/// will result in a <c>NotSupportedException</c>.
/// </summary>
public WriteMode writeMode;
protected override void FinishSetup()
{
base.FinishSetup();
if (!stateBlock.format.IsIntegerFormat())
throw new NotSupportedException(
$"Non-integer format '{stateBlock.format}' is not supported for DiscreteButtonControl '{this}'");
}
public override unsafe float ReadUnprocessedValueFromState(void* statePtr)
{
var valuePtr = (byte*)statePtr + (int)m_StateBlock.byteOffset;
// Note that all signed data in state buffers is in excess-K format.
var intValue = MemoryHelpers.ReadTwosComplementMultipleBitsAsInt(valuePtr, m_StateBlock.bitOffset, m_StateBlock.sizeInBits);
var value = 0.0f;
if (minValue > maxValue)
{
// If no wrapping point is set, default to wrapping around exactly
// at the point of minValue.
if (wrapAtValue == nullValue)
wrapAtValue = minValue;
if ((intValue >= minValue && intValue <= wrapAtValue)
|| (intValue != nullValue && intValue <= maxValue))
value = 1.0f;
}
else
{
value = intValue >= minValue && intValue <= maxValue ? 1.0f : 0.0f;
}
return Preprocess(value);
}
public override unsafe void WriteValueIntoState(float value, void* statePtr)
{
if (writeMode == WriteMode.WriteNullAndMaxValue)
{
var valuePtr = (byte*)statePtr + (int)m_StateBlock.byteOffset;
var valueToWrite = value >= pressPointOrDefault ? maxValue : nullValue;
MemoryHelpers.WriteIntAsTwosComplementMultipleBits(valuePtr, m_StateBlock.bitOffset, m_StateBlock.sizeInBits, valueToWrite);
return;
}
// The way these controls are usually used, the state is shared between multiple DiscreteButtons. So writing one
// may have unpredictable effects on the value of other buttons.
throw new NotSupportedException("Writing value states for DiscreteButtonControl is not supported as a single value may correspond to multiple states");
}
/// <summary>
/// How <see cref="DiscreteButtonControl.WriteValueIntoState"/> should behave.
/// </summary>
public enum WriteMode
{
/// <summary>
/// <see cref="DiscreteButtonControl.WriteValueIntoState"/> will throw <c>NotSupportedException</c>.
/// </summary>
WriteDisabled = 0,
/// <summary>
/// Write <see cref="DiscreteButtonControl.nullValue"/> for when the button is considered not pressed and
/// write <see cref="DiscreteButtonControl.maxValue"/> for when the button is considered pressed.
/// </summary>
WriteNullAndMaxValue = 1,
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1ab470181bfe4ba891e0d1584730fc4c
timeCreated: 1517190786

View File

@@ -0,0 +1,30 @@
using UnityEngine.InputSystem.LowLevel;
namespace UnityEngine.InputSystem.Controls
{
/// <summary>
/// A control reading a <see cref="double"/>.
/// </summary>
public class DoubleControl : InputControl<double>
{
/// <summary>
/// Default-initialize the control.
/// </summary>
public DoubleControl()
{
m_StateBlock.format = InputStateBlock.FormatDouble;
}
/// <inheritdoc/>
public override unsafe double ReadUnprocessedValueFromState(void* statePtr)
{
return m_StateBlock.ReadDouble(statePtr);
}
/// <inheritdoc/>
public override unsafe void WriteValueIntoState(double value, void* statePtr)
{
m_StateBlock.WriteDouble(statePtr, value);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 72251f49d44478b4c93462938d7ce4cf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,165 @@
using System;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
namespace UnityEngine.InputSystem.Controls
{
/// <summary>
/// A control made up of four discrete, directional buttons. Forms a vector
/// but can also be addressed as individual buttons.
/// </summary>
/// <remarks>
/// Is stored as four bits by default.
///
/// The vector that is aggregated from the button states is normalized. I.e.
/// even if pressing diagonally, the vector will have a length of 1 (instead
/// of reading something like <c>(1,1)</c> for example).
/// </remarks>
public class DpadControl : Vector2Control
{
[InputControlLayout(hideInUI = true)]
public class DpadAxisControl : AxisControl
{
public int component { get; set; }
protected override void FinishSetup()
{
base.FinishSetup();
component = name == "x" ? 0 : 1;
// Set the state block to be the parent's state block. We don't use that to read
// the axis directly (we call the parent control to do that), but we need to set
// it up the actions know to monitor this memory for changes to the control.
m_StateBlock = m_Parent.m_StateBlock;
}
public override unsafe float ReadUnprocessedValueFromState(void* statePtr)
{
var value = ((DpadControl)m_Parent).ReadUnprocessedValueFromState(statePtr);
return value[component];
}
}
// The DpadAxisControl has it's own logic to read state from the parent dpad.
// The useStateFrom argument here is not actually used by that. The only reason
// it is set up here is to avoid any state bytes being reserved for the DpadAxisControl.
[InputControl(name = "x", layout = "DpadAxis", useStateFrom = "right", synthetic = true)]
[InputControl(name = "y", layout = "DpadAxis", useStateFrom = "up", synthetic = true)]
/// <summary>
/// The button representing the vertical upwards state of the D-Pad.
/// </summary>
[InputControl(bit = (int)ButtonBits.Up, displayName = "Up")]
public ButtonControl up { get; set; }
/// <summary>
/// The button representing the vertical downwards state of the D-Pad.
/// </summary>
[InputControl(bit = (int)ButtonBits.Down, displayName = "Down")]
public ButtonControl down { get; set; }
/// <summary>
/// The button representing the horizontal left state of the D-Pad.
/// </summary>
[InputControl(bit = (int)ButtonBits.Left, displayName = "Left")]
public ButtonControl left { get; set; }
/// <summary>
/// The button representing the horizontal right state of the D-Pad.
/// </summary>
[InputControl(bit = (int)ButtonBits.Right, displayName = "Right")]
public ButtonControl right { get; set; }
////TODO: should have X and Y child controls as well
public DpadControl()
{
m_StateBlock.sizeInBits = 4;
m_StateBlock.format = InputStateBlock.FormatBit;
}
protected override void FinishSetup()
{
up = GetChildControl<ButtonControl>("up");
down = GetChildControl<ButtonControl>("down");
left = GetChildControl<ButtonControl>("left");
right = GetChildControl<ButtonControl>("right");
base.FinishSetup();
}
public override unsafe Vector2 ReadUnprocessedValueFromState(void* statePtr)
{
var upIsPressed = up.ReadValueFromStateWithCaching(statePtr) >= up.pressPointOrDefault;
var downIsPressed = down.ReadValueFromStateWithCaching(statePtr) >= down.pressPointOrDefault;
var leftIsPressed = left.ReadValueFromStateWithCaching(statePtr) >= left.pressPointOrDefault;
var rightIsPressed = right.ReadValueFromStateWithCaching(statePtr) >= right.pressPointOrDefault;
return MakeDpadVector(upIsPressed, downIsPressed, leftIsPressed, rightIsPressed);
}
public override unsafe void WriteValueIntoState(Vector2 value, void* statePtr)
{
var upIsPressed = up.IsValueConsideredPressed(value.y);
var downIsPressed = down.IsValueConsideredPressed(value.y * -1f);
var leftIsPressed = left.IsValueConsideredPressed(value.x * -1f);
var rightIsPressed = right.IsValueConsideredPressed(value.x);
up.WriteValueIntoState(upIsPressed && !downIsPressed ? value.y : 0f, statePtr);
down.WriteValueIntoState(downIsPressed && !upIsPressed ? value.y * -1f : 0f, statePtr);
left.WriteValueIntoState(leftIsPressed && !rightIsPressed ? value.x * -1f : 0f, statePtr);
right.WriteValueIntoState(rightIsPressed && !leftIsPressed ? value.x : 0f, statePtr);
}
/// <summary>
/// Create a direction vector from the given four button states.
/// </summary>
/// <param name="up">Whether button representing the up direction is pressed.</param>
/// <param name="down">Whether button representing the down direction is pressed.</param>
/// <param name="left">Whether button representing the left direction is pressed.</param>
/// <param name="right">Whether button representing the right direction is pressed.</param>
/// <param name="normalize">Whether to normalize the resulting vector. If this is false, vectors in the diagonal
/// directions will have a magnitude of greater than 1. For example, up-left will be (-1,1).</param>
/// <returns>A 2D direction vector.</returns>
public static Vector2 MakeDpadVector(bool up, bool down, bool left, bool right, bool normalize = true)
{
var upValue = up ? 1.0f : 0.0f;
var downValue = down ? -1.0f : 0.0f;
var leftValue = left ? -1.0f : 0.0f;
var rightValue = right ? 1.0f : 0.0f;
var result = new Vector2(leftValue + rightValue, upValue + downValue);
if (normalize)
{
// If press is diagonal, adjust coordinates to produce vector of length 1.
// pow(0.707107) is roughly 0.5 so sqrt(pow(0.707107)+pow(0.707107)) is ~1.
const float diagonal = 0.707107f;
if (result.x != 0 && result.y != 0)
result = new Vector2(result.x * diagonal, result.y * diagonal);
}
return result;
}
/// <summary>
/// Create a direction vector from the given axis states.
/// </summary>
/// <param name="up">Axis value representing the up direction.</param>
/// <param name="down">Axis value representing the down direction.</param>
/// <param name="left">Axis value representing the left direction.</param>
/// <param name="right">Axis value representing the right direction.</param>
/// <returns>A 2D direction vector.</returns>
public static Vector2 MakeDpadVector(float up, float down, float left, float right)
{
return new Vector2(-left + right, up - down);
}
internal enum ButtonBits
{
Up,
Down,
Left,
Right,
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b00a605ae58d438baf0b80388b377972
timeCreated: 1506743933

View File

@@ -0,0 +1,1817 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.InputSystem.Layouts;
////REVIEW: should EvaluateMagnitude() be called EvaluateActuation() or something similar?
////REVIEW: as soon as we gain the ability to have blittable type constraints, InputControl<TValue> should be constrained such
////REVIEW: Reading and writing is asymmetric. Writing does not involve processors, reading does.
////REVIEW: While the arrays used by controls are already nicely centralized on InputDevice, InputControls still
//// hold a bunch of reference data that requires separate scanning. Can we move *all* reference data to arrays
//// on InputDevice and make InputControls reference-free? Most challenging thing probably is getting rid of
//// the InputDevice reference itself.
////REVIEW: how do we do stuff like smoothing over time?
////TODO: allow easier access to the default state such that you can easily create a state event containing only default state
////TODO: come up with a way where we do ReadValue on the most common forms/setups of controls and not have any virtual method dispatch but
//// rather go with minimal overhead directly to reading out memory
//// (this should at least cover FLT, single BIT, and INT controls; and should be able to apply the common transformations
//// as per AxisControl)
namespace UnityEngine.InputSystem
{
/// <summary>
/// A typed and named source of input values in a hierarchy of controls.
/// </summary>
/// <remarks>
/// Controls can have children which in turn may have children. At the root of the child
/// hierarchy is always an <see cref="InputDevice"/> (which themselves are InputControls).
///
/// Controls can be looked up by their <see cref="path"/> (see <see cref="InputControlPath.TryFindControl"/>).
///
/// Each control must have a unique <see cref="name"/> within the <see cref="children"/> of
/// its <see cref="parent"/>. Multiple names can be assigned to controls using aliases (see
/// <see cref="aliases"/>). Name lookup is case-insensitive.
///
/// For display purposes, a control may have a separate <see cref="displayName"/>. This name
/// will usually correspond to what the control is caused on the actual underlying hardware.
/// For example, on an Xbox gamepad, the control with the name "buttonSouth" will have a display
/// name of "A". Controls that have very long display names may also have a <see cref="shortDisplayName"/>.
/// This is the case for the "Left Button" on the <see cref="Mouse"/>, for example, which is
/// commonly abbreviated "LMB".
///
/// In addition to names, a control may have usages associated with it (see <see cref="usages"/>).
/// A usage indicates how a control is meant to be used. For example, a button can be assigned
/// the "PrimaryAction" usage to indicate it is the primary action button the device. Within a
/// device, usages have to be unique. See <see cref="CommonUsages"/> for a list of standardized usages.
///
/// Controls do not actually store values. Instead, every control receives an <see cref="InputStateBlock"/>
/// which, after the control's device has been added to the system, is used to read out values
/// from the device's backing store. This backing store is referred to as "state" in the API
/// as opposed to "values" which represent the data resulting from reading state. The format that
/// each control stores state in is specific to the control. It can vary not only between controls
/// of different types but also between controls of the same type. An <see cref="AxisControl"/>,
/// for example, can be stored as a float or as a byte or in a number of other formats. <see cref="stateBlock"/>
/// identifies both where the control stores its state as well as the format it stores it in.
///
/// Controls are generally not created directly but are created internally by the input system
/// from data known as "layouts" (see <see cref="InputControlLayout"/>). Each such layout describes
/// the setup of a specific hierarchy of controls. The system internally maintains a registry of
/// layouts and produces devices and controls from them as needed. The layout that a control has
/// been created from can be queried using <see cref="layout"/>. For most purposes, the intricacies
/// of the control layout mechanisms can be ignored and it is sufficient to know the names of a
/// small set of common device layouts such as "Keyboard", "Mouse", "Gamepad", and "Touchscreen".
///
/// Each control has a single, fixed value type. The type can be queried at runtime using
/// <see cref="valueType"/>. Most types of controls are derived from <see cref="InputControl{TValue}"/>
/// which has APIs specific to the type of value of the control (e.g. <see cref="InputControl{TValue}.ReadValue()"/>.
///
/// The following example demonstrates various common operations performed on input controls:
/// </remarks>
/// <example>
/// <code>
/// // Look up dpad/up control on current gamepad.
/// var dpadUpControl = Gamepad.current["dpad/up"];
///
/// // Look up the back button on the current gamepad.
/// var backButton = Gamepad.current["{Back}"];
///
/// // Look up all dpad/up controls on all gamepads in the system.
/// using (var controls = InputSystem.FindControls("&lt;Gamepad&gt;/dpad/up"))
/// Debug.Log($"Found {controls.Count} controls");
///
/// // Display the value of all controls on the current gamepad.
/// foreach (var control in Gamepad.current.allControls)
/// Debug.Log(controls.ReadValueAsObject());
///
/// // Track the value of the left stick on the current gamepad over time.
/// var leftStickHistory = new InputStateHistory(Gamepad.current.leftStick);
/// leftStickHistory.Enable();
/// </code>
/// </example>
/// <seealso cref="InputControl{TValue}"/>
/// <seealso cref="InputDevice"/>
/// <seealso cref="InputControlPath"/>
/// <seealso cref="InputStateBlock"/>
[DebuggerDisplay("{DebuggerDisplay(),nq}")]
public abstract class InputControl
{
/// <summary>
/// The name of the control, i.e. the final name part in its path.
/// </summary>
/// <remarks>
/// Names of controls must be unique within the context of their parent.
///
/// Note that this is the name of the control as assigned internally (like "buttonSouth")
/// and not necessarily a good display name. Use <see cref="displayName"/> for
/// getting more readable names for display purposes (where available).
///
/// Lookup of names is case-insensitive.
///
/// This is set from the name of the control in the layout.
/// See <see cref="path"/>, <see cref="aliases"/>, <see cref="InputControlAttribute.name"/> and <see cref="InputControlLayout.ControlItem.name"/>.
/// </remarks>
public string name => m_Name;
////TODO: protect against empty strings
/// <summary>
/// The text to display as the name of the control.
/// </summary>
/// <remarks>
/// Note that the display name of a control may change over time. For example, when changing
/// from a QWERTY keyboard layout to an AZERTY keyboard layout, the "q" key (which will keep
/// that <see cref="name"/>) will change its display name from "q" to "a".
///
/// By default, a control's display name will come from its layout. If it is not assigned
/// a display name there, the display name will default to <see cref="name"/>. However, specific
/// controls may override this behavior. <see cref="KeyControl"/>, for example, will set the
/// display name to the actual key name corresponding to the current keyboard layout.
///
/// For nested controls, the display name will include the display names of all parent controls,
/// i.e. the display name will fully identify the control on the device. For example, the display
/// name for the left D-Pad button on a gamepad is "D-Pad Left" and not just "Left".
///
/// There is a short name version, see <see cref="shortDisplayName"/>.
/// </remarks>
public string displayName
{
get
{
RefreshConfigurationIfNeeded();
if (m_DisplayName != null)
return m_DisplayName;
if (m_DisplayNameFromLayout != null)
return m_DisplayNameFromLayout;
return m_Name;
}
// This is not public as a domain reload will wipe the change. This should really
// come from the control itself *if* the control wants to have a custom display name
// not driven by its layout.
protected set => m_DisplayName = value;
}
/// <summary>
/// An alternate, abbreviated <see cref="displayName"/> (for example "LMB" instead of "Left Button").
/// </summary>
/// <remarks>
/// If the control has no abbreviated version, this will be null. Note that this behavior is different
/// from <see cref="displayName"/> which will fall back to <see cref="name"/> if no display name has
/// been assigned to the control.
///
/// For nested controls, the short display name will include the short display names of all parent controls,
/// that is, the display name will fully identify the control on the device. For example, the display
/// name for the left D-Pad button on a gamepad is "D-Pad \u2190" and not just "\u2190". Note that if a parent
/// control has no short name, its long name will be used instead. See <see cref="displayName"/>.
/// </remarks>
public string shortDisplayName
{
get
{
RefreshConfigurationIfNeeded();
if (m_ShortDisplayName != null)
return m_ShortDisplayName;
if (m_ShortDisplayNameFromLayout != null)
return m_ShortDisplayNameFromLayout;
return null;
}
protected set => m_ShortDisplayName = value;
}
/// <summary>
/// Full path all the way from the root.
/// </summary>
/// <remarks>
/// This will always be the "effective" path of the control, i.e. it will not contain
/// elements such as usages (<c>"{Back}"</c>) and other elements that can be part of
/// control paths used for matching. Instead, this property will always be a simple
/// linear ordering of names leading from the device at the top to the control with each
/// element being separated by a forward slash (<c>/</c>).
///
/// Allocates on first hit. Paths are not created until someone asks for them.
///
/// For more details on the path see <see cref="InputControlPath"/>.
/// </remarks>
/// <example>
/// Example: "/gamepad/leftStick/x"
/// </example>
public string path
{
get
{
if (m_Path == null)
m_Path = InputControlPath.Combine(m_Parent, m_Name);
return m_Path;
}
}
/// <summary>
/// Layout name for the control it is based on.
/// </summary>
/// <remarks>
/// This is the layout name rather than a reference to an <see cref="InputControlLayout"/> as
/// we only create layout instances during device creation and treat them
/// as temporaries in general so as to not waste heap space during normal operation.
/// </remarks>
public string layout => m_Layout;
/// <summary>
/// Semicolon-separated list of variants of the control layout or "default".
/// </summary>
/// <example>
/// "Lefty" when using the "Lefty" gamepad layout.
/// </example>
public string variants => m_Variants;
/// <summary>
/// The device that this control is a part of.
/// </summary>
/// <remarks>
/// This is the root of the control hierarchy. For the device at the root, this
/// will point to itself (See <see cref="InputDevice.allControls"/>).
/// </remarks>
public InputDevice device => m_Device;
/// <summary>
/// The immediate parent of the control.
/// </summary>
/// <value>
/// The immediate parent of the control or null if the control has no parent
/// (which, once fully constructed, will only be the case for InputDevices).
/// See the related <see cref="children"/> field.
/// </value>
public InputControl parent => m_Parent;
/// <summary>
/// List of immediate child controls below this.
/// </summary>
/// <remarks>
/// Does not allocate.
/// See the related <see cref="parent"/> field.
/// </remarks>
public ReadOnlyArray<InputControl> children =>
new ReadOnlyArray<InputControl>(m_Device.m_ChildrenForEachControl, m_ChildStartIndex, m_ChildCount);
/// <summary>
/// List of usage tags associated with the control.
/// </summary>
/// <remarks>
/// Usages apply "semantics" to a control. Whereas the name of a control identifies a particular
/// "endpoint" within the control hierarchy, the usages of a control identify particular roles
/// of specific control. A simple example is <see cref="CommonUsages.Back"/> which identifies a
/// control generally used to move backwards in the navigation history of a UI. On a keyboard,
/// it is the escape key that generally fulfills this role whereas on a gamepad, it is generally
/// the "B" / "Circle" button. Some devices may not have a control that generally fulfills this
/// function and thus may not have any control with the "Back" usage.
///
/// By looking up controls by usage rather than by name, it is possible to locate the correct
/// control to use for certain standardized situation without having to know the particulars of
/// the device or platform.
///
/// Note that usages on devices work slightly differently than usages of controls on devices.
/// They are also queried through this property but unlike the usages of controls, the set of
/// usages of a device can be changed dynamically as the role of the device changes. For details,
/// see <see cref="InputSystem.SetDeviceUsage(InputDevice,string)"/>. Controls, on the other hand,
/// can currently only be assigned usages through layouts (<see cref="InputControlAttribute.usage"/>
/// or <see cref="InputControlAttribute.usages"/>).
///
/// <see cref="InputSystem.AddDeviceUsage(InputDevice,string)"/> can be used to add a device.
/// <see cref="InputSystem.RemoveDeviceUsage(InputDevice,string)"/> can be used to remove a device.
/// </remarks>
/// <example>
/// <code>
/// // Bind to any control which is tagged with the "Back" usage on any device.
/// var backAction = new InputAction(binding: "*/{Back}");
/// </code>
/// </example>
public ReadOnlyArray<InternedString> usages =>
new ReadOnlyArray<InternedString>(m_Device.m_UsagesForEachControl, m_UsageStartIndex, m_UsageCount);
/// <summary>
/// List of alternate names for the control.
/// </summary>
/// <value>
/// List of aliased alternate names for the control.
/// An example of an alias would be '<c>North</c>' for the '<c>Triangle</c>' button on a Playstation pad (or '<c>Y</c>' button on Xbox pad).
/// </value>
public ReadOnlyArray<InternedString> aliases =>
new ReadOnlyArray<InternedString>(m_Device.m_AliasesForEachControl, m_AliasStartIndex, m_AliasCount);
/// <summary>
/// Information about where the control stores its state, such as format, offset and size.
/// </summary>
public InputStateBlock stateBlock => m_StateBlock;
/// <summary>
/// Whether the control is considered noisy.
/// </summary>
/// <value>True if the control produces noisy input.</value>
/// <remarks>
/// A control is considered "noisy" if it produces different values without necessarily requiring user
/// interaction. A good example are sensors (see <see cref="Sensor"/>). For example, the PS4 controller
/// which has a gyroscope sensor built into the device. Whereas sticks and buttons on the device require
/// user interaction to produce non-default values, the gyro will produce varying values even if the
/// device just sits there without user interaction.
///
/// The value of this property is determined by the layout (<see cref="InputControlLayout"/>) that the
/// control has been built from.
///
/// Note that for devices (<see cref="InputDevice"/>) this property is true if any control on the device
/// is marked as noisy.
///
/// The primary effect of being noise is on <see cref="InputDevice.MakeCurrent"/> and
/// on interactive rebinding (see <see cref="InputActionRebindingExtensions.RebindingOperation"/>).
/// However, being noisy also affects automatic resetting of controls that happens when the application
/// loses focus. While other controls are reset to their default value (except if <c>Application.runInBackground</c>
/// is true and the device the control belongs to is marked as <see cref="InputDevice.canRunInBackground"/>),
/// noisy controls will not be reset but rather remain at their current value. This is based on the assumption
/// that noisy controls most often represent sensor values and snapping the last sampling value back to default
/// will usually have undesirable effects on an application's simulation logic.
/// </remarks>
/// <seealso cref="InputControlLayout.ControlItem.isNoisy"/>
/// <seealso cref="InputControlAttribute.noisy"/>
public bool noisy
{
get => (m_ControlFlags & ControlFlags.IsNoisy) != 0;
internal set
{
if (value)
{
m_ControlFlags |= ControlFlags.IsNoisy;
// Making a control noisy makes all its children noisy.
var list = children;
for (var i = 0; i < list.Count; ++i)
{
if (null != list[i])
list[i].noisy = true;
}
}
else
m_ControlFlags &= ~ControlFlags.IsNoisy;
}
}
/// <summary>
/// Whether the control is considered synthetic.
/// </summary>
/// <value>True if the control does not represent an actual physical control on the device.</value>
/// <remarks>
/// A control is considered "synthetic" if it does not correspond to an actual, physical control on the
/// device. An example for this is <see cref="Keyboard.anyKey"/> or the up/down/left/right buttons added
/// by <see cref="StickControl"/>.
///
/// The value of this property is determined by the layout (<see cref="InputControlLayout"/>) that the
/// control has been built from.
///
/// The primary effect of being synthetic is in interactive rebinding (see
/// <see cref="InputActionRebindingExtensions.RebindingOperation"/>) where non-synthetic
/// controls will be favored over synthetic ones. This means, for example, that if both
/// <c>"&lt;Gamepad&gt;/leftStick/x"</c> and <c>"&lt;Gamepad&gt;/leftStick/left"</c> are
/// suitable picks, <c>"&lt;Gamepad&gt;/leftStick/x"</c> will be favored as it represents
/// input from an actual physical control whereas <c>"&lt;Gamepad&gt;/leftStick/left"</c>
/// represents input from a made-up control. If, however, the "left" button is the only
/// viable pick, it will be accepted.
///
/// A control layout will specify if it is synthetic using <see cref="InputControlLayout.ControlItem.isSynthetic"/>.
/// See <see cref="InputControlAttribute.synthetic"/>.
/// </remarks>
public bool synthetic
{
get => (m_ControlFlags & ControlFlags.IsSynthetic) != 0;
internal set
{
if (value)
m_ControlFlags |= ControlFlags.IsSynthetic;
else
m_ControlFlags &= ~ControlFlags.IsSynthetic;
}
}
/// <summary>
/// Fetch a control from the control's hierarchy by name.
/// </summary>
/// <param name="path">A control path. See <see cref="InputControlPath"/>.</param>
/// <remarks>
/// Note that <see cref="path"/> matching is case-insensitive.
/// (see <see cref="InputControlPath"/>).
/// An alternative method is <see cref="TryGetChildControl"/>.
/// </remarks>
/// <example>
/// <code>
/// gamepad["leftStick"] // Returns Gamepad.leftStick
/// gamepad["leftStick/x"] // Returns Gamepad.leftStick.x
/// gamepad["{PrimaryAction}"] // Returns the control with PrimaryAction usage, that is, Gamepad.aButton
/// </code>
/// </example>
/// <exception cref="KeyNotFoundException"><paramref name="path"/> cannot be found.</exception>
public InputControl this[string path]
{
get
{
var control = InputControlPath.TryFindChild(this, path);
if (control == null)
throw new KeyNotFoundException(
$"Cannot find control '{path}' as child of '{this}'");
return control;
}
}
/// <summary>
/// Returns the underlying value type of this control.
/// </summary>
/// <value>Type of values produced by the control.</value>
/// <remarks>
/// This is the type of values that are returned when reading the current value of a control
/// or when reading a value of a control from an event with <see cref="ReadValueFromStateAsObject"/>.
/// The size can be determined with <see cref="valueSizeInBytes"/>.
/// </remarks>
public abstract Type valueType { get; }
/// <summary>
/// Size in bytes of values that the control returns.
/// </summary>
/// <remarks>
/// The type can be determined with <see cref="valueType"/>.
/// </remarks>
public abstract int valueSizeInBytes { get; }
/// <summary>
/// Compute an absolute, normalized magnitude value that indicates the extent to which the control
/// is actuated. Shortcut for <see cref="EvaluateMagnitude()"/>.
/// </summary>
/// <value>
/// Amount of actuation of the control or -1 if it cannot be determined.
/// </value>
public float magnitude => EvaluateMagnitude();
/// <summary>
/// Return a string representation of the control.
/// </summary>
/// <remarks>
/// Return a string representation of the control. Useful for debugging.
/// </remarks>
/// <returns>A string representation of the control.</returns>
public override string ToString()
{
return $"{layout}:{path}";
}
private string DebuggerDisplay()
{
// If the device hasn't been added, don't try to read the control's value.
if (!device.added)
return ToString();
// ReadValueAsObject might throw. Revert to just ToString() in that case.
try
{
return $"{layout}:{path}={this.ReadValueAsObject()}";
}
catch (Exception)
{
return ToString();
}
}
////REVIEW: The -1 behavior seems bad; probably better to just return 1 for controls that do not support finer levels of actuation
/// <summary>
/// Compute an absolute, normalized magnitude value that indicates the extent to which the control
/// is actuated.
/// </summary>
/// <returns>Amount of actuation of the control or -1 if it cannot be determined.</returns>
/// <remarks>
/// Magnitudes do not make sense for all types of controls. For example, for a control that represents
/// an enumeration of values (such as <see cref="TouchPhaseControl"/>), there is no meaningful
/// linear ordering of values (one could derive a linear ordering through the actual enum values but
/// their assignment may be entirely arbitrary; it is unclear whether a state of <see cref="TouchPhase.Canceled"/>
/// has a higher or lower "magnitude" as a state of <see cref="TouchPhase.Began"/>).
///
/// Controls that have no meaningful magnitude will return -1 when calling this method. Any negative
/// return value should be considered an invalid value.
/// </remarks>
/// <seealso cref="EvaluateMagnitude(void*)"/>
public unsafe float EvaluateMagnitude()
{
return EvaluateMagnitude(currentStatePtr);
}
/// <summary>
/// Compute an absolute, normalized magnitude value that indicates the extent to which the control
/// is actuated in the given state.
/// </summary>
/// <remarks>
/// Magnitudes do not make sense for all types of controls. For example, for a control that represents
/// an enumeration of values (such as <see cref="TouchPhaseControl"/>), there is no meaningful
/// linear ordering of values (one could derive a linear ordering through the actual enum values but
/// their assignment may be entirely arbitrary; it is unclear whether a state of <see cref="TouchPhase.Canceled"/>
/// has a higher or lower "magnitude" as a state of <see cref="TouchPhase.Began"/>).
///
/// Controls that have no meaningful magnitude will return -1 when calling this method. Any negative
/// return value should be considered an invalid value.
/// </remarks>
/// <param name="statePtr">State containing the control's <see cref="stateBlock"/>.</param>
/// <returns>Amount of actuation of the control or -1 if it cannot be determined.</returns>
/// <seealso cref="EvaluateMagnitude()"/>
public virtual unsafe float EvaluateMagnitude(void* statePtr)
{
return -1;
}
/// <summary>
/// Read the control's final, processed value from the given buffer and return the value as an object.
/// </summary>
/// <param name="buffer">Buffer to read the value from.</param>
/// <param name="bufferSize">Size of <paramref name="buffer"/> in bytes, which must be large enough to store the value.</param>
/// <returns>The control's value as stored in <paramref name="buffer"/>.</returns>
/// <remarks>
/// Read the control's final, processed value from the given buffer and return the value as an object.
///
/// This method allocates GC memory and should not be used during normal gameplay operation.
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="buffer"/> is null.</exception>
/// <exception cref="ArgumentException"><paramref name="bufferSize"/> is smaller than the size of the value to be read.</exception>
/// <seealso cref="ReadValueFromStateAsObject"/>
public abstract unsafe object ReadValueFromBufferAsObject(void* buffer, int bufferSize);
/// <summary>
/// Read the control's final, processed value from the given state and return the value as an object.
/// </summary>
/// <param name="statePtr">State to read the value for the control from.</param>
/// <returns>The control's value as stored in <paramref name="statePtr"/>.</returns>
/// <remarks>
/// Read the control's final, processed value from the given state and return the value as an object.
///
/// This method allocates GC memory and should not be used during normal gameplay operation.
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="statePtr"/> is null.</exception>
/// <seealso cref="ReadValueFromStateIntoBuffer"/>
public abstract unsafe object ReadValueFromStateAsObject(void* statePtr);
/// <summary>
/// Read the control's final, processed value from the given state and store it in the given buffer.
/// </summary>
/// <param name="statePtr">State to read the value for the control from.</param>
/// <param name="bufferPtr">Buffer to store the value in.</param>
/// <param name="bufferSize">Size of <paramref name="bufferPtr"/> in bytes. Must be at least <see cref="valueSizeInBytes"/>.
/// If it is smaller, <see cref="ArgumentException"/> will be thrown.</param>
/// <exception cref="ArgumentNullException"><paramref name="statePtr"/> is null, or <paramref name="bufferPtr"/> is null.</exception>
/// <exception cref="ArgumentException"><paramref name="bufferSize"/> is smaller than <see cref="valueSizeInBytes"/>.</exception>
/// <seealso cref="ReadValueFromStateAsObject"/>
/// <seealso cref="WriteValueFromBufferIntoState"/>
public abstract unsafe void ReadValueFromStateIntoBuffer(void* statePtr, void* bufferPtr, int bufferSize);
/// <summary>
/// Read a value from the given memory and store it as state.
/// </summary>
/// <param name="bufferPtr">Memory containing value, to store into the state.</param>
/// <param name="bufferSize">Size of <paramref name="bufferPtr"/> in bytes. Must be at least <see cref="valueSizeInBytes"/>.</param>
/// <param name="statePtr">State containing the control's <see cref="stateBlock"/>. Will receive the state
/// as converted from the given value.</param>
/// <remarks>
/// Writing values will NOT apply processors to the given value. This can mean that when reading a value
/// from a control after it has been written to its state, the resulting value differs from what was
/// written.
/// </remarks>
/// <exception cref="NotSupportedException">The control does not support writing. This is the case, for
/// example, that compute values (such as the magnitude of a vector).</exception>
/// <seealso cref="ReadValueFromStateIntoBuffer"/>
/// <seealso cref="WriteValueFromObjectIntoState"/>
public virtual unsafe void WriteValueFromBufferIntoState(void* bufferPtr, int bufferSize, void* statePtr)
{
throw new NotSupportedException(
$"Control '{this}' does not support writing");
}
/// <summary>
/// Read a value object and store it as state in the given memory.
/// </summary>
/// <param name="value">Value for the control to store in the state.</param>
/// <param name="statePtr">State containing the control's <see cref="stateBlock"/>. Will receive
/// the state as converted from the given value.</param>
/// <remarks>
/// Writing values will NOT apply processors to the given value. This can mean that when reading a value
/// from a control after it has been written to its state, the resulting value differs from what was
/// written.
/// </remarks>
/// <exception cref="NotSupportedException">The control does not support writing. This is the case, for
/// example, that compute values (such as the magnitude of a vector).</exception>
/// <seealso cref="WriteValueFromBufferIntoState"/>
public virtual unsafe void WriteValueFromObjectIntoState(object value, void* statePtr)
{
throw new NotSupportedException(
$"Control '{this}' does not support writing");
}
/// <summary>
/// Compare the value of the control as read from <paramref name="firstStatePtr"/> to that read from
/// <paramref name="secondStatePtr"/> and return true if they are equal.
/// </summary>
/// <param name="firstStatePtr">Memory containing the control's <see cref="stateBlock"/>.</param>
/// <param name="secondStatePtr">Memory containing the control's <see cref="stateBlock"/></param>
/// <returns>True if the value of the control is equal in both <paramref name="firstStatePtr"/> and
/// <paramref name="secondStatePtr"/>.</returns>
/// <remarks>
/// Unlike <see cref="CompareValue"/>, this method will have to do more than just compare the memory
/// for the control in the two state buffers. It will have to read out state for the control and run
/// the full processing machinery for the control to turn the state into a final, processed value.
/// CompareValue is thus more costly than <see cref="CompareValue"/>.
///
/// This method will apply epsilons (<see cref="Mathf.Epsilon"/>) when comparing floats.
/// </remarks>
/// <seealso cref="CompareValue"/>
public abstract unsafe bool CompareValue(void* firstStatePtr, void* secondStatePtr);
/// <summary>
/// Try to find a child control matching the given path.
/// </summary>
/// <param name="path">A control path. See <see cref="InputControlPath"/>.</param>
/// <returns>The first direct or indirect child control that matches the given <paramref name="path"/>
/// or null if no control was found to match.</returns>
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c> or empty.</exception>
/// <remarks>
/// Note that if the given path matches multiple child controls, only the first control
/// encountered in the search will be returned.
///
/// This method is equivalent to calling <see cref="InputControlPath.TryFindChild"/>.
/// </remarks>
/// <example>
/// <code>
/// // Returns the leftStick control of the current gamepad.
/// Gamepad.current.TryGetChildControl("leftStick");
///
/// // Returns the X axis control of the leftStick on the current gamepad.
/// Gamepad.current.TryGetChildControl("leftStick/x");
///
/// // Returns the first control ending with "stick" in its name. Note that it
/// // undetermined whether this is leftStick or rightStick (or even another stick
/// // added by the given gamepad).
/// Gamepad.current.TryGetChildControl("*stick");
/// </code>
/// </example>
public InputControl TryGetChildControl(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));
return InputControlPath.TryFindChild(this, path);
}
/// <summary>
/// Try to find a child control matching the given path.
/// </summary>
/// <param name="path">A control path. See <see cref="InputControlPath"/>.</param>
/// <typeparam name="TControl">The type of control to locate.</typeparam>
/// <returns>The first direct or indirect child control that matches the given <paramref name="path"/>
/// or null if no control was found to match.</returns>
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c> or empty.</exception>
/// <exception cref="InvalidOperationException">No control found with the specified type.</exception>
/// <remarks>
/// Note that if the given path matches multiple child controls, only the first control
/// encountered in the search will be returned.
/// </remarks>
public TControl TryGetChildControl<TControl>(string path)
where TControl : InputControl
{
if (string.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));
var control = TryGetChildControl(path);
if (control == null)
return null;
var controlOfType = control as TControl;
if (controlOfType == null)
throw new InvalidOperationException(
$"Expected control '{path}' to be of type '{typeof(TControl).Name}' but is of type '{control.GetType().Name}' instead!");
return controlOfType;
}
/// <summary>
/// Find a child control matching the given path.
/// </summary>
/// <param name="path">A control path. See <see cref="InputControlPath"/>.</param>
/// <returns>The first direct or indirect child control that matches the given <paramref name="path"/>
/// or null if no control was found to match.</returns>
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c> or empty.</exception>
/// <exception cref="InvalidOperationException">No control found with the specified type.</exception>
/// <exception cref="ArgumentException">The control cannot be found.</exception>
/// <remarks>
/// Note that if the given path matches multiple child controls, only the first control
/// encountered in the search will be returned.
/// </remarks>
public InputControl GetChildControl(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));
var control = TryGetChildControl(path);
if (control == null)
throw new ArgumentException($"Cannot find input control '{MakeChildPath(path)}'", nameof(path));
return control;
}
/// <summary>
/// Find a child control matching the given path.
/// </summary>
/// <param name="path">A control path. See <see cref="InputControlPath"/>.</param>
/// <typeparam name="TControl">The type of control to locate.</typeparam>
/// <returns>The first direct or indirect child control that matches the given <paramref name="path"/>
/// or null if no control was found to match.</returns>
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c> or empty.</exception>
/// <exception cref="InvalidOperationException">No control found with the specified type.</exception>
/// <exception cref="ArgumentException">The control cannot be found.</exception>
/// <remarks>
/// Note that if the given path matches multiple child controls, only the first control
/// encountered in the search will be returned.
/// </remarks>
public TControl GetChildControl<TControl>(string path)
where TControl : InputControl
{
var control = GetChildControl(path);
if (!(control is TControl controlOfType))
throw new ArgumentException(
$"Expected control '{path}' to be of type '{typeof(TControl).Name}' but is of type '{control.GetType().Name}' instead!", nameof(path));
return controlOfType;
}
/// <summary>
/// Constructor for the InputControl
/// </summary>
/// <remarks>
/// Constructor for the InputControl
/// </remarks>
protected InputControl()
{
// Set defaults for state block setup. Subclasses may override.
m_StateBlock.byteOffset = InputStateBlock.AutomaticOffset; // Request automatic layout by default.
}
/// <summary>
/// Perform final initialization tasks after the control hierarchy has been put into place.
/// </summary>
/// <remarks>
/// This method can be overridden to perform control- or device-specific setup work. The most
/// common use case is for looking up child controls and storing them in local getters.
/// </remarks>
/// <example>
/// <code>
/// public class MyDevice : InputDevice
/// {
/// public ButtonControl button { get; private set; }
/// public AxisControl axis { get; private set; }
///
/// protected override void OnFinishSetup()
/// {
/// // Cache controls in getters.
/// button = GetChildControl("button");
/// axis = GetChildControl("axis");
/// }
/// }
/// </code>
/// </example>
protected virtual void FinishSetup()
{
}
/// <summary>
/// Call <see cref="RefreshConfiguration"/> if the configuration has in the interim been invalidated
/// by a <see cref="DeviceConfigurationEvent"/>.
/// </summary>
/// <remarks>
/// This method is only relevant if you are implementing your own devices or new
/// types of controls which are fetching configuration data from the devices (such
/// as <see cref="KeyControl"/> which is fetching display names for individual keys
/// from the underlying platform).
///
/// This method should be called if you are accessing cached data set up by
/// <see cref="RefreshConfiguration"/>.
/// </remarks>
/// <example>
/// <code>
/// using UnityEngine.InputSystem;
/// using UnityEngine.InputSystem.Utilities;
///
/// // Let's say your device has an associated orientation which it can be held with
/// // and you want to surface both as a property and as a usage on the device.
/// // Whenever your backend code detects a change in orientation, it should send
/// // a DeviceConfigurationEvent to your device to signal that the configuration
/// // of the device has changed. You can then implement RefreshConfiguration() to
/// // read out and update the device orientation on the managed InputDevice instance.
/// public class MyDevice : InputDevice
/// {
/// public enum Orientation
/// {
/// Horizontal,
/// Vertical,
/// }
///
/// private Orientation m_Orientation;
/// public Orientation orientation
/// {
/// get
/// {
/// // Call RefreshOrientation if the configuration of the device has been
/// // invalidated since last time we initialized m_Orientation.
/// RefreshConfigurationIfNeeded();
/// return m_Orientation;
/// }
/// }
/// protected override void RefreshConfiguration()
/// {
/// // Fetch the current orientation from the backend. How you do this
/// // depends on your device. Using DeviceCommands is one way.
/// var fetchOrientationCommand = new FetchOrientationCommand();
/// ExecuteCommand(ref fetchOrientationCommand);
/// m_Orientation = fetchOrientation;
///
/// // Reflect the orientation on the device.
/// switch (m_Orientation)
/// {
/// case Orientation.Vertical:
/// InputSystem.RemoveDeviceUsage(this, s_Horizontal);
/// InputSystem.AddDeviceUsage(this, s_Vertical);
/// break;
///
/// case Orientation.Horizontal:
/// InputSystem.RemoveDeviceUsage(this, s_Vertical);
/// InputSystem.AddDeviceUsage(this, s_Horizontal);
/// break;
/// }
/// }
///
/// private static InternedString s_Vertical = new InternedString("Vertical");
/// private static InternedString s_Horizontal = new InternedString("Horizontal");
/// }
/// </code>
/// </example>
/// <seealso cref="RefreshConfiguration"/>
protected void RefreshConfigurationIfNeeded()
{
if (!isConfigUpToDate)
{
RefreshConfiguration();
isConfigUpToDate = true;
}
}
/// <summary>
/// Refresh the configuration of the control. This is used to update the control's state (e.g. Keyboard Layout or display Name of Keys).
/// </summary>
/// <remarks>
/// The system will call this method automatically whenever a change is made to one of the control's configuration properties.
/// This method is only relevant if you are implementing your own devices or new
/// types of controls which are fetching configuration data from the devices (such
/// as <see cref="KeyControl"/> which is fetching display names for individual keys
/// from the underlying platform).
/// See <see cref="RefreshConfigurationIfNeeded"/>.
/// </remarks>
/// <example>
/// <code>
/// using UnityEngine.InputSystem;
/// using UnityEngine.InputSystem.Utilities;
///
/// public class MyDevice : InputDevice
/// {
/// public enum Orientation
/// {
/// Horizontal,
/// Vertical,
/// }
/// private Orientation m_Orientation;
/// private static InternedString s_Vertical = new InternedString("Vertical");
/// private static InternedString s_Horizontal = new InternedString("Horizontal");
///
/// public Orientation orientation
/// {
/// get
/// {
/// // Call RefreshOrientation if the configuration of the device has been
/// // invalidated since last time we initialized m_Orientation.
/// // Calling RefreshConfigurationIfNeeded() is sufficient in most cases, RefreshConfiguration() forces the refresh.
/// RefreshConfiguration();
/// return m_Orientation;
/// }
/// }
/// protected override void RefreshConfiguration()
/// {
/// // Set Orientation back to horizontal. Alternatively fetch from device.
/// m_Orientation = Orientation.Horizontal;
/// // Reflect the orientation on the device.
/// switch (m_Orientation)
/// {
/// case Orientation.Vertical:
/// InputSystem.RemoveDeviceUsage(this, s_Horizontal);
/// InputSystem.AddDeviceUsage(this, s_Vertical);
/// break;
///
/// case Orientation.Horizontal:
/// InputSystem.RemoveDeviceUsage(this, s_Vertical);
/// InputSystem.AddDeviceUsage(this, s_Horizontal);
/// break;
/// }
/// }
/// }
/// </code>
/// </example>
protected virtual void RefreshConfiguration()
{
}
////TODO: drop protected access
/// <summary>
/// Information about a memory region storing input state.
/// </summary>
protected internal InputStateBlock m_StateBlock;
////REVIEW: shouldn't these sit on the device?
/// <summary>
/// The state data buffer for the device.
/// </summary>
/// <value>
/// The state data buffer for the device.
/// </value>
protected internal unsafe void* currentStatePtr => InputStateBuffers.GetFrontBufferForDevice(GetDeviceIndex());
/// <summary>
/// The state data buffer for the device from the previous frame.
/// </summary>
/// <value>
/// The state data buffer for the device from the previous frame.
/// </value>
protected internal unsafe void* previousFrameStatePtr => InputStateBuffers.GetBackBufferForDevice(GetDeviceIndex());
/// <summary>
/// The default state data buffer
/// </summary>
/// <value>
/// Buffer that has state for each device initialized with default values.
/// </value>
protected internal unsafe void* defaultStatePtr => InputStateBuffers.s_DefaultStateBuffer;
/// <summary>
/// Return the memory that holds the noise mask for the control.
/// </summary>
/// <value>Noise bit mask for the control.</value>
/// <remarks>
/// Like with all state blocks, the specific memory block for the control is found at the memory
/// region specified by <see cref="stateBlock"/>.
///
/// The noise mask can be overlaid as a bit mask over the state for the control. When doing so, all state
/// that is noise will be masked out whereas all state that isn't will come through unmodified. In other words,
/// any bit that is set in the noise mask indicates that the corresponding bit in the control's state memory
/// is noise.
///
/// A control can be marked as <see cref="noisy"/>.
/// </remarks>
protected internal unsafe void* noiseMaskPtr => InputStateBuffers.s_NoiseMaskBuffer;
/// <summary>
/// The offset of this control's state relative to its device root.
/// </summary>
/// <remarks>
/// Once a device has been added to the system, its state block will get allocated
/// in the global state buffers and the offset of the device's state block will
/// get baked into all the controls on the device. This property always returns
/// the "unbaked" offset.
/// </remarks>
protected internal uint stateOffsetRelativeToDeviceRoot
{
get
{
var deviceStateOffset = device.m_StateBlock.byteOffset;
Debug.Assert(deviceStateOffset <= m_StateBlock.byteOffset);
return m_StateBlock.byteOffset - deviceStateOffset;
}
}
// This data is initialized by InputDeviceBuilder.
internal InternedString m_Name;
internal string m_Path;
internal string m_DisplayName; // Display name set by the control itself (may be null).
internal string m_DisplayNameFromLayout; // Display name coming from layout (may be null).
internal string m_ShortDisplayName; // Short display name set by the control itself (may be null).
internal string m_ShortDisplayNameFromLayout; // Short display name coming from layout (may be null).
internal InternedString m_Layout;
internal InternedString m_Variants;
internal InputDevice m_Device;
internal InputControl m_Parent;
internal int m_UsageCount;
internal int m_UsageStartIndex;
internal int m_AliasCount;
internal int m_AliasStartIndex;
internal int m_ChildCount;
internal int m_ChildStartIndex;
internal ControlFlags m_ControlFlags;
// Value caching
// These values will be set to true during state updates if the control has actually changed value.
// Set to true initially so default state will be returned on the first call
internal bool m_CachedValueIsStale = true;
internal bool m_UnprocessedCachedValueIsStale = true;
////REVIEW: store these in arrays in InputDevice instead?
internal PrimitiveValue m_DefaultState;
internal PrimitiveValue m_MinValue;
internal PrimitiveValue m_MaxValue;
internal FourCC m_OptimizedControlDataType;
/// <summary>
/// The type of the state memory associated with the control.
/// </summary>
/// <remarks>
/// For some types of control you can safely read/write state memory directly
/// which is much faster than calling ReadUnprocessedValueFromState/WriteValueIntoState.
/// This method returns a type that you can use for reading/writing the control directly,
/// or it returns <see cref="InputStateBlock.FormatInvalid"/> if it's not possible for this type of control.
///
/// For example, AxisControl <see cref="valueType"/> might be a "float" in state memory, and if no processing is applied during reading (e.g. no invert/scale/etc),
/// then you could read it as float in memory directly without calling ReadUnprocessedValueFromState, which is faster.
/// Additionally, if you have a Vector3Control which uses 3 AxisControls as consecutive floats in memory,
/// you can cast the Vector3Control state memory directly to Vector3 without calling ReadUnprocessedValueFromState on x/y/z axes.
///
/// The value returned for any given control is computed automatically by the Input System, when the control's setup configuration changes. <see cref="InputControl.CalculateOptimizedControlDataType"/>
/// There are some parameter changes which don't trigger a configuration change (such as the clamp, invert, normalize, and scale parameters on AxisControl),
/// so if you modify these, the optimized data type is not automatically updated. In this situation, you should manually update it by calling <see cref="InputControl.ApplyParameterChanges"/>.
/// </remarks>
public FourCC optimizedControlDataType => m_OptimizedControlDataType;
/// <summary>
/// Calculates and returns an optimized data type that can represent a control's value in memory directly.
/// </summary>
/// <remarks>
/// The value then is cached in <see cref="InputControl.optimizedControlDataType"/>.
/// This method is for internal use only, you should not call this from your own code.
/// </remarks>
/// <returns>
/// An optimized data type that can represent a control's value in memory directly. <see cref="InputStateBlock"/>
/// </returns>
protected virtual FourCC CalculateOptimizedControlDataType()
{
return InputStateBlock.kFormatInvalid;
}
/// <summary>
/// Apply built-in parameters changes.
/// </summary>
/// <remarks>
/// Apply built-in parameters changes (e.g. <see cref="AxisControl.invert"/>, others).
/// Recompute <see cref="InputControl.optimizedControlDataType"/> for impacted controls and clear cached value
/// </remarks>
/// <example>
/// <code>
/// Gamepad.all[0].leftTrigger.WriteValueIntoState(0.5f, Gamepad.all[0].currentStatePtr);
/// Gamepad.all[0].ApplyParameterChanges();
/// </code>
/// </example>
public void ApplyParameterChanges()
{
// First we go through all children of our own hierarchy
SetOptimizedControlDataTypeRecursively();
// Then we go through all parents up to the root, because our own change might influence their optimization status
// e.g. let's say we have a tree where root is Vector3 and children are three AxisControl
// And user is calling this method on AxisControl which goes from Float to NotOptimized.
// Then we need to also transition Vector3 to NotOptimized as well.
var currentParent = parent;
while (currentParent != null)
{
currentParent.SetOptimizedControlDataType();
currentParent = currentParent.parent;
}
// Also use this method to mark cached values as stale
MarkAsStaleRecursively();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetOptimizedControlDataType()
{
// setting check need to be inline so we clear optimizations if setting is disabled after the fact
m_OptimizedControlDataType = InputSystem.s_Manager.optimizedControlsFeatureEnabled
? CalculateOptimizedControlDataType()
: (FourCC)InputStateBlock.kFormatInvalid;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void SetOptimizedControlDataTypeRecursively()
{
// Need to go depth-first because CalculateOptimizedControlDataType might depend on computed values of children
if (m_ChildCount > 0)
{
foreach (var inputControl in children)
inputControl.SetOptimizedControlDataTypeRecursively();
}
SetOptimizedControlDataType();
}
// This function exists to warn users to start using ApplyParameterChanges for edge cases that were previously not intentionally supported,
// where control properties suddenly change underneath us without us anticipating that.
// This is mainly to AxisControl fields being public and capable of changing at any time even if we were not anticipated such a usage pattern.
// Also it's not clear if InputControl.stateBlock.format can potentially change at any time, likely not.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
// Only do this check in and editor in hope that it will be sufficient to catch any misuse during development.
// It is not done in debug builds because it has a performance cost and it will show up when profiled.
[Conditional("UNITY_EDITOR")]
internal void EnsureOptimizationTypeHasNotChanged()
{
if (!InputSystem.s_Manager.optimizedControlsFeatureEnabled)
return;
var currentOptimizedControlDataType = CalculateOptimizedControlDataType();
if (currentOptimizedControlDataType != optimizedControlDataType)
{
Debug.LogError(
$"Control '{name}' / '{path}' suddenly changed optimization state due to either format " +
$"change or control parameters change (was '{optimizedControlDataType}' but became '{currentOptimizedControlDataType}'), " +
"this hinders control hot path optimization, please call control.ApplyParameterChanges() " +
"after the changes to the control to fix this error.");
// Automatically fix the issue
// Note this function is only executed in the editor
m_OptimizedControlDataType = currentOptimizedControlDataType;
}
if (m_ChildCount > 0)
{
foreach (var inputControl in children)
inputControl.EnsureOptimizationTypeHasNotChanged();
}
}
[Flags]
internal enum ControlFlags
{
ConfigUpToDate = 1 << 0,
IsNoisy = 1 << 1,
IsSynthetic = 1 << 2,
IsButton = 1 << 3,
DontReset = 1 << 4,
SetupFinished = 1 << 5, // Can't be modified once this is set.
UsesStateFromOtherControl = 1 << 6,
}
internal bool isSetupFinished
{
get => (m_ControlFlags & ControlFlags.SetupFinished) == ControlFlags.SetupFinished;
set
{
if (value)
m_ControlFlags |= ControlFlags.SetupFinished;
else
m_ControlFlags &= ~ControlFlags.SetupFinished;
}
}
internal bool isButton
{
get => (m_ControlFlags & ControlFlags.IsButton) == ControlFlags.IsButton;
set
{
if (value)
m_ControlFlags |= ControlFlags.IsButton;
else
m_ControlFlags &= ~ControlFlags.IsButton;
}
}
internal bool isConfigUpToDate
{
get => (m_ControlFlags & ControlFlags.ConfigUpToDate) == ControlFlags.ConfigUpToDate;
set
{
if (value)
m_ControlFlags |= ControlFlags.ConfigUpToDate;
else
m_ControlFlags &= ~ControlFlags.ConfigUpToDate;
}
}
internal bool dontReset
{
get => (m_ControlFlags & ControlFlags.DontReset) == ControlFlags.DontReset;
set
{
if (value)
m_ControlFlags |= ControlFlags.DontReset;
else
m_ControlFlags &= ~ControlFlags.DontReset;
}
}
internal bool usesStateFromOtherControl
{
get => (m_ControlFlags & ControlFlags.UsesStateFromOtherControl) == ControlFlags.UsesStateFromOtherControl;
set
{
if (value)
m_ControlFlags |= ControlFlags.UsesStateFromOtherControl;
else
m_ControlFlags &= ~ControlFlags.UsesStateFromOtherControl;
}
}
internal bool hasDefaultState => !m_DefaultState.isEmpty;
// This method exists only to not slap the internal interaction on all overrides of
// FinishSetup().
internal void CallFinishSetupRecursive()
{
var list = children;
for (var i = 0; i < list.Count; ++i)
list[i].CallFinishSetupRecursive();
FinishSetup();
SetOptimizedControlDataTypeRecursively();
}
internal string MakeChildPath(string path)
{
if (this is InputDevice)
return path;
return $"{this.path}/{path}";
}
internal void BakeOffsetIntoStateBlockRecursive(uint offset)
{
m_StateBlock.byteOffset += offset;
var list = children;
for (var i = 0; i < list.Count; ++i)
list[i].BakeOffsetIntoStateBlockRecursive(offset);
}
internal int GetDeviceIndex()
{
var deviceIndex = m_Device.m_DeviceIndex;
if (deviceIndex == InputDevice.kInvalidDeviceIndex)
throw new InvalidOperationException(
$"Cannot query value of control '{path}' before '{device.name}' has been added to system!");
return deviceIndex;
}
internal bool IsValueConsideredPressed(float value)
{
if (isButton)
return ((ButtonControl)this).IsValueConsideredPressed(value);
return value >= ButtonControl.s_GlobalDefaultButtonPressPoint;
}
internal virtual void AddProcessor(object first)
{
}
internal void MarkAsStale()
{
m_CachedValueIsStale = true;
m_UnprocessedCachedValueIsStale = true;
}
internal void MarkAsStaleRecursively()
{
MarkAsStale();
foreach (var inputControl in children)
{
inputControl.MarkAsStale();
if (inputControl is ButtonControl buttonControl)
{
// If everything is becoming stale, update all press states so we can reevaluate
buttonControl.UpdateWasPressed();
#if UNITY_EDITOR
buttonControl.UpdateWasPressedEditor();
#endif
}
}
}
#if UNITY_EDITOR
internal virtual IEnumerable<object> GetProcessors()
{
yield return null;
}
#endif
}
/// <summary>
/// Base class for input controls with a specific value type.
/// </summary>
/// <typeparam name="TValue">Type of value captured by the control. Note that this does not mean
/// that the control has to store data in the given value format. A control that captures float
/// values, for example, may be stored in state as byte values instead.</typeparam>
public abstract class InputControl<TValue> : InputControl
where TValue : struct
{
/// <inheritdoc/>
public override Type valueType => typeof(TValue);
/// <inheritdoc/>
public override int valueSizeInBytes => UnsafeUtility.SizeOf<TValue>();
/// <summary>
/// Returns the current value of the control after processors have been applied.
/// </summary>
/// <value>The controls current value.</value>
/// <remarks>
/// This can only be called on devices that have been added to the system (<see cref="InputDevice.added"/>).
///
/// If internal feature "USE_READ_VALUE_CACHING" is enabled, then this property implements caching
/// to avoid applying processors when the underlying control has not changed.
/// With this in mind, be aware of processors that use global state, such as the <see cref="Processors.AxisDeadzoneProcessor"/>.
/// Unless the control unprocessed value has been changed, input system settings changed or <see cref="InputControl.ApplyParameterChanges()"/> invoked,
/// the processors will not run and calls to <see cref="value"/> will return the same result as previous calls.
///
/// If a processor requires to be run on every read, override <see cref="InputProcessor.cachingPolicy"/> property
/// in the processor and set it to <see cref="InputProcessor.CachingPolicy.EvaluateOnEveryRead"/>.
///
/// To improve debugging try setting "PARANOID_READ_VALUE_CACHING_CHECKS" internal feature flag to check if cache value is still consistent.
///
/// Also note that this property returns the result as ref readonly. If custom control states are in use, i.e.
/// any controls not shipped with the Input System package, be careful of accidental defensive copies
/// <a href="https://docs.microsoft.com/en-us/dotnet/csharp/write-safe-efficient-code#avoid-defensive-copies">https://docs.microsoft.com/en-us/dotnet/csharp/write-safe-efficient-code#avoid-defensive-copies</a>.
/// </remarks>
/// <seealso cref="ReadValue"/>
public ref readonly TValue value
{
get
{
#if UNITY_EDITOR
if (InputUpdate.s_LatestUpdateType.IsEditorUpdate())
return ref ReadStateInEditor();
#endif
if (
// if feature is disabled we re-evaluate every call
!InputSystem.s_Manager.readValueCachingFeatureEnabled
// if cached value is stale we re-evaluate and clear the flag
|| m_CachedValueIsStale
// if a processor in stack needs to be re-evaluated, but unprocessedValue is still can be cached
|| evaluateProcessorsEveryRead
)
{
m_CachedValue = ProcessValue(unprocessedValue);
m_CachedValueIsStale = false;
}
#if DEBUG
else if (InputSystem.s_Manager.paranoidReadValueCachingChecksEnabled)
{
var oldUnprocessedValue = m_UnprocessedCachedValue;
var newUnprocessedValue = unprocessedValue;
var currentProcessedValue = ProcessValue(newUnprocessedValue);
if (CompareValue(ref newUnprocessedValue, ref oldUnprocessedValue))
{
// don't warn if unprocessedValue caching failed
m_CachedValue = currentProcessedValue;
}
else if (CompareValue(ref currentProcessedValue, ref m_CachedValue))
{
// processors are not behaving as expected if unprocessedValue stays the same but processedValue changed
var namesList = new List<string>();
foreach (var inputProcessor in m_ProcessorStack)
namesList.Add(inputProcessor.ToString());
var names = string.Join(", ", namesList);
Debug.LogError(
"Cached processed value unexpectedly became outdated due to InputProcessor's returning a different value, " +
$"new value '{currentProcessedValue}' old value '{m_CachedValue}', current processors are: {names}. " +
"If your processor need to be recomputed on every read please add \"public override CachingPolicy cachingPolicy => CachingPolicy.EvaluateOnEveryRead;\" to the processor.");
m_CachedValue = currentProcessedValue;
}
}
#endif
return ref m_CachedValue;
}
}
internal unsafe ref readonly TValue unprocessedValue
{
get
{
#if UNITY_EDITOR
if (InputUpdate.s_LatestUpdateType.IsEditorUpdate())
return ref ReadUnprocessedStateInEditor();
#endif
// Case ISXB-606
// If an object reference has the underlying object deleted then a device can go
// away which means that the underlying state buffers will have been resized.
//
// The currentStatePtr accessor uses GetDeviceIndex() to index into the state
// buffers but this index can then be out of bounds.
//
// InputStateBuffers.Get{Front,Back}Buffer() now check for the requested index being
// in-bounds and return null if not - check that here to avoid null derefence later.
//
if (currentStatePtr == null)
{
return ref m_UnprocessedCachedValue;
}
if (
// if feature is disabled we re-evaluate every call
!InputSystem.s_Manager.readValueCachingFeatureEnabled
// if cached value is stale we re-evaluate and clear the flag
|| m_UnprocessedCachedValueIsStale
)
{
m_UnprocessedCachedValue = ReadUnprocessedValueFromState(currentStatePtr);
m_UnprocessedCachedValueIsStale = false;
}
#if DEBUG
else if (InputSystem.s_Manager.paranoidReadValueCachingChecksEnabled)
{
var currentUnprocessedValue = ReadUnprocessedValueFromState(currentStatePtr);
if (CompareValue(ref currentUnprocessedValue, ref m_UnprocessedCachedValue))
{
Debug.LogError($"Cached unprocessed value unexpectedly became outdated for unknown reason, new value '{currentUnprocessedValue}' old value '{m_UnprocessedCachedValue}'.");
m_UnprocessedCachedValue = currentUnprocessedValue;
}
}
#endif
return ref m_UnprocessedCachedValue;
}
}
/// <summary>
/// Returns the current value of the control after processors have been applied.
/// </summary>
/// <returns>The controls current value.</returns>
/// <remarks>
/// This can only be called on devices that have been added to the system (<see cref="InputDevice.added"/>).
///
/// If internal feature "USE_READ_VALUE_CACHING" is enabled, then this property implements caching
/// to avoid applying processors when the underlying control has not changed.
/// With this in mind, be aware of processors that use global state, such as the <see cref="Processors.AxisDeadzoneProcessor"/>.
/// Unless the control unprocessed value has been changed, input system settings changed or <see cref="InputControl.ApplyParameterChanges()"/> invoked,
/// the processors will not run and calls to <see cref="value"/> will return the same result as previous calls.
///
/// If a processor requires to be run on every read, override <see cref="InputProcessor.cachingPolicy"/> property
/// in the processor and set it to <see cref="InputProcessor.CachingPolicy.EvaluateOnEveryRead"/>.
///
/// To improve debugging try setting "PARANOID_READ_VALUE_CACHING_CHECKS" internal feature flag to check if cache value is still consistent.
/// <a href="https://docs.microsoft.com/en-us/dotnet/csharp/write-safe-efficient-code#avoid-defensive-copies">https://docs.microsoft.com/en-us/dotnet/csharp/write-safe-efficient-code#avoid-defensive-copies</a>.
/// </remarks>
/// <seealso cref="value"/>
public TValue ReadValue()
{
return value;
}
////REVIEW: is 'frame' really the best wording here?
/// <summary>
/// Get the control's value from the previous frame (<see cref="InputControl.previousFrameStatePtr"/>).
/// </summary>
/// <returns>The control's value in the previous frame.</returns>
public TValue ReadValueFromPreviousFrame()
{
unsafe
{
return ReadValueFromState(previousFrameStatePtr);
}
}
/// <summary>
/// Get the control's default value.
/// </summary>
/// <returns>The control's default value.</returns>
/// <remarks>
/// This is not necessarily equivalent to <c>default(TValue)</c>. A control's default value is determined
/// by reading its value from the default state (<see cref="InputControl.defaultStatePtr"/>) which in turn
/// is determined from settings in the control's registered layout (<see cref="InputControlLayout.ControlItem.defaultState"/>).
/// </remarks>
public TValue ReadDefaultValue()
{
unsafe
{
return ReadValueFromState(defaultStatePtr);
}
}
/// <summary>
/// Get the control's default value.
/// </summary>
/// <param name="statePtr">State containing the control's <see cref="InputControl.stateBlock"/>.</param>
/// <returns>The control's default value.</returns>
/// <remarks>
/// This is not necessarily equivalent to <c>default(TValue)</c>. A control's default value is determined
/// by reading its value from the default state (<see cref="InputControl.defaultStatePtr"/>) which in turn
/// is determined from settings in the control's registered layout (<see cref="InputControlLayout.ControlItem.defaultState"/>).
/// </remarks>
public unsafe TValue ReadValueFromState(void* statePtr)
{
if (statePtr == null)
throw new ArgumentNullException(nameof(statePtr));
return ProcessValue(ReadUnprocessedValueFromState(statePtr));
}
/// <summary>
/// Read value from provided <paramref name="statePtr"/> and apply processors. Try cache result if possible.
/// </summary>
/// <param name="statePtr">State pointer to read from.</param>
/// <returns>The controls current value.</returns>
/// <remarks>
/// If <paramref name="statePtr"/> is "currentStatePtr", then read will be done via <see cref="value"/> property to improve performance.
/// </remarks>
/// <seealso cref="value"/>
public unsafe TValue ReadValueFromStateWithCaching(void* statePtr)
{
return statePtr == currentStatePtr ? value : ReadValueFromState(statePtr);
}
/// <summary>
/// Read value from provided <paramref name="statePtr"/>. Try cache result if possible.
/// </summary>
/// <param name="statePtr">State pointer to read from.</param>
/// <returns>The controls current value.</returns>
/// <remarks>
/// If <paramref name="statePtr"/> is "currentStatePtr", then read will be done via <see cref="unprocessedValue"/> property to improve performance.
/// </remarks>
/// <seealso cref="value"/>
public unsafe TValue ReadUnprocessedValueFromStateWithCaching(void* statePtr)
{
return statePtr == currentStatePtr ? unprocessedValue : ReadUnprocessedValueFromState(statePtr);
}
/// <summary>
/// Read value from control
/// </summary>
/// <returns>The controls current value.</returns>
/// <remarks>
/// This is the locally cached value. Use <see cref="ReadUnprocessedValueFromStateWithCaching"/>to get the uncached version.
/// </remarks>
/// <seealso cref="value"/>
public TValue ReadUnprocessedValue()
{
return unprocessedValue;
}
/// <summary>
/// Read value from provided <paramref name="statePtr"/>.
/// </summary>
/// <param name="statePtr">State pointer to read from.</param>
/// <returns>The controls current value.</returns>
/// <remarks>
/// Read value from provided <paramref name="statePtr"/> without any caching.
/// </remarks>
/// <seealso cref="value"/>
public abstract unsafe TValue ReadUnprocessedValueFromState(void* statePtr);
/// <inheritdoc />
public override unsafe object ReadValueFromStateAsObject(void* statePtr)
{
return ReadValueFromState(statePtr);
}
/// <inheritdoc />
public override unsafe void ReadValueFromStateIntoBuffer(void* statePtr, void* bufferPtr, int bufferSize)
{
if (statePtr == null)
throw new ArgumentNullException(nameof(statePtr));
if (bufferPtr == null)
throw new ArgumentNullException(nameof(bufferPtr));
var numBytes = UnsafeUtility.SizeOf<TValue>();
if (bufferSize < numBytes)
throw new ArgumentException(
$"bufferSize={bufferSize} < sizeof(TValue)={numBytes}", nameof(bufferSize));
var value = ReadValueFromState(statePtr);
var valuePtr = UnsafeUtility.AddressOf(ref value);
UnsafeUtility.MemCpy(bufferPtr, valuePtr, numBytes);
}
/// <inheritdoc />
public override unsafe void WriteValueFromBufferIntoState(void* bufferPtr, int bufferSize, void* statePtr)
{
if (bufferPtr == null)
throw new ArgumentNullException(nameof(bufferPtr));
if (statePtr == null)
throw new ArgumentNullException(nameof(statePtr));
var numBytes = UnsafeUtility.SizeOf<TValue>();
if (bufferSize < numBytes)
throw new ArgumentException(
$"bufferSize={bufferSize} < sizeof(TValue)={numBytes}", nameof(bufferSize));
// C# won't let us use a pointer to a generically defined type. Work
// around this by using UnsafeUtility.
var value = default(TValue);
var valuePtr = UnsafeUtility.AddressOf(ref value);
UnsafeUtility.MemCpy(valuePtr, bufferPtr, numBytes);
WriteValueIntoState(value, statePtr);
}
/// <inheritdoc />
public override unsafe void WriteValueFromObjectIntoState(object value, void* statePtr)
{
if (statePtr == null)
throw new ArgumentNullException(nameof(statePtr));
if (value == null)
throw new ArgumentNullException(nameof(value));
// If value is not of expected type, try to convert.
if (!(value is TValue))
value = Convert.ChangeType(value, typeof(TValue));
var valueOfType = (TValue)value;
WriteValueIntoState(valueOfType, statePtr);
}
/// <summary>
/// Write a value into state at the given memory.
/// </summary>
/// <param name="value">Value for the control to store in the state.</param>
/// <param name="statePtr">State containing the control's <see cref="InputControl.stateBlock"/>. Will receive
/// the state as converted from the given value.</param>
/// <remarks>
/// Writing values will NOT apply processors to the given value. This can mean that when reading a value
/// from a control after it has been written to its state, the resulting value differs from what was
/// written.
/// </remarks>
/// <exception cref="NotSupportedException">The control does not support writing. This is the case, for
/// example, that compute values (such as the magnitude of a vector).</exception>
/// <seealso cref="WriteValueFromBufferIntoState"/>
public virtual unsafe void WriteValueIntoState(TValue value, void* statePtr)
{
////REVIEW: should we be able to even tell from layouts which controls support writing and which don't?
throw new NotSupportedException(
$"Control '{this}' does not support writing");
}
/// <inheritdoc />
public override unsafe object ReadValueFromBufferAsObject(void* buffer, int bufferSize)
{
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
var valueSize = UnsafeUtility.SizeOf<TValue>();
if (bufferSize < valueSize)
throw new ArgumentException(
$"Expecting buffer of at least {valueSize} bytes for value of type {typeof(TValue).Name} but got buffer of only {bufferSize} bytes instead",
nameof(bufferSize));
var value = default(TValue);
var valuePtr = UnsafeUtility.AddressOf(ref value);
UnsafeUtility.MemCpy(valuePtr, buffer, valueSize);
return value;
}
private static unsafe bool CompareValue(ref TValue firstValue, ref TValue secondValue)
{
var firstValuePtr = UnsafeUtility.AddressOf(ref firstValue);
var secondValuePtr = UnsafeUtility.AddressOf(ref secondValue);
// NOTE: We're comparing raw memory of processed values here (which are guaranteed to be structs or
// primitives), not state. Means we don't have to take bits into account here.
return UnsafeUtility.MemCmp(firstValuePtr, secondValuePtr, UnsafeUtility.SizeOf<TValue>()) != 0;
}
/// <summary>
/// Compared values in state buffers.
/// </summary>
/// <param name="firstStatePtr">The first state buffer to read value from.</param>
/// <param name="secondStatePtr">The second state buffer to read value from.</param>
/// <returns>True if the buffer values match. False if they differ.</returns>
public override unsafe bool CompareValue(void* firstStatePtr, void* secondStatePtr)
{
////REVIEW: should we first compare state here? if there's no change in state, there can be no change in value and we can skip the rest
var firstValue = ReadValueFromState(firstStatePtr);
var secondValue = ReadValueFromState(secondStatePtr);
return CompareValue(ref firstValue, ref secondValue);
}
/// <summary>
/// Applies all control processors to the passed value.
/// </summary>
/// <param name="value">value to run processors on.</param>
/// <returns>The processed value.</returns>
/// <remarks>
/// Applies all control processors to the passed value.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue ProcessValue(TValue value)
{
ProcessValue(ref value);
return value;
}
/// <summary>
/// Applies all control processors to the passed value.
/// </summary>
/// <param name="value">value to run processors on.</param>
/// <remarks>
/// Use this overload when your state struct is large to avoid creating copies of the state.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ProcessValue(ref TValue value)
{
if (m_ProcessorStack.length <= 0)
return;
value = m_ProcessorStack.firstValue.Process(value, this);
if (m_ProcessorStack.additionalValues == null)
return;
for (var i = 0; i < m_ProcessorStack.length - 1; ++i)
value = m_ProcessorStack.additionalValues[i].Process(value, this);
}
internal InlinedArray<InputProcessor<TValue>> m_ProcessorStack;
private TValue m_CachedValue;
private TValue m_UnprocessedCachedValue;
#if UNITY_EDITOR
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe ref readonly TValue ReadStateInEditor()
{
// we don't use cached values during editor updates because editor updates cause controls to look at a
// different block of state memory, and since the cached values are from the play mode memory, we'd
// end up returning the wrong values.
m_EditorValue = ReadValueFromState(currentStatePtr);
return ref m_EditorValue;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe ref readonly TValue ReadUnprocessedStateInEditor()
{
m_UnprocessedEditorValue = ReadUnprocessedValueFromState(currentStatePtr);
return ref m_UnprocessedEditorValue;
}
// these fields are just to work with the fact that the 'value' property is ref readonly, so we
// need somewhere with a known lifetime to store these so they can be returned by ref.
private TValue m_EditorValue;
private TValue m_UnprocessedEditorValue;
#endif
// Only layouts are allowed to modify the processor stack.
internal TProcessor TryGetProcessor<TProcessor>()
where TProcessor : InputProcessor<TValue>
{
if (m_ProcessorStack.length > 0)
{
if (m_ProcessorStack.firstValue is TProcessor processor)
return processor;
if (m_ProcessorStack.additionalValues != null)
for (var i = 0; i < m_ProcessorStack.length - 1; ++i)
if (m_ProcessorStack.additionalValues[i] is TProcessor result)
return result;
}
return default;
}
internal override void AddProcessor(object processor)
{
if (!(processor is InputProcessor<TValue> processorOfType))
throw new ArgumentException(
$"Cannot add processor of type '{processor.GetType().Name}' to control of type '{GetType().Name}'", nameof(processor));
m_ProcessorStack.Append(processorOfType);
}
#if UNITY_EDITOR
internal override IEnumerable<object> GetProcessors()
{
foreach (var processor in m_ProcessorStack)
yield return processor;
}
#endif
internal bool evaluateProcessorsEveryRead = false;
/// <inheritdoc />
protected override void FinishSetup()
{
foreach (var processor in m_ProcessorStack)
if (processor.cachingPolicy == InputProcessor.CachingPolicy.EvaluateOnEveryRead)
evaluateProcessorsEveryRead = true;
base.FinishSetup();
}
internal InputProcessor<TValue>[] processors => m_ProcessorStack.ToArray();
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d59a0dc8508f4cf1819f6ba99250cd8e
timeCreated: 1506735383

View File

@@ -0,0 +1,352 @@
using System;
using UnityEngine.InputSystem.Composites;
using UnityEngine.InputSystem.LowLevel;
namespace UnityEngine.InputSystem.Layouts
{
/// <summary>
/// Mark a field or property as representing/identifying an input control in some form.
/// </summary>
/// <remarks>
/// This attribute is used in different places for different purposes.
///
/// When creating input control layouts (<see cref="InputControlLayout"/>) in C#, applying the
/// attribute to fields in a state struct (see <see cref="IInputStateTypeInfo"/> or <see cref="GamepadState"/>
/// for an example) or to properties in an input device (<see cref="InputDevice"/>), will cause an
/// <see cref="InputControl"/> to be created from the field or property at runtime. The attribute
/// can be applied multiple times to create multiple input controls (e.g. when having an int field
/// that represents a bitfield where each bit is a separate button).
///
/// <example>
/// <code>
/// public class MyDevice : InputDevice
/// {
/// // Adds an InputControl with name=myButton and layout=Button to the device.
/// [InputControl]
/// public ButtonControl myButton { get; set; }
/// }
/// </code>
/// </example>
///
/// Another use is for marking <c>string</c> type fields that represent input control paths. Applying
/// the attribute to them will cause them to automatically use a custom inspector similar to the one
/// found in the action editor. For this use, only the <see cref="layout"/> property is taken into
/// account.
///
/// <example>
/// <code>
/// public class MyBehavior : MonoBehaviour
/// {
/// // In the inspector, shows a control selector that is restricted to
/// // selecting buttons. As a result, controlPath will be set to path
/// // representing the control that was picked (e.g. "&lt;Gamepad&gt;/buttonSouth").
/// [InputControl(layout = "Button")]
/// public string controlPath;
///
/// protected void OnEnable()
/// {
/// // Find controls by path.
/// var controls = InputSystem.FindControl(controlPath);
/// //...
/// }
/// }
/// </code>
/// </example>
///
/// Finally, the attribute is also used in composite bindings (<see cref="InputBindingComposite"/>)
/// to mark fields that reference parts of the composite. An example for this is <see cref="AxisComposite.negative"/>.
/// In this use, also only the <see cref="layout"/> property is taken into account while other properties
/// are ignored.
///
/// <example>
/// <code>
/// public class MyComposite : InputBindingComposite&lt;float&gt;
/// {
/// // Add a part to the composite called 'firstControl' which expects
/// // AxisControls.
/// [InputControl(layout = "Axis")]
/// public int firstControl;
///
/// // Add a part to the composite called 'secondControl' which expects
/// // Vector3Controls.
/// [InputControl(layout = "Vector3")]
/// public int secondControl;
///
/// //...
/// }
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputControlLayout"/>
/// <seealso cref="InputBindingComposite"/>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)]
public sealed class InputControlAttribute : PropertyAttribute
{
/// <summary>
/// Layout to use for the control.
/// </summary>
/// <value>Layout to use for the control.</value>
/// <remarks>
/// If this is not set, the system tries to infer the layout type from the value type of
/// the field or property. If the value type is itself registered as a layout, that layout
/// will be used (e.g. when you have a property of type <see cref="Controls.ButtonControl"/>, the layout
/// will be inferred to be "Button"). Otherwise, if a layout with the same name as the type is registered,
/// that layout will be used (e.g. when you have a field of type <see cref="Vector3"/>, the layout
/// will be inferred to be "Vector3").
/// </remarks>
/// <seealso cref="InputControlLayout"/>
public string layout { get; set; }
/// <summary>
/// Layout variant to use for the control.
/// </summary>
/// <value>Layout variant to use for the control.</value>
public string variants { get; set; }
/// <summary>
/// Name to give to the name. If null or empty, the name of the property or
/// field the attribute is applied to will be used.
/// </summary>
/// <value>Name to give to the control.</value>
/// <seealso cref="InputControl.name"/>
public string name { get; set; }
/// <summary>
/// Storage format to use for the control. If not set, default storage format
/// for the given <see cref="layout"/> is used.
/// </summary>
/// <value>Memory storage format to use for the control.</value>
/// <seealso cref="InputStateBlock.format"/>
public string format { get; set; }
/// <summary>
/// Usage to apply to the control.
/// </summary>
/// <value>Usage for the control.</value>
/// <remarks>
/// This property can be used in place of <see cref="usages"/> to set just a single
/// usage on the control.
/// </remarks>
/// <seealso cref="InputControl.usages"/>
/// <seealso cref="InputControlLayout.ControlItem.usages"/>
/// <seealso cref="CommonUsages"/>
public string usage { get; set; }
/// <summary>
/// Usages to apply to the control.
/// </summary>
/// <value>Usages for the control.</value>
/// <remarks>
/// This property should be used instead of <see cref="usage"/> when a control has multiple usages.
/// </remarks>
/// <seealso cref="InputControl.usages"/>
/// <seealso cref="InputControlLayout.ControlItem.usages"/>
/// <seealso cref="CommonUsages"/>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "According to MSDN, this message can be ignored for attribute parameters, as there are no better alternatives.")]
public string[] usages { get; set; }
/// <summary>
/// Optional list of parameters to apply to the control.
/// </summary>
/// <value>Parameters to apply to the control.</value>
/// <remarks>
/// An <see cref="InputControl"/> may expose public fields which can be set as
/// parameters. An example of this is <see cref="Controls.AxisControl.clamp"/>.
///
/// <example>
/// <code>
/// public struct MyStateStruct : IInputStateTypeInfo
/// {
/// [InputControl(parameters = "clamp,clampMin=-0.5,clampMax=0.5")]
/// public float axis;
/// }
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputControlLayout.ControlItem.parameters"/>
public string parameters { get; set; }
/// <summary>
/// Optional list of processors to add to the control.
/// </summary>
/// <value>Processors to apply to the control.</value>
/// <remarks>
/// Each element in the list is a name of a processor (as registered with
/// <see cref="InputSystem.RegisterProcessor{T}"/>) followed by an optional
/// list of parameters.
///
/// For example, <c>"normalize(min=0,max=256)"</c> is one element that puts
/// a <c>NormalizeProcessor</c> on the control and sets its <c>min</c> field
/// to 0 and its its <c>max</c> field to 256.
///
/// Multiple processors can be put on a control by separating them with a comma.
/// For example, <c>"normalize(max=256),scale(factor=2)"</c> puts both a <c>NormalizeProcessor</c>
/// and a <c>ScaleProcessor</c> on the control. Processors are applied in the
/// order they are listed.
/// </remarks>
/// <seealso cref="InputControlLayout.ControlItem.processors"/>
/// <seealso cref="InputBinding.processors"/>
public string processors { get; set; }
/// <summary>
/// An alternative name that can be used in place of <see cref="name"/> to find
/// the control.
/// </summary>
/// <value>Alternative name for the control.</value>
/// <remarks>
/// This property can be used instead of <see cref="aliases"/> when there is only a
/// single alias for the control.
///
/// Aliases, like names, are case-insensitive. Any control may have arbitrary many
/// aliases.
/// </remarks>
/// <seealso cref="InputControl.aliases"/>
/// <seealso cref="InputControlLayout.ControlItem.aliases"/>
public string alias { get; set; }
/// <summary>
/// A list of alternative names that can be used in place of <see cref="name"/> to
/// find the control.
/// </summary>
/// <value>Alternative names for the control.</value>
/// <remarks>
/// This property should be used instead of <see cref="alias"/> when a control has
/// multiple aliases.
///
/// Aliases, like names, are case-insensitive. Any control may have arbitrary many
/// aliases.
/// </remarks>
/// <seealso cref="InputControl.aliases"/>
/// <seealso cref="InputControlLayout.ControlItem.aliases"/>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "According to MSDN, this message can be ignored for attribute parameters, as there are no better alternatives.")]
public string[] aliases { get; set; }
public string useStateFrom { get; set; }
public uint bit { get; set; } = InputStateBlock.InvalidOffset;
/// <summary>
/// Offset in bytes to where the memory of the control starts. Relative to
/// the offset of the parent control (which may be the device itself).
/// </summary>
/// <value>Byte offset of the control.</value>
/// <remarks>
/// If the attribute is applied to fields in an <see cref="InputControlLayout"/> and
/// this property is not set, the offset of the field is used instead.
///
/// <example>
/// <code>
/// public struct MyStateStruct : IInputStateTypeInfo
/// {
/// public int buttons;
///
/// [InputControl] // Automatically uses the offset of 'axis'.
/// public float axis;
/// }
///
/// [InputControlLayout(stateType = typeof(MyStateStruct))]
/// public class MyDevice : InputDevice
/// {
/// }
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputControlLayout.ControlItem.offset"/>
public uint offset { get; set; } = InputStateBlock.InvalidOffset;
/// <summary>
/// Size of the memory storage for the control in bits.
/// </summary>
/// <value>Size of the control in bits.</value>
/// <remarks>
/// If the attribute is applied to fields in an <see cref="InputControlLayout"/> and
/// this property is not set, the size is taken from the field.
///
/// <example>
/// <code>
/// public struct MyStateStruct : IInputStateTypeInfo
/// {
/// public int buttons;
///
/// [InputControl] // Automatically uses sizeof(float).
/// public float axis;
/// }
///
/// [InputControlLayout(stateType = typeof(MyStateStruct))]
/// public class MyDevice : InputDevice
/// {
/// }
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputControlLayout.ControlItem.sizeInBits"/>
/// <seealso cref="InputStateBlock.sizeInBits"/>
public uint sizeInBits { get; set; }
public int arraySize { get; set; }
/// <summary>
/// Display name to assign to the control.
/// </summary>
/// <value>Display name for the control.</value>
/// <seealso cref="InputControl.displayName"/>
/// <seealso cref="InputControlLayout.ControlItem.displayName"/>
public string displayName { get; set; }
/// <summary>
/// Short display name to assign to the control.
/// </summary>
/// <value>Short display name for the control.</value>
/// <seealso cref="InputControl.shortDisplayName"/>
/// <seealso cref="InputControlLayout.ControlItem.shortDisplayName"/>
public string shortDisplayName { get; set; }
/// <summary>
/// Whether the control is noisy. Off by default.
/// </summary>
/// <value>Whether control is noisy.</value>
/// <seealso cref="InputControl.noisy"/>
/// <seealso cref="InputControlLayout.ControlItem.isNoisy"/>
public bool noisy { get; set; }
/// <summary>
/// Whether the control is synthetic. Off by default.
/// </summary>
/// <value>Whether control is synthetic.</value>
/// <seealso cref="InputControl.synthetic"/>
/// <seealso cref="InputControlLayout.ControlItem.isSynthetic"/>
public bool synthetic { get; set; }
/// <summary>
/// Allows you to specify that a control should not be reset when its device is reset.
/// </summary>
/// <value>If true, resets of the device will leave the value of the control untouched except if a "hard" reset
/// is explicitly enforced.</value>
/// <seealso cref="InputSystem.ResetDevice"/>
/// <seealso cref="InputControlLayout.ControlItem.dontReset"/>
public bool dontReset { get; set; }
/// <summary>
/// Default state to write into the control's memory.
/// </summary>
/// <value>Default memory state for the control.</value>
/// <remarks>
/// This is not the default <em>value</em> but rather the default memory state, i.e.
/// the raw memory value read and the processed and returned as a value. By default
/// this is <c>null</c> and result in a control's memory to be initialized with all
/// zeroes.
/// </remarks>
/// <seealso cref="InputControlLayout.ControlItem.defaultState"/>
public object defaultState { get; set; }
/// <summary>
/// Lower limit for values of the control.
/// </summary>
/// <value>Lower limit for values of the control.</value>
/// <remarks>
/// This is null by default in which case no lower bound is applied to the TODO
/// </remarks>
public object minValue { get; set; }
public object maxValue { get; set; }
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f07d0a558a2d4f5c885936a3a1219008
timeCreated: 1506739435

View File

@@ -0,0 +1,1945 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
////REVIEW: some of the stuff here is really low-level; should we move it into a separate static class inside of .LowLevel?
namespace UnityEngine.InputSystem
{
/// <summary>
/// Various extension methods for <see cref="InputControl"/>. Mostly low-level routines.
/// </summary>
public static class InputControlExtensions
{
/// <summary>
/// Find a control of the given type in control hierarchy of <paramref name="control"/>.
/// </summary>
/// <param name="control">Control whose parents to inspect.</param>
/// <typeparam name="TControl">Type of control to look for. Actual control type can be
/// subtype of this.</typeparam>
/// <remarks>The found control of type <typeparamref name="TControl"/> which may be either
/// <paramref name="control"/> itself or one of its parents. If no such control was found,
/// returns <c>null</c>.</remarks>
/// <exception cref="ArgumentNullException"><paramref name="control"/> is <c>null</c>.</exception>
public static TControl FindInParentChain<TControl>(this InputControl control)
where TControl : InputControl
{
if (control == null)
throw new ArgumentNullException(nameof(control));
for (var parent = control; parent != null; parent = parent.parent)
if (parent is TControl parentOfType)
return parentOfType;
return null;
}
////REVIEW: This ist too high up in the class hierarchy; can be applied to any kind of control without it being readily apparent what exactly it means
/// <summary>
/// Check whether the given control is considered pressed according to the button press threshold.
/// </summary>
/// <param name="control">Control to check.</param>
/// <param name="buttonPressPoint">Optional custom button press point. If not supplied, <see cref="InputSettings.defaultButtonPressPoint"/>
/// is used.</param>
/// <returns>True if the actuation of the given control is high enough for it to be considered pressed.</returns>
/// <exception cref="ArgumentNullException"><paramref name="control"/> is <c>null</c>.</exception>
/// <remarks>
/// This method checks the actuation level of the control as <see cref="IsActuated"/> does. For <see cref="Controls.ButtonControl"/>s
/// and other <c>float</c> value controls, this will effectively check whether the float value of the control exceeds the button
/// point threshold. Note that if the control is an axis that can be both positive and negative, the press threshold works in
/// both directions, i.e. it can be crossed both in the positive direction and in the negative direction.
/// </remarks>
/// <seealso cref="IsActuated"/>
/// <seealso cref="InputSettings.defaultButtonPressPoint"/>
/// <seealso cref="InputSystem.settings"/>
public static bool IsPressed(this InputControl control, float buttonPressPoint = 0)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (Mathf.Approximately(0, buttonPressPoint))
{
if (control is ButtonControl button)
buttonPressPoint = button.pressPointOrDefault;
else
buttonPressPoint = ButtonControl.s_GlobalDefaultButtonPressPoint;
}
return control.IsActuated(buttonPressPoint);
}
/// <summary>
/// Return true if the given control is actuated.
/// </summary>
/// <param name="control"></param>
/// <param name="threshold">Magnitude threshold that the control must match or exceed to be considered actuated.
/// An exception to this is the default value of zero. If threshold is zero, the control must have a magnitude
/// greater than zero.</param>
/// <returns></returns>
/// <remarks>
/// Actuation is defined as a control having a magnitude (<see cref="InputControl.EvaluateMagnitude()"/>
/// greater than zero or, if the control does not support magnitudes, has been moved from its default
/// state.
///
/// In practice, this means that when actuated, a control will produce a value other than its default
/// value.
/// </remarks>
public static bool IsActuated(this InputControl control, float threshold = 0)
{
// First perform cheap memory check. If we're in default state, we don't
// need to invoke virtuals on the control.
if (control.CheckStateIsAtDefault())
return false;
// Check magnitude of actuation.
var magnitude = control.magnitude;
if (magnitude < 0)
{
// We know the control is not in default state but we also know it doesn't support
// magnitude. So, all we can say is that it is actuated. Not how much it is actuated.
//
// If we're looking for a specific threshold here, consider the control to always
// be under. But if not, consider it actuated "by virtue of not being in default state".
if (Mathf.Approximately(threshold, 0))
return true;
return false;
}
if (Mathf.Approximately(threshold, 0))
return magnitude > 0;
return magnitude >= threshold;
}
/// <summary>
/// Read the current value of the control and return it as an object.
/// </summary>
/// <returns></returns>
/// <remarks>
/// This method allocates GC memory and thus may cause garbage collection when used during gameplay.
///
/// Use <seealso cref="ReadValueIntoBuffer"/> to read values generically without having to know the
/// specific value type of a control.
/// </remarks>
/// <seealso cref="ReadValueIntoBuffer"/>
/// <seealso cref="InputControl{TValue}.ReadValue"/>
public static unsafe object ReadValueAsObject(this InputControl control)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
return control.ReadValueFromStateAsObject(control.currentStatePtr);
}
/// <summary>
/// Read the current, processed value of the control and store it into the given memory buffer.
/// </summary>
/// <param name="buffer">Buffer to store value in. Note that the value is not stored with the offset
/// found in <see cref="InputStateBlock.byteOffset"/> of the control's <see cref="InputControl.stateBlock"/>. It will
/// be stored directly at the given address.</param>
/// <param name="bufferSize">Size of the memory available at <paramref name="buffer"/> in bytes. Has to be
/// at least <see cref="InputControl.valueSizeInBytes"/>. If the size is smaller, nothing will be written to the buffer.</param>
/// <seealso cref="InputControl.valueSizeInBytes"/>
/// <seealso cref="InputControl.valueType"/>
/// <seealso cref="InputControl.ReadValueFromStateIntoBuffer"/>
public static unsafe void ReadValueIntoBuffer(this InputControl control, void* buffer, int bufferSize)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
control.ReadValueFromStateIntoBuffer(control.currentStatePtr, buffer, bufferSize);
}
/// <summary>
/// Read the control's default value and return it as an object.
/// </summary>
/// <param name="control">Control to read default value from.</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"><paramref name="control"/> is null.</exception>
/// <remarks>
/// This method allocates GC memory and should thus not be used during normal gameplay.
/// </remarks>
/// <seealso cref="InputControl.hasDefaultState"/>
/// <seealso cref="InputControl.defaultStatePtr"/>
public static unsafe object ReadDefaultValueAsObject(this InputControl control)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
return control.ReadValueFromStateAsObject(control.defaultStatePtr);
}
/// <summary>
/// Read the value for the given control from the given event.
/// </summary>
/// <param name="control">Control to read value for.</param>
/// <param name="inputEvent">Event to read value from. Must be a <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/>.</param>
/// <typeparam name="TValue">Type of value to read.</typeparam>
/// <exception cref="ArgumentNullException"><paramref name="control"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"><paramref name="inputEvent"/> is not a <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/>.</exception>
/// <returns>The value for the given control as read out from the given event or <c>default(TValue)</c> if the given
/// event does not contain a value for the control (e.g. if it is a <see cref="DeltaStateEvent"/> not containing the relevant
/// portion of the device's state memory).</returns>
public static TValue ReadValueFromEvent<TValue>(this InputControl<TValue> control, InputEventPtr inputEvent)
where TValue : struct
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (!ReadValueFromEvent(control, inputEvent, out var value))
return default;
return value;
}
/// <summary>
/// Check if the given event contains a value for the given control and if so, read the value.
/// </summary>
/// <param name="control">Control to read value for.</param>
/// <param name="inputEvent">Input event. This must be a <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/>.
/// Note that in the case of a <see cref="DeltaStateEvent"/>, the control may not actually be part of the event. In this
/// case, the method returns false and stores <c>default(TValue)</c> in <paramref name="value"/>.</param>
/// <param name="value">Variable that receives the control value.</param>
/// <typeparam name="TValue">Type of value to read.</typeparam>
/// <returns>True if the value has been successfully read from the event, false otherwise.</returns>
/// <exception cref="ArgumentNullException"><paramref name="control"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"><paramref name="inputEvent"/> is not a <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/>.</exception>
/// <seealso cref="ReadUnprocessedValueFromEvent{TValue}(InputControl{TValue},InputEventPtr)"/>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#")]
public static unsafe bool ReadValueFromEvent<TValue>(this InputControl<TValue> control, InputEventPtr inputEvent, out TValue value)
where TValue : struct
{
if (control == null)
throw new ArgumentNullException(nameof(control));
var statePtr = control.GetStatePtrFromStateEvent(inputEvent);
if (statePtr == null)
{
value = control.ReadDefaultValue();
return false;
}
value = control.ReadValueFromState(statePtr);
return true;
}
/// <summary>
/// Read the value of <paramref name="control"/> from the given <paramref name="inputEvent"/> without having to
/// know the specific value type of the control.
/// </summary>
/// <param name="control">Control to read the value for.</param>
/// <param name="inputEvent">An <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/> to read the value from.</param>
/// <returns>The current value for the control or <c>null</c> if the control's value is not included
/// in the event.</returns>
/// <seealso cref="InputControl.ReadValueFromStateAsObject"/>
public static unsafe object ReadValueFromEventAsObject(this InputControl control, InputEventPtr inputEvent)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
var statePtr = control.GetStatePtrFromStateEvent(inputEvent);
if (statePtr == null)
return control.ReadDefaultValueAsObject();
return control.ReadValueFromStateAsObject(statePtr);
}
public static TValue ReadUnprocessedValueFromEvent<TValue>(this InputControl<TValue> control, InputEventPtr eventPtr)
where TValue : struct
{
if (control == null)
throw new ArgumentNullException(nameof(control));
var result = default(TValue);
control.ReadUnprocessedValueFromEvent(eventPtr, out result);
return result;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#")]
public static unsafe bool ReadUnprocessedValueFromEvent<TValue>(this InputControl<TValue> control, InputEventPtr inputEvent, out TValue value)
where TValue : struct
{
if (control == null)
throw new ArgumentNullException(nameof(control));
var statePtr = control.GetStatePtrFromStateEvent(inputEvent);
if (statePtr == null)
{
value = control.ReadDefaultValue();
return false;
}
value = control.ReadUnprocessedValueFromState(statePtr);
return true;
}
////REVIEW: this has the opposite argument order of WriteValueFromObjectIntoState; fix!
public static unsafe void WriteValueFromObjectIntoEvent(this InputControl control, InputEventPtr eventPtr, object value)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
var statePtr = control.GetStatePtrFromStateEvent(eventPtr);
if (statePtr == null)
return;
control.WriteValueFromObjectIntoState(value, statePtr);
}
/// <summary>
/// Write the control's current value into <paramref name="statePtr"/>.
/// </summary>
/// <param name="control">Control to read the current value from and to store state for in <paramref name="statePtr"/>.</param>
/// <param name="statePtr">State to receive the control's value in its respective <see cref="InputControl.stateBlock"/>.</param>
/// <exception cref="ArgumentNullException"><paramref name="control"/> is null or <paramref name="statePtr"/> is null.</exception>
/// <remarks>
/// This method is equivalent to <see cref="InputControl{TValue}.WriteValueIntoState"/> except that one does
/// not have to know the value type of the given control.
/// </remarks>
/// <exception cref="NotSupportedException">The control does not support writing. This is the case, for
/// example, that compute values (such as the magnitude of a vector).</exception>
/// <seealso cref="InputControl{TValue}.WriteValueIntoState"/>
public static unsafe void WriteValueIntoState(this InputControl control, void* statePtr)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (statePtr == null)
throw new ArgumentNullException(nameof(statePtr));
var valueSize = control.valueSizeInBytes;
var valuePtr = UnsafeUtility.Malloc(valueSize, 8, Allocator.Temp);
try
{
control.ReadValueFromStateIntoBuffer(control.currentStatePtr, valuePtr, valueSize);
control.WriteValueFromBufferIntoState(valuePtr, valueSize, statePtr);
}
finally
{
UnsafeUtility.Free(valuePtr, Allocator.Temp);
}
}
public static unsafe void WriteValueIntoState<TValue>(this InputControl control, TValue value, void* statePtr)
where TValue : struct
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (!(control is InputControl<TValue> controlOfType))
throw new ArgumentException(
$"Expecting control of type '{typeof(TValue).Name}' but got '{control.GetType().Name}'");
controlOfType.WriteValueIntoState(value, statePtr);
}
public static unsafe void WriteValueIntoState<TValue>(this InputControl<TValue> control, TValue value, void* statePtr)
where TValue : struct
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (statePtr == null)
throw new ArgumentNullException(nameof(statePtr));
var valuePtr = UnsafeUtility.AddressOf(ref value);
var valueSize = UnsafeUtility.SizeOf<TValue>();
control.WriteValueFromBufferIntoState(valuePtr, valueSize, statePtr);
}
public static unsafe void WriteValueIntoState<TValue>(this InputControl<TValue> control, void* statePtr)
where TValue : struct
{
if (control == null)
throw new ArgumentNullException(nameof(control));
control.WriteValueIntoState(control.ReadValue(), statePtr);
}
/// <summary>
///
/// </summary>
/// <param name="state"></param>
/// <param name="value">Value for <paramref name="control"/> to write into <paramref name="state"/>.</param>
/// <typeparam name="TState"></typeparam>
/// <exception cref="ArgumentNullException"><paramref name="control"/> is null.</exception>
/// <exception cref="ArgumentException">Control's value does not fit within the memory of <paramref name="state"/>.</exception>
/// <exception cref="NotSupportedException"><paramref name="control"/> does not support writing.</exception>
public static unsafe void WriteValueIntoState<TValue, TState>(this InputControl<TValue> control, TValue value, ref TState state)
where TValue : struct
where TState : struct, IInputStateTypeInfo
{
if (control == null)
throw new ArgumentNullException(nameof(control));
// Make sure the control's state actually fits within the given state.
var sizeOfState = UnsafeUtility.SizeOf<TState>();
if (control.stateOffsetRelativeToDeviceRoot + control.m_StateBlock.alignedSizeInBytes >= sizeOfState)
throw new ArgumentException(
$"Control {control.path} with offset {control.stateOffsetRelativeToDeviceRoot} and size of {control.m_StateBlock.sizeInBits} bits is out of bounds for state of type {typeof(TState).Name} with size {sizeOfState}",
nameof(state));
// Write value.
var statePtr = (byte*)UnsafeUtility.AddressOf(ref state);
control.WriteValueIntoState(value, statePtr);
}
public static void WriteValueIntoEvent<TValue>(this InputControl control, TValue value, InputEventPtr eventPtr)
where TValue : struct
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (!eventPtr.valid)
throw new ArgumentNullException(nameof(eventPtr));
if (!(control is InputControl<TValue> controlOfType))
throw new ArgumentException(
$"Expecting control of type '{typeof(TValue).Name}' but got '{control.GetType().Name}'");
controlOfType.WriteValueIntoEvent(value, eventPtr);
}
public static unsafe void WriteValueIntoEvent<TValue>(this InputControl<TValue> control, TValue value, InputEventPtr eventPtr)
where TValue : struct
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (!eventPtr.valid)
throw new ArgumentNullException(nameof(eventPtr));
var statePtr = control.GetStatePtrFromStateEvent(eventPtr);
if (statePtr == null)
return;
control.WriteValueIntoState(value, statePtr);
}
/// <summary>
/// Copy the state of the device to the given memory buffer.
/// </summary>
/// <param name="device">An input device.</param>
/// <param name="buffer">Buffer to copy the state of the device to.</param>
/// <param name="bufferSizeInBytes">Size of <paramref name="buffer"/> in bytes.</param>
/// <exception cref="ArgumentException"><paramref name="bufferSizeInBytes"/> is less than or equal to 0.</exception>
/// <exception cref="ArgumentNullException"><paramref name="device"/> is <c>null</c>.</exception>
/// <remarks>
/// The method will copy however much fits into the given buffer. This means that if the state of the device
/// is larger than what fits into the buffer, not all of the device's state is copied.
/// </remarks>
/// <seealso cref="InputControl.stateBlock"/>
public static unsafe void CopyState(this InputDevice device, void* buffer, int bufferSizeInBytes)
{
if (device == null)
throw new ArgumentNullException(nameof(device));
if (bufferSizeInBytes <= 0)
throw new ArgumentException("bufferSizeInBytes must be positive", nameof(bufferSizeInBytes));
var stateBlock = device.m_StateBlock;
var sizeToCopy = Math.Min(bufferSizeInBytes, stateBlock.alignedSizeInBytes);
UnsafeUtility.MemCpy(buffer, (byte*)device.currentStatePtr + stateBlock.byteOffset, sizeToCopy);
}
/// <summary>
/// Copy the state of the device to the given struct.
/// </summary>
/// <param name="device">An input device.</param>
/// <param name="state">Struct to copy the state of the device into.</param>
/// <typeparam name="TState">A state struct type such as <see cref="MouseState"/>.</typeparam>
/// <exception cref="ArgumentException">The state format of <typeparamref name="TState"/> does not match
/// the state form of <paramref name="device"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="device"/> is <c>null</c>.</exception>
/// <remarks>
/// This method will copy memory verbatim into the memory of the given struct. It will copy whatever
/// memory of the device fits into the given struct.
/// </remarks>
/// <seealso cref="InputControl.stateBlock"/>
public static unsafe void CopyState<TState>(this InputDevice device, out TState state)
where TState : struct, IInputStateTypeInfo
{
if (device == null)
throw new ArgumentNullException(nameof(device));
state = default;
if (device.stateBlock.format != state.format)
throw new ArgumentException(
$"Struct '{typeof(TState).Name}' has state format '{state.format}' which doesn't match device '{device}' with state format '{device.stateBlock.format}'",
nameof(TState));
var stateSize = UnsafeUtility.SizeOf<TState>();
var statePtr = UnsafeUtility.AddressOf(ref state);
device.CopyState(statePtr, stateSize);
}
/// <summary>
/// Check whether the memory of the given control is in its default state.
/// </summary>
/// <param name="control">An input control on a device that's been added to the system (see <see cref="InputDevice.added"/>).</param>
/// <returns>True if the state memory of the given control corresponds to the control's default.</returns>
/// <exception cref="ArgumentNullException"><paramref name="control"/> is <c>null</c>.</exception>
/// <remarks>
/// This method is a cheaper check than actually reading out the value from the control and checking whether it
/// is the same value as the default value. The method bypasses all value reading and simply performs a trivial
/// memory comparison of the control's current state memory to the default state memory stored for the control.
///
/// Note that the default state for the memory of a control does not necessary need to be all zeroes. For example,
/// a stick axis may be stored as an unsigned 8-bit value with the memory state corresponding to a 0 value being 127.
/// </remarks>
/// <seealso cref="InputControl.stateBlock"/>
public static unsafe bool CheckStateIsAtDefault(this InputControl control)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
return CheckStateIsAtDefault(control, control.currentStatePtr);
}
/// <summary>
/// Check if the given state corresponds to the default state of the control.
/// </summary>
/// <param name="control">Control to check the state for in <paramref name="statePtr"/>.</param>
/// <param name="statePtr">Pointer to a state buffer containing the <see cref="InputControl.stateBlock"/> for <paramref name="control"/>.</param>
/// <param name="maskPtr">If not null, only bits set to <c>false</c> (!) in the buffer will be taken into account. This can be used
/// to mask out noise, i.e. every bit that is set in the mask is considered to represent noise.</param>
/// <returns>True if the control/device is in its default state.</returns>
/// <remarks>
/// Note that default does not equate all zeroes. Stick axes, for example, that are stored as unsigned byte
/// values will have their resting position at 127 and not at 0. This is why we explicitly store default
/// state in a memory buffer instead of assuming zeroes.
/// </remarks>
/// <seealso cref="InputControl{TValue}.ReadDefaultValue"/>
public static unsafe bool CheckStateIsAtDefault(this InputControl control, void* statePtr, void* maskPtr = null)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (statePtr == null)
throw new ArgumentNullException(nameof(statePtr));
return control.CompareState(statePtr, control.defaultStatePtr, maskPtr);
}
public static unsafe bool CheckStateIsAtDefaultIgnoringNoise(this InputControl control)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
return control.CheckStateIsAtDefaultIgnoringNoise(control.currentStatePtr);
}
/// <summary>
/// Check if the given state corresponds to the default state of the control or has different state only
/// for parts marked as noise.
/// </summary>
/// <param name="control">Control to check the state for in <paramref name="statePtr"/>.</param>
/// <param name="statePtr">Pointer to a state buffer containing the <see cref="InputControl.stateBlock"/> for <paramref name="control"/>.</param>
/// <returns>True if the control/device is in its default state (ignoring any bits marked as noise).</returns>
/// <remarks>
/// Note that default does not equate all zeroes. Stick axes, for example, that are stored as unsigned byte
/// values will have their resting position at 127 and not at 0. This is why we explicitly store default
/// state in a memory buffer instead of assuming zeroes.
/// </remarks>
/// <seealso cref="InputControl{TValue}.ReadDefaultValue"/>
/// <seealso cref="InputControl.noisy"/>
/// <seealso cref="InputControl.noiseMaskPtr"/>
public static unsafe bool CheckStateIsAtDefaultIgnoringNoise(this InputControl control, void* statePtr)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (statePtr == null)
throw new ArgumentNullException(nameof(statePtr));
return control.CheckStateIsAtDefault(statePtr, InputStateBuffers.s_NoiseMaskBuffer);
}
/// <summary>
/// Compare the control's current state to the state stored in <paramref name="statePtr"/>.
/// </summary>
/// <param name="statePtr">State memory containing the control's <see cref="InputControl.stateBlock"/>.</param>
/// <returns>True if </returns>
/// <seealso cref="InputControl.currentStatePtr"/>
/// <remarks>
/// This method ignores noise
///
/// This method will not actually read values but will instead compare state directly as it is stored
/// in memory. <see cref="InputControl{TValue}.ReadValue"/> is not invoked and thus processors will
/// not be run.
/// </remarks>
public static unsafe bool CompareStateIgnoringNoise(this InputControl control, void* statePtr)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (statePtr == null)
throw new ArgumentNullException(nameof(statePtr));
return control.CompareState(control.currentStatePtr, statePtr, control.noiseMaskPtr);
}
/// <summary>
/// Compare the control's stored state in <paramref name="firstStatePtr"/> to <paramref name="secondStatePtr"/>.
/// </summary>
/// <param name="firstStatePtr">Memory containing the control's <see cref="InputControl.stateBlock"/>.</param>
/// <param name="secondStatePtr">Memory containing the control's <see cref="InputControl.stateBlock"/></param>
/// <param name="maskPtr">Optional mask. If supplied, it will be used to mask the comparison between
/// <paramref name="firstStatePtr"/> and <paramref name="secondStatePtr"/> such that any bit not set in the
/// mask will be ignored even if different between the two states. This can be used, for example, to ignore
/// noise in the state (<see cref="InputControl.noiseMaskPtr"/>).</param>
/// <returns>True if the state is equivalent in both memory buffers.</returns>
/// <remarks>
/// Unlike <see cref="InputControl.CompareValue"/>, this method only compares raw memory state. If used on a stick, for example,
/// it may mean that this method returns false for two stick values that would compare equal using <see cref="InputControl.CompareValue"/>
/// (e.g. if both stick values fall below the deadzone).
/// </remarks>
/// <seealso cref="InputControl.CompareValue"/>
public static unsafe bool CompareState(this InputControl control, void* firstStatePtr, void* secondStatePtr, void* maskPtr = null)
{
////REVIEW: for compound controls, do we want to go check leaves so as to not pick up on non-control noise in the state?
//// e.g. from HID input reports; or should we just leave that to maskPtr?
var firstPtr = (byte*)firstStatePtr + (int)control.m_StateBlock.byteOffset;
var secondPtr = (byte*)secondStatePtr + (int)control.m_StateBlock.byteOffset;
var mask = maskPtr != null ? (byte*)maskPtr + (int)control.m_StateBlock.byteOffset : null;
if (control.m_StateBlock.sizeInBits == 1)
{
// If we have a mask and the bit is set in the mask, the control is to be ignored
// and thus we consider it at default value.
if (mask != null && MemoryHelpers.ReadSingleBit(mask, control.m_StateBlock.bitOffset))
return true;
return MemoryHelpers.ReadSingleBit(secondPtr, control.m_StateBlock.bitOffset) ==
MemoryHelpers.ReadSingleBit(firstPtr, control.m_StateBlock.bitOffset);
}
return MemoryHelpers.MemCmpBitRegion(firstPtr, secondPtr,
control.m_StateBlock.bitOffset, control.m_StateBlock.sizeInBits, mask);
}
public static unsafe bool CompareState(this InputControl control, void* statePtr, void* maskPtr = null)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (statePtr == null)
throw new ArgumentNullException(nameof(statePtr));
return control.CompareState(control.currentStatePtr, statePtr, maskPtr);
}
/// <summary>
/// Return true if the current value of <paramref name="control"/> is different to the one found
/// in <paramref name="statePtr"/>.
/// </summary>
/// <param name="control">Control whose state to compare to what is stored in <paramref name="statePtr"/>.</param>
/// <param name="statePtr">A block of input state memory containing the <see cref="InputControl.stateBlock"/>
/// of <paramref name="control."/></param>
/// <exception cref="ArgumentNullException"><paramref name="control"/> is <c>null</c> or <paramref name="statePtr"/>
/// is <c>null</c>.</exception>
/// <returns>True if the value of <paramref name="control"/> stored in <paramref name="statePtr"/> is different
/// compared to what <see cref="InputControl{T}.ReadValue"/> of the control returns.</returns>
public static unsafe bool HasValueChangeInState(this InputControl control, void* statePtr)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (statePtr == null)
throw new ArgumentNullException(nameof(statePtr));
return control.CompareValue(control.currentStatePtr, statePtr);
}
/// <summary>
/// Return true if <paramref name="control"/> has a different value (from its current one) in
/// <paramref name="eventPtr"/>.
/// </summary>
/// <param name="control">An input control.</param>
/// <param name="eventPtr">An input event. Must be a <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/>.</param>
/// <returns>True if <paramref name="eventPtr"/> contains a value for <paramref name="control"/> that is different
/// from the control's current value (see <see cref="InputControl{TValue}.ReadValue"/>).</returns>
/// <exception cref="ArgumentNullException"><paramref name="control"/> is <c>null</c> -or- <paramref name="eventPtr"/> is a <c>null</c> pointer (see <see cref="InputEventPtr.valid"/>).</exception>
/// <exception cref="ArgumentException"><paramref name="eventPtr"/> is not a <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/>.</exception>
public static unsafe bool HasValueChangeInEvent(this InputControl control, InputEventPtr eventPtr)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (!eventPtr.valid)
throw new ArgumentNullException(nameof(eventPtr));
var statePtr = control.GetStatePtrFromStateEvent(eventPtr);
if (statePtr == null)
return false;
return control.CompareValue(control.currentStatePtr, statePtr);
}
/// <summary>
/// Given a <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/>, return the raw memory pointer that can
/// be used, for example, with <see cref="InputControl{T}.ReadValueFromState"/> to read out the value of <paramref name="control"/>
/// contained in the event.
/// </summary>
/// <param name="control">Control to access state for in the given state event.</param>
/// <param name="eventPtr">A <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/> containing input state.</param>
/// <returns>A pointer that can be used with methods such as <see cref="InputControl{TValue}.ReadValueFromState"/> or <c>null</c>
/// if <paramref name="eventPtr"/> does not contain state for the given <paramref name="control"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="control"/> is <c>null</c> -or- <paramref name="eventPtr"/> is invalid.</exception>
/// <exception cref="ArgumentException"><paramref name="eventPtr"/> is not a <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/>.</exception>
/// <remarks>
/// Note that the given state event must have the same state format (see <see cref="InputStateBlock.format"/>) as the device
/// to which <paramref name="control"/> belongs. If this is not the case, the method will invariably return <c>null</c>.
///
/// In practice, this means that the method cannot be used with touch events or, in general, with events sent to devices
/// that implement <see cref="IInputStateCallbackReceiver"/> (which <see cref="Touchscreen"/> does) except if the event
/// is in the same state format as the device. Touch events will generally be sent as state events containing only the
/// state of a single <see cref="Controls.TouchControl"/>, not the state of the entire <see cref="Touchscreen"/>.
/// </remarks>
public static unsafe void* GetStatePtrFromStateEvent(this InputControl control, InputEventPtr eventPtr)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (!eventPtr.valid)
throw new ArgumentNullException(nameof(eventPtr));
return GetStatePtrFromStateEventUnchecked(control, eventPtr, eventPtr.type);
}
internal static unsafe void* GetStatePtrFromStateEventUnchecked(this InputControl control, InputEventPtr eventPtr, FourCC eventType)
{
uint stateOffset;
FourCC stateFormat;
uint stateSizeInBytes;
void* statePtr;
if (eventType == StateEvent.Type)
{
var stateEvent = StateEvent.FromUnchecked(eventPtr);
stateOffset = 0;
stateFormat = stateEvent->stateFormat;
stateSizeInBytes = stateEvent->stateSizeInBytes;
statePtr = stateEvent->state;
}
else if (eventType == DeltaStateEvent.Type)
{
var deltaEvent = DeltaStateEvent.FromUnchecked(eventPtr);
// If it's a delta event, we need to subtract the delta state offset if it's not set to the root of the device
stateOffset = deltaEvent->stateOffset;
stateFormat = deltaEvent->stateFormat;
stateSizeInBytes = deltaEvent->deltaStateSizeInBytes;
statePtr = deltaEvent->deltaState;
}
else
{
throw new ArgumentException($"Event must be a StateEvent or DeltaStateEvent but is a {eventType} instead",
nameof(eventPtr));
}
// Make sure we have a state event compatible with our device. The event doesn't
// have to be specifically for our device (we don't require device IDs to match) but
// the formats have to match and the size must be within range of what we're trying
// to read.
var device = control.device;
if (stateFormat != device.m_StateBlock.format)
{
// If the device is an IInputStateCallbackReceiver, there's a chance it actually recognizes
// the state format in the event and can correlate it to the state as found on the device.
if (!device.hasStateCallbacks ||
!((IInputStateCallbackReceiver)device).GetStateOffsetForEvent(control, eventPtr, ref stateOffset))
return null;
}
// Once a device has been added, global state buffer offsets are baked into control hierarchies.
// We need to unsubtract those offsets here.
// NOTE: If the given device has not actually been added to the system, the offset is simply 0
// and this is a harmless NOP.
stateOffset += device.m_StateBlock.byteOffset;
// Return null if state is out of range.
ref var controlStateBlock = ref control.m_StateBlock;
var controlOffset = (int)controlStateBlock.effectiveByteOffset - stateOffset;
if (controlOffset < 0 || controlOffset + controlStateBlock.alignedSizeInBytes > stateSizeInBytes)
return null;
return (byte*)statePtr - (int)stateOffset;
}
/// <summary>
/// Writes the default state of <paramref name="control"/> into <paramref name="eventPtr"/>.
/// </summary>
/// <param name="control">A control whose default state to write.</param>
/// <param name="eventPtr">A valid pointer to either a <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/>.</param>
/// <exception cref="ArgumentNullException"><paramref name="control"/> is <c>null</c> -or- <paramref name="eventPtr"/> contains
/// a null pointer.</exception>
/// <exception cref="ArgumentException"><paramref name="eventPtr"/> is not a <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/>.</exception>
/// <returns>True if the default state for <paramref name="control"/> was written to <paramref name="eventPtr"/>, false if the
/// given state or delta state event did not include memory for the given control.</returns>
/// <remarks>
/// Note that the default state of a control does not necessarily need to correspond to zero-initialized memory. For example, if
/// an axis control yields a range of [-1..1] and is stored as a signed 8-bit value, the default state will be 127, not 0.
///
/// <example>
/// <code>
/// // Reset the left gamepad stick to its default state (which results in a
/// // value of (0,0).
/// using (StateEvent.From(Gamepad.all[0], out var eventPtr))
/// {
/// Gamepad.all[0].leftStick.ResetToDefaultStateInEvent(eventPtr);
/// InputSystem.QueueEvent(eventPtr);
/// }
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputControl.defaultStatePtr"/>
public static unsafe bool ResetToDefaultStateInEvent(this InputControl control, InputEventPtr eventPtr)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (!eventPtr.valid)
throw new ArgumentNullException(nameof(eventPtr));
var eventType = eventPtr.type;
if (eventType != StateEvent.Type && eventType != DeltaStateEvent.Type)
throw new ArgumentException("Given event is not a StateEvent or a DeltaStateEvent", nameof(eventPtr));
var statePtr = (byte*)control.GetStatePtrFromStateEvent(eventPtr);
if (statePtr == null)
return false;
var defaultStatePtr = (byte*)control.defaultStatePtr;
ref var stateBlock = ref control.m_StateBlock;
var offset = stateBlock.byteOffset;
MemoryHelpers.MemCpyBitRegion(statePtr + offset, defaultStatePtr + offset, stateBlock.bitOffset, stateBlock.sizeInBits);
return true;
}
/// <summary>
/// Queue a value change on the given <paramref name="control"/> which will be processed and take effect
/// in the next input update.
/// </summary>
/// <param name="control">Control to change the value of.</param>
/// <param name="value">New value for the control.</param>
/// <param name="time">Optional time at which the value change should take effect. If set, this will become
/// the <see cref="InputEvent.time"/> of the queued event. If the time is in the future and the update mode is
/// set to <see cref="InputSettings.UpdateMode.ProcessEventsInFixedUpdate"/>, the event will not be processed until
/// it falls within the time of an input update slice. Otherwise, the event will invariably be consumed in the
/// next input update (see <see cref="InputSystem.Update"/>).</param>
/// <typeparam name="TValue">Type of value.</typeparam>
/// <exception cref="ArgumentNullException"><paramref name="control"/> is null.</exception>
public static void QueueValueChange<TValue>(this InputControl<TValue> control, TValue value, double time = -1)
where TValue : struct
{
if (control == null)
throw new ArgumentNullException(nameof(control));
////TODO: if it's not a bit-addressing control, send a delta state change only
using (StateEvent.From(control.device, out var eventPtr))
{
if (time >= 0)
eventPtr.time = time;
control.WriteValueIntoEvent(value, eventPtr);
InputSystem.QueueEvent(eventPtr);
}
}
/// <summary>
/// Modify <paramref name="newState"/> to write an accumulated value of the control
/// rather than the value currently found in the event.
/// </summary>
/// <param name="control">Control to perform the accumulation on.</param>
/// <param name="currentStatePtr">Memory containing the control's current state. See <see
/// cref="InputControl.currentStatePtr"/>.</param>
/// <param name="newState">Event containing the new state about to be written to the device.</param>
/// <exception cref="ArgumentNullException"><paramref name="control"/> is <c>null</c>.</exception>
/// <remarks>
/// This method reads the current, unprocessed value of the control from <see cref="InputControl.currentStatePtr"/>
/// and then adds it to the value of the control found in <paramref name="newState"/>.
///
/// Note that the method does nothing if a value for the control is not contained in <paramref name="newState"/>.
/// This can be the case, for example, for <see cref="DeltaStateEvent"/>s.
/// </remarks>
/// <seealso cref="Pointer.delta"/>
public static unsafe void AccumulateValueInEvent(this InputControl<float> control, void* currentStatePtr, InputEventPtr newState)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (!control.ReadUnprocessedValueFromEvent(newState, out var newValue))
return; // Value for the control not contained in the given event.
var oldValue = control.ReadUnprocessedValueFromState(currentStatePtr);
control.WriteValueIntoEvent(oldValue + newValue, newState);
}
internal static unsafe void AccumulateValueInEvent(this InputControl<Vector2> control, void* currentStatePtr, InputEventPtr newState)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (!control.ReadUnprocessedValueFromEvent(newState, out var newValue))
return; // Value for the control not contained in the given event.
var oldDelta = control.ReadUnprocessedValueFromState(currentStatePtr);
control.WriteValueIntoEvent(oldDelta + newValue, newState);
}
public static void FindControlsRecursive<TControl>(this InputControl parent, IList<TControl> controls, Func<TControl, bool> predicate)
where TControl : InputControl
{
if (parent == null)
throw new ArgumentNullException(nameof(parent));
if (controls == null)
throw new ArgumentNullException(nameof(controls));
if (predicate == null)
throw new ArgumentNullException(nameof(predicate));
if (parent is TControl parentAsTControl && predicate(parentAsTControl))
controls.Add(parentAsTControl);
var children = parent.children;
var childCount = children.Count;
for (var i = 0; i < childCount; ++i)
{
var child = parent.children[i];
FindControlsRecursive(child, controls, predicate);
}
}
internal static string BuildPath(this InputControl control, string deviceLayout, StringBuilder builder = null)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (string.IsNullOrEmpty(deviceLayout))
throw new ArgumentNullException(nameof(deviceLayout));
if (builder == null)
builder = new StringBuilder();
var device = control.device;
builder.Append('<');
builder.Append(deviceLayout.Escape("\\>", "\\>"));
builder.Append('>');
// Add usages of device, if any.
var deviceUsages = device.usages;
for (var i = 0; i < deviceUsages.Count; ++i)
{
builder.Append('{');
builder.Append(deviceUsages[i].ToString().Escape("\\}", "\\}"));
builder.Append('}');
}
builder.Append(InputControlPath.Separator);
// If any of the components contains a backslash, double it up as in control paths,
// these serve as escape characters.
var devicePath = device.path.Replace("\\", "\\\\");
var controlPath = control.path.Replace("\\", "\\\\");
builder.Append(controlPath, devicePath.Length + 1, controlPath.Length - devicePath.Length - 1);
return builder.ToString();
}
/// <summary>
/// Flags that control which controls are returned by <see cref="InputControlExtensions.EnumerateControls"/>.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1714:Flags enums should have plural names", Justification = "False positive: `IgnoreControlsInDefaultState` is a plural form.")]
[Flags]
public enum Enumerate
{
/// <summary>
/// Ignore controls whose value is at default (see <see cref="InputControl{TValue}.ReadDefaultValue"/>).
/// </summary>
IgnoreControlsInDefaultState = 1 << 0,
/// <summary>
/// Ignore controls whose value is the same as their current value (see <see cref="InputControl{TValue}.ReadValue"/>).
/// </summary>
IgnoreControlsInCurrentState = 1 << 1,
/// <summary>
/// Include controls that are <see cref="InputControl.synthetic"/> and/or use state from other other controls (see
/// <see cref="Layouts.InputControlLayout.ControlItem.useStateFrom"/>). These are excluded by default.
/// </summary>
IncludeSyntheticControls = 1 << 2,
/// <summary>
/// Include noisy controls (see <see cref="InputControl.noisy"/>). These are excluded by default.
/// </summary>
IncludeNoisyControls = 1 << 3,
/// <summary>
/// For any leaf control returned by the enumeration, also return all the parent controls (see <see cref="InputControl.parent"/>)
/// in turn (but not the root <see cref="InputDevice"/> itself).
/// </summary>
IncludeNonLeafControls = 1 << 4,
}
/// <summary>
/// Go through the controls that have values in <paramref name="eventPtr"/>, apply the given filters, and return each
/// control one by one.
/// </summary>
/// <param name="eventPtr">An input event. Must be a <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/>.</param>
/// <param name="flags">Filter settings that determine which controls to return.</param>
/// <param name="device">Input device from which to enumerate controls. If this is <c>null</c>, then the <see cref="InputEvent.deviceId"/>
/// from the given <paramref name="eventPtr"/> will be used to locate the device via <see cref="InputSystem.GetDeviceById"/>. If the device
/// cannot be found, an exception will be thrown. Note that type of device must match the state stored in the given event.</param>
/// <param name="magnitudeThreshold">If not zero, minimum actuation threshold (see <see cref="InputControl.EvaluateMagnitude()"/>) that
/// a control must reach (as per value in the given <paramref name="eventPtr"/>) in order for it to be included in the enumeration.</param>
/// <returns>An enumerator for the controls with values in <paramref name="eventPtr"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="eventPtr"/> is a <c>null</c> pointer (see <see cref="InputEventPtr.valid"/>).</exception>
/// <exception cref="ArgumentException"><paramref name="eventPtr"/> is not a <see cref="StateEvent"/> and not a <see cref="DeltaStateEvent"/> -or-
/// <paramref name="device"/> is <c>null</c> and no device was found with a <see cref="InputDevice.deviceId"/> matching that of <see cref="InputEvent.deviceId"/>
/// found in <paramref name="eventPtr"/>.</exception>
/// <remarks>
/// This method is much more efficient than manually iterating over the controls of <paramref name="device"/> and locating
/// the ones that have changed in <paramref name="eventPtr"/>. See <see cref="InputEventControlEnumerator"/> for details.
///
/// This method will not allocate GC memory and can safely be used with <c>foreach</c> loops.
/// </remarks>
/// <seealso cref="InputEventControlEnumerator"/>
/// <seealso cref="StateEvent"/>
/// <seealso cref="DeltaStateEvent"/>
/// <seealso cref="EnumerateChangedControls"/>
public static InputEventControlCollection EnumerateControls(this InputEventPtr eventPtr, Enumerate flags, InputDevice device = null, float magnitudeThreshold = 0)
{
if (!eventPtr.valid)
throw new ArgumentNullException(nameof(eventPtr), "Given event pointer must not be null");
var eventType = eventPtr.type;
if (eventType != StateEvent.Type && eventType != DeltaStateEvent.Type)
throw new ArgumentException($"Event must be a StateEvent or DeltaStateEvent but is a {eventType} instead", nameof(eventPtr));
// Look up device from event, if no device was supplied.
if (device == null)
{
var deviceId = eventPtr.deviceId;
device = InputSystem.GetDeviceById(deviceId);
if (device == null)
throw new ArgumentException($"Cannot find device with ID {deviceId} referenced by event", nameof(eventPtr));
}
return new InputEventControlCollection { m_Device = device, m_EventPtr = eventPtr, m_Flags = flags, m_MagnitudeThreshold = magnitudeThreshold };
}
/// <summary>
/// Go through all controls in the given <paramref name="eventPtr"/> that have changed value.
/// </summary>
/// <param name="eventPtr">An input event. Must be a <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/>.</param>
/// <param name="device">Input device from which to enumerate controls. If this is <c>null</c>, then the <see cref="InputEvent.deviceId"/>
/// from the given <paramref name="eventPtr"/> will be used to locate the device via <see cref="InputSystem.GetDeviceById"/>. If the device
/// cannot be found, an exception will be thrown. Note that type of device must match the state stored in the given event.</param>
/// <param name="magnitudeThreshold">If not zero, minimum actuation threshold (see <see cref="InputControl.EvaluateMagnitude()"/>) that
/// a control must reach (as per value in the given <paramref name="eventPtr"/>) in order for it to be included in the enumeration.</param>
/// <returns>An enumerator for the controls that have changed values in <paramref name="eventPtr"/>.</returns>
/// <remarks>
/// This method is a shorthand for calling <see cref="EnumerateControls"/> with <see cref="Enumerate.IgnoreControlsInCurrentState"/>.
///
/// <example>
/// <code>
/// // Detect button presses.
/// InputSystem.onEvent +=
/// (eventPtr, device) =>
/// {
/// // Ignore anything that is not a state event.
/// var eventType = eventPtr.type;
/// if (eventType != StateEvent.Type &amp;&amp; eventType != DeltaStateEvent.Type)
/// return;
///
/// // Find all changed controls actuated above the button press threshold.
/// foreach (var control in eventPtr.EnumerateChangedControls
/// (device: device, magnitudeThreshold: InputSystem.settings.defaultButtonPressThreshold))
/// if (control is ButtonControl button)
/// Debug.Log($"Button {button} was pressed");
/// }
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputSystem.onEvent"/>
/// <seealso cref="EnumerateControls"/>
/// <seealso cref="InputEventControlEnumerator"/>
public static InputEventControlCollection EnumerateChangedControls(this InputEventPtr eventPtr, InputDevice device = null, float magnitudeThreshold = 0)
{
return eventPtr.EnumerateControls
(Enumerate.IgnoreControlsInCurrentState, device, magnitudeThreshold);
}
/// <summary>
/// Return true if the given <paramref name="eventPtr"/> has any <see cref="Input"/>
/// </summary>
/// <param name="eventPtr">An event. Must be a <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/>.</param>
/// <param name="magnitude">The threshold value that a button must be actuated by to be considered pressed.</param>
/// <param name="buttonControlsOnly">Whether the method should only consider button controls.</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"><paramref name="eventPtr"/> is a <c>null</c> pointer.</exception>
/// <exception cref="ArgumentException"><paramref name="eventPtr"/> is not a <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/> -or-
/// the <see cref="InputDevice"/> referenced by the <see cref="InputEvent.deviceId"/> in the event cannot be found.</exception>
/// <seealso cref="EnumerateChangedControls"/>
/// <seealso cref="ButtonControl.isPressed"/>
public static bool HasButtonPress(this InputEventPtr eventPtr, float magnitude = -1, bool buttonControlsOnly = true)
{
return eventPtr.GetFirstButtonPressOrNull(magnitude, buttonControlsOnly) != null;
}
/// <summary>
/// Get the first pressed button from the given event or null if the event doesn't contain a new button press.
/// </summary>
/// <param name="eventPtr">An event. Must be a <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/>.</param>
/// <param name="magnitude">The threshold value that a control must be actuated by (see
/// <see cref="InputControl.EvaluateMagnitude()"/>) to be considered pressed. If not given, defaults to <see
/// cref="InputSettings.defaultButtonPressPoint"/>.</param>
/// <param name="buttonControlsOnly">Whether the method should only consider <see cref="ButtonControl"/>s. Otherwise,
/// any <see cref="InputControl"/> that has an actuation (see <see cref="InputControl.EvaluateMagnitude()"/>) equal to
/// or greater than the given <paramref name="magnitude"/> will be considered a pressed button. This is 'true' by
/// default.</param>
/// <returns>The control that was pressed.</returns>
/// <exception cref="ArgumentNullException"><paramref name="eventPtr"/> is a <c>null</c> pointer.</exception>
/// <exception cref="ArgumentException">The <see cref="InputDevice"/> referenced by the <see cref="InputEvent.deviceId"/> in the event cannot
/// be found.</exception>
/// <seealso cref="EnumerateChangedControls"/>
/// <seealso cref="ButtonControl.isPressed"/>
/// <remarks>Buttons will be evaluated in the order that they appear in the devices layout i.e. the bit position of each control
/// in the devices state memory. For example, in the gamepad state, button north (bit position 4) will be evaluated before button
/// east (bit position 5), so if both buttons were pressed in the given event, button north would be returned.
/// Note that the function returns null if the <paramref name="eventPtr"/> is not a StateEvent or DeltaStateEvent.</remarks>
public static InputControl GetFirstButtonPressOrNull(this InputEventPtr eventPtr, float magnitude = -1, bool buttonControlsOnly = true)
{
if (eventPtr.type != StateEvent.Type && eventPtr.type != DeltaStateEvent.Type)
return null;
if (magnitude < 0)
magnitude = InputSystem.settings.defaultButtonPressPoint;
foreach (var control in eventPtr.EnumerateControls(Enumerate.IgnoreControlsInDefaultState, magnitudeThreshold: magnitude))
{
if (buttonControlsOnly && !control.isButton)
continue;
return control;
}
return null;
}
/// <summary>
/// Enumerate all pressed buttons in the given event.
/// </summary>
/// <param name="eventPtr">An event. Must be a <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/>.</param>
/// <param name="magnitude">The threshold value that a button must be actuated by to be considered pressed.</param>
/// <param name="buttonControlsOnly">Whether the method should only consider button controls.</param>
/// <returns>An enumerable collection containing all buttons that were pressed in the given event.</returns>
/// <exception cref="ArgumentNullException"><paramref name="eventPtr"/> is a <c>null</c> pointer.</exception>
/// <exception cref="ArgumentException">The <see cref="InputDevice"/> referenced by the <see cref="InputEvent.deviceId"/> in the event cannot be found.</exception>
/// <remarks>Returns an empty enumerable if the <paramref name="eventPtr"/> is not a <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/>.</remarks>
/// <seealso cref="EnumerateChangedControls"/>
/// <seealso cref="ButtonControl.isPressed"/>
public static IEnumerable<InputControl> GetAllButtonPresses(this InputEventPtr eventPtr, float magnitude = -1, bool buttonControlsOnly = true)
{
if (eventPtr.type != StateEvent.Type && eventPtr.type != DeltaStateEvent.Type)
yield break;
if (magnitude < 0)
magnitude = InputSystem.settings.defaultButtonPressPoint;
foreach (var control in eventPtr.EnumerateControls(Enumerate.IgnoreControlsInDefaultState, magnitudeThreshold: magnitude))
{
if (buttonControlsOnly && !control.isButton)
continue;
yield return control;
}
}
/// <summary>
/// Allows iterating over the controls referenced by an <see cref="InputEvent"/> via <see cref="InputEventControlEnumerator"/>.
/// </summary>
/// <seealso cref="InputControlExtensions.EnumerateControls"/>
/// <seealso cref="InputControlExtensions.EnumerateChangedControls"/>
public struct InputEventControlCollection : IEnumerable<InputControl>
{
internal InputDevice m_Device;
internal InputEventPtr m_EventPtr;
internal Enumerate m_Flags;
internal float m_MagnitudeThreshold;
/// <summary>
/// The event being iterated over. A <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/>.
/// </summary>
public InputEventPtr eventPtr => m_EventPtr;
/// <summary>
/// Enumerate the controls in the event.
/// </summary>
/// <returns>An enumerator.</returns>
public InputEventControlEnumerator GetEnumerator()
{
return new InputEventControlEnumerator(m_EventPtr, m_Device, m_Flags, m_MagnitudeThreshold);
}
IEnumerator<InputControl> IEnumerable<InputControl>.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
/// <summary>
/// Iterates over the controls in a <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/>
/// while optionally applying certain filtering criteria.
/// </summary>
/// <remarks>
/// One problem with state events (that is, <see cref="StateEvent"/> and <see cref="DeltaStateEvent"/>)
/// is that they contain raw blocks of memory which may contain state changes for arbitrary many
/// controls on a device at the same time. Locating individual controls and determining which have
/// changed state and how can thus be quite inefficient.
///
/// This helper aims to provide an easy and efficient way to iterate over controls relevant to a
/// given state event. Instead of iterating over the controls of a device looking for the ones
/// relevant to a given event, enumeration is done the opposite by efficiently searching through
/// the memory contained in an event and then mapping data found in the event back to controls
/// on a given device.
///
/// <example>
/// <code>
/// // Inefficient:
/// foreach (var control in device.allControls)
/// {
/// // Skip control if it is noisy, synthetic, or not a leaf control.
/// if (control.synthetic || control.noisy || control.children.Count > 0)
/// continue;
///
/// // Locate the control in the event.
/// var statePtr = eventPtr.GetStatePtrFromStateEvent(eventPtr);
/// if (statePtr == null)
/// continue; // Control not included in event.
///
/// // See if the control is actuated beyond a minimum threshold.
/// if (control.EvaluateMagnitude(statePtr) &lt; 0.001f)
/// continue;
///
/// Debug.Log($"Found actuated control {control}");
/// }
///
/// // Much more efficient:
/// foreach (var control in eventPtr.EnumerateControls(
/// InputControlExtensions.Enumerate.IgnoreControlsInDefaultState,
/// device: device,
/// magnitudeThreshold: 0.001f))
/// {
/// Debug.Log($"Found actuated control {control}");
/// }
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputControlExtensions.EnumerateControls"/>
/// <seealso cref="InputControlExtensions.EnumerateChangedControls"/>
public unsafe struct InputEventControlEnumerator : IEnumerator<InputControl>
{
private Enumerate m_Flags;
private readonly InputDevice m_Device;
private readonly uint[] m_StateOffsetToControlIndex;
private readonly int m_StateOffsetToControlIndexLength;
private readonly InputControl[] m_AllControls;
private byte* m_DefaultState; // Already offset by device offset.
private byte* m_CurrentState;
private byte* m_NoiseMask; // Already offset by device offset.
private InputEventPtr m_EventPtr;
private InputControl m_CurrentControl;
private int m_CurrentIndexInStateOffsetToControlIndexMap;
private uint m_CurrentControlStateBitOffset;
private byte* m_EventState;
private uint m_CurrentBitOffset;
private uint m_EndBitOffset;
private float m_MagnitudeThreshold;
internal InputEventControlEnumerator(InputEventPtr eventPtr, InputDevice device, Enumerate flags, float magnitudeThreshold = 0)
{
Debug.Assert(eventPtr.valid, "eventPtr should be valid at this point");
Debug.Assert(device != null, "Need to have valid device at this point");
m_Device = device;
m_StateOffsetToControlIndex = device.m_StateOffsetToControlMap;
m_StateOffsetToControlIndexLength = m_StateOffsetToControlIndex.LengthSafe();
m_AllControls = device.m_ChildrenForEachControl;
m_EventPtr = eventPtr;
m_Flags = flags;
m_CurrentControl = null;
m_CurrentIndexInStateOffsetToControlIndexMap = default;
m_CurrentControlStateBitOffset = 0;
m_EventState = default;
m_CurrentBitOffset = default;
m_EndBitOffset = default;
m_MagnitudeThreshold = magnitudeThreshold;
if ((flags & Enumerate.IncludeNoisyControls) == 0)
m_NoiseMask = (byte*)device.noiseMaskPtr + device.m_StateBlock.byteOffset;
else
m_NoiseMask = default;
if ((flags & Enumerate.IgnoreControlsInDefaultState) != 0)
m_DefaultState = (byte*)device.defaultStatePtr + device.m_StateBlock.byteOffset;
else
m_DefaultState = default;
if ((flags & Enumerate.IgnoreControlsInCurrentState) != 0)
m_CurrentState = (byte*)device.currentStatePtr + device.m_StateBlock.byteOffset;
else
m_CurrentState = default;
Reset();
}
private bool CheckDefault(uint numBits)
{
return MemoryHelpers.MemCmpBitRegion(m_EventState, m_DefaultState, m_CurrentBitOffset, numBits, m_NoiseMask);
}
private bool CheckCurrent(uint numBits)
{
return MemoryHelpers.MemCmpBitRegion(m_EventState, m_CurrentState, m_CurrentBitOffset, numBits, m_NoiseMask);
}
public bool MoveNext()
{
if (!m_EventPtr.valid)
throw new ObjectDisposedException("Enumerator has already been disposed");
// If we are to include non-leaf controls and we have a current control, walk
// up the tree until we reach the device.
if (m_CurrentControl != null && (m_Flags & Enumerate.IncludeNonLeafControls) != 0)
{
var parent = m_CurrentControl.parent;
if (parent != m_Device)
{
m_CurrentControl = parent;
return true;
}
}
var ignoreDefault = m_DefaultState != null;
var ignoreCurrent = m_CurrentState != null;
// Search for the next control that matches our filter criteria.
while (true)
{
m_CurrentControl = null;
// If we are ignoring certain state values, try to skip over as much memory as we can.
if (ignoreCurrent || ignoreDefault)
{
// If we are not byte-aligned, search whatever bits are left in the current byte.
if ((m_CurrentBitOffset & 0x7) != 0)
{
var bitsLeftInByte = (m_CurrentBitOffset + 8) & 0x7;
if ((ignoreCurrent && CheckCurrent(bitsLeftInByte))
|| (ignoreDefault && CheckDefault(bitsLeftInByte)))
m_CurrentBitOffset += bitsLeftInByte;
}
// Search byte by byte.
while (m_CurrentBitOffset < m_EndBitOffset)
{
var byteOffset = m_CurrentBitOffset >> 3;
var eventByte = m_EventState[byteOffset];
var maskByte = m_NoiseMask != null ? m_NoiseMask[byteOffset] : 0xff;
if (ignoreCurrent)
{
var currentByte = m_CurrentState[byteOffset];
if ((currentByte & maskByte) == (eventByte & maskByte))
{
m_CurrentBitOffset += 8;
continue;
}
}
if (ignoreDefault)
{
var defaultByte = m_DefaultState[byteOffset];
if ((defaultByte & maskByte) == (eventByte & maskByte))
{
m_CurrentBitOffset += 8;
continue;
}
}
break;
}
}
// See if we've reached the end.
if (m_CurrentBitOffset >= m_EndBitOffset
|| m_CurrentIndexInStateOffsetToControlIndexMap >= m_StateOffsetToControlIndexLength) // No more controls.
return false;
// No, so find the control at the current bit offset.
for (;
m_CurrentIndexInStateOffsetToControlIndexMap < m_StateOffsetToControlIndexLength;
++m_CurrentIndexInStateOffsetToControlIndexMap)
{
InputDevice.DecodeStateOffsetToControlMapEntry(
m_StateOffsetToControlIndex[m_CurrentIndexInStateOffsetToControlIndexMap],
out var controlIndex,
out var controlBitOffset,
out var controlBitSize);
// If the control's bit region lies *before* the memory we're looking at,
// skip it.
if (controlBitOffset < m_CurrentControlStateBitOffset ||
m_CurrentBitOffset >= (controlBitOffset + controlBitSize - m_CurrentControlStateBitOffset))
continue;
// If the bit region we're looking at lies *before* the current control,
// keep searching through memory.
if ((controlBitOffset - m_CurrentControlStateBitOffset) >= m_CurrentBitOffset + 8)
{
// Jump to location of control.
m_CurrentBitOffset = controlBitOffset - m_CurrentControlStateBitOffset;
break;
}
// If the control's bit region runs past of what we actually have (may be the case both
// with delta events and normal state events), skip it.
if (controlBitOffset + controlBitSize - m_CurrentControlStateBitOffset > m_EndBitOffset)
continue;
// If the control is byte-aligned both in its start offset and its length,
// we have what we're looking for.
if ((controlBitOffset & 0x7) == 0 && (controlBitSize & 0x7) == 0)
{
m_CurrentControl = m_AllControls[controlIndex];
}
else
{
// Otherwise, we may need to check the bit region specifically for the control.
if ((ignoreCurrent && MemoryHelpers.MemCmpBitRegion(m_EventState, m_CurrentState, controlBitOffset - m_CurrentControlStateBitOffset, controlBitSize, m_NoiseMask))
|| (ignoreDefault && MemoryHelpers.MemCmpBitRegion(m_EventState, m_DefaultState, controlBitOffset - m_CurrentControlStateBitOffset, controlBitSize, m_NoiseMask)))
continue;
m_CurrentControl = m_AllControls[controlIndex];
}
if ((m_Flags & Enumerate.IncludeNoisyControls) == 0 && m_CurrentControl.noisy)
{
m_CurrentControl = null;
continue;
}
if ((m_Flags & Enumerate.IncludeSyntheticControls) == 0)
{
var controlHasSharedState = (m_CurrentControl.m_ControlFlags &
(InputControl.ControlFlags.UsesStateFromOtherControl |
InputControl.ControlFlags.IsSynthetic)) != 0;
// Filter out synthetic and useStateFrom controls.
if (controlHasSharedState)
{
m_CurrentControl = null;
continue;
}
}
++m_CurrentIndexInStateOffsetToControlIndexMap;
break;
}
if (m_CurrentControl != null)
{
// If we are the filter by magnitude, last check is to go let the control evaluate
// its magnitude based on the data in the event and if it's too low, keep searching.
if (m_MagnitudeThreshold != 0)
{
var statePtr = m_EventState - (m_CurrentControlStateBitOffset >> 3) - m_Device.m_StateBlock.byteOffset;
var magnitude = m_CurrentControl.EvaluateMagnitude(statePtr);
if (magnitude >= 0 && magnitude < m_MagnitudeThreshold)
continue;
}
return true;
}
}
}
public void Reset()
{
if (!m_EventPtr.valid)
throw new ObjectDisposedException("Enumerator has already been disposed");
var eventType = m_EventPtr.type;
FourCC stateFormat;
if (eventType == StateEvent.Type)
{
var stateEvent = StateEvent.FromUnchecked(m_EventPtr);
m_EventState = (byte*)stateEvent->state;
m_EndBitOffset = stateEvent->stateSizeInBytes * 8;
m_CurrentBitOffset = 0;
stateFormat = stateEvent->stateFormat;
}
else if (eventType == DeltaStateEvent.Type)
{
var deltaEvent = DeltaStateEvent.FromUnchecked(m_EventPtr);
m_EventState = (byte*)deltaEvent->deltaState - deltaEvent->stateOffset; // We access m_EventState as if it contains a full state event.
m_CurrentBitOffset = deltaEvent->stateOffset * 8;
m_EndBitOffset = m_CurrentBitOffset + deltaEvent->deltaStateSizeInBytes * 8;
stateFormat = deltaEvent->stateFormat;
}
else
{
throw new NotSupportedException($"Cannot iterate over controls in event of type '{eventType}'");
}
m_CurrentIndexInStateOffsetToControlIndexMap = 0;
m_CurrentControlStateBitOffset = 0;
m_CurrentControl = null;
// If the state format of the event does not match that of the device,
// we need to go through the IInputStateCallbackReceiver machinery to adapt.
if (stateFormat != m_Device.m_StateBlock.format)
{
var stateOffset = 0u;
if (m_Device.hasStateCallbacks &&
((IInputStateCallbackReceiver)m_Device).GetStateOffsetForEvent(null, m_EventPtr, ref stateOffset))
{
m_CurrentControlStateBitOffset = stateOffset * 8;
if (m_CurrentState != null)
m_CurrentState += stateOffset;
if (m_DefaultState != null)
m_DefaultState += stateOffset;
if (m_NoiseMask != null)
m_NoiseMask += stateOffset;
}
else
{
// https://fogbugz.unity3d.com/f/cases/1395648/
if (m_Device is Touchscreen && m_EventPtr.IsA<StateEvent>() &&
StateEvent.FromUnchecked(m_EventPtr)->stateFormat == TouchState.Format)
{
// if GetStateOffsetForEvent(null, ...) return false on touchscreen it means that
// we don't have a free slot for incoming touch, so ignore it for now
}
else
throw new InvalidOperationException(
$"{eventType} event with state format {stateFormat} cannot be used with device '{m_Device}'");
}
}
// NOTE: We *could* run a CheckDefault() or even CheckCurrent() over the entire event here to rule
// it out entirely. However, we don't do so based on the assumption that *in general* this will
// only *add* time. Rationale:
//
// - We assume that it is very rare for devices to send events matching the state the device
// already has (i.e. *entire* event is just == current state).
// - We assume that it is less common than the opposite for devices to send StateEvents containing
// nothing but default state. This happens frequently for the keyboard but is very uncommon for mice,
// touchscreens, and gamepads (where the sticks will almost never be exactly at default).
// - We assume that for DeltaStateEvents it is in fact quite common to contain only default state but
// that since in most cases these will contain state for either a very small set of controls or even
// just a single one, the work we do in MoveNext somewhat closely matches that we'd here with a CheckXXX()
// call but that we'd add work to every DeltaStateEvent if we were to have the upfront comparison here.
}
public void Dispose()
{
m_EventPtr = default;
}
public InputControl Current => m_CurrentControl;
object IEnumerator.Current => Current;
}
// Undocumented APIs. Meant to be used only by auto-generated, precompiled layouts.
// These APIs exist solely to keep access to the various properties/fields internal
// and only allow their contents to be modified in a controlled manner.
#region Undocumented
public static ControlBuilder Setup(this InputControl control)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (control.isSetupFinished)
throw new InvalidOperationException($"The setup of {control} cannot be modified; control is already in use");
return new ControlBuilder { control = control };
}
public static DeviceBuilder Setup(this InputDevice device, int controlCount, int usageCount, int aliasCount)
{
if (device == null)
throw new ArgumentNullException(nameof(device));
if (device.isSetupFinished)
throw new InvalidOperationException($"The setup of {device} cannot be modified; control is already in use");
if (controlCount < 1)
throw new ArgumentOutOfRangeException(nameof(controlCount));
if (usageCount < 0)
throw new ArgumentOutOfRangeException(nameof(usageCount));
if (aliasCount < 0)
throw new ArgumentOutOfRangeException(nameof(aliasCount));
device.m_Device = device;
device.m_ChildrenForEachControl = new InputControl[controlCount];
if (usageCount > 0)
{
device.m_UsagesForEachControl = new InternedString[usageCount];
device.m_UsageToControl = new InputControl[usageCount];
}
if (aliasCount > 0)
device.m_AliasesForEachControl = new InternedString[aliasCount];
return new DeviceBuilder { device = device };
}
public struct ControlBuilder
{
public InputControl control { get; internal set; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder At(InputDevice device, int index)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (device == null)
throw new ArgumentNullException(nameof(device));
if (index < 0 || index >= device.m_ChildrenForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(index));
#endif
device.m_ChildrenForEachControl[index] = control;
control.m_Device = device;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithParent(InputControl parent)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (parent == null)
throw new ArgumentNullException(nameof(parent));
if (parent == control)
throw new ArgumentException("Control cannot be its own parent", nameof(parent));
#endif
control.m_Parent = parent;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithName(string name)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (string.IsNullOrEmpty(name))
throw new ArgumentNullException(nameof(name));
#endif
control.m_Name = new InternedString(name);
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithDisplayName(string displayName)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (string.IsNullOrEmpty(displayName))
throw new ArgumentNullException(nameof(displayName));
#endif
control.m_DisplayNameFromLayout = new InternedString(displayName);
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithShortDisplayName(string shortDisplayName)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (string.IsNullOrEmpty(shortDisplayName))
throw new ArgumentNullException(nameof(shortDisplayName));
#endif
control.m_ShortDisplayNameFromLayout = new InternedString(shortDisplayName);
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithLayout(InternedString layout)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (layout.IsEmpty())
throw new ArgumentException("Layout name cannot be empty", nameof(layout));
#endif
control.m_Layout = layout;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithUsages(int startIndex, int count)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (startIndex < 0 || startIndex >= control.device.m_UsagesForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(startIndex));
if (count < 0 || startIndex + count > control.device.m_UsagesForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(count));
#endif
control.m_UsageStartIndex = startIndex;
control.m_UsageCount = count;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithAliases(int startIndex, int count)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (startIndex < 0 || startIndex >= control.device.m_AliasesForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(startIndex));
if (count < 0 || startIndex + count > control.device.m_AliasesForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(count));
#endif
control.m_AliasStartIndex = startIndex;
control.m_AliasCount = count;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithChildren(int startIndex, int count)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (startIndex < 0 || startIndex >= control.device.m_ChildrenForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(startIndex));
if (count < 0 || startIndex + count > control.device.m_ChildrenForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(count));
#endif
control.m_ChildStartIndex = startIndex;
control.m_ChildCount = count;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithStateBlock(InputStateBlock stateBlock)
{
control.m_StateBlock = stateBlock;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithDefaultState(PrimitiveValue value)
{
control.m_DefaultState = value;
control.m_Device.hasControlsWithDefaultState = true;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithMinAndMax(PrimitiveValue min, PrimitiveValue max)
{
control.m_MinValue = min;
control.m_MaxValue = max;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithProcessor<TProcessor, TValue>(TProcessor processor)
where TValue : struct
where TProcessor : InputProcessor<TValue>
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (processor == null)
throw new ArgumentNullException(nameof(processor));
#endif
////REVIEW: have a parameterized version of ControlBuilder<TValue> so we don't need the cast?
////TODO: size array to exact needed size before-hand
((InputControl<TValue>)control).m_ProcessorStack.Append(processor);
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder IsNoisy(bool value)
{
control.noisy = value;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder IsSynthetic(bool value)
{
control.synthetic = value;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder DontReset(bool value)
{
control.dontReset = value;
if (value)
control.m_Device.hasDontResetControls = true;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder IsButton(bool value)
{
control.isButton = value;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Finish()
{
control.isSetupFinished = true;
}
}
public struct DeviceBuilder
{
public InputDevice device { get; internal set; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DeviceBuilder WithName(string name)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (string.IsNullOrEmpty(name))
throw new ArgumentNullException(nameof(name));
#endif
device.m_Name = new InternedString(name);
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DeviceBuilder WithDisplayName(string displayName)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (string.IsNullOrEmpty(displayName))
throw new ArgumentNullException(nameof(displayName));
#endif
device.m_DisplayNameFromLayout = new InternedString(displayName);
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DeviceBuilder WithShortDisplayName(string shortDisplayName)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (string.IsNullOrEmpty(shortDisplayName))
throw new ArgumentNullException(nameof(shortDisplayName));
#endif
device.m_ShortDisplayNameFromLayout = new InternedString(shortDisplayName);
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DeviceBuilder WithLayout(InternedString layout)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (layout.IsEmpty())
throw new ArgumentException("Layout name cannot be empty", nameof(layout));
#endif
device.m_Layout = layout;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DeviceBuilder WithChildren(int startIndex, int count)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (startIndex < 0 || startIndex >= device.device.m_ChildrenForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(startIndex));
if (count < 0 || startIndex + count > device.device.m_ChildrenForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(count));
#endif
device.m_ChildStartIndex = startIndex;
device.m_ChildCount = count;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DeviceBuilder WithStateBlock(InputStateBlock stateBlock)
{
device.m_StateBlock = stateBlock;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DeviceBuilder IsNoisy(bool value)
{
device.noisy = value;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DeviceBuilder WithControlUsage(int controlIndex, InternedString usage, InputControl control)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (controlIndex < 0 || controlIndex >= device.m_UsagesForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(controlIndex));
if (usage.IsEmpty())
throw new ArgumentException(nameof(usage));
if (control == null)
throw new ArgumentNullException(nameof(control));
#endif
device.m_UsagesForEachControl[controlIndex] = usage;
device.m_UsageToControl[controlIndex] = control;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DeviceBuilder WithControlAlias(int controlIndex, InternedString alias)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (controlIndex < 0 || controlIndex >= device.m_AliasesForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(controlIndex));
if (alias.IsEmpty())
throw new ArgumentException(nameof(alias));
#endif
device.m_AliasesForEachControl[controlIndex] = alias;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DeviceBuilder WithStateOffsetToControlIndexMap(uint[] map)
{
device.m_StateOffsetToControlMap = map;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe DeviceBuilder WithControlTree(byte[] controlTreeNodes, ushort[] controlTreeIndicies)
{
var sizeOfNode = UnsafeUtility.SizeOf<InputDevice.ControlBitRangeNode>();
var numNodes = controlTreeNodes.Length / sizeOfNode;
device.m_ControlTreeNodes = new InputDevice.ControlBitRangeNode[numNodes];
fixed(byte* nodePtr = controlTreeNodes)
{
for (var i = 0; i < numNodes; i++)
{
device.m_ControlTreeNodes[i] = *(InputDevice.ControlBitRangeNode*)(nodePtr + i * sizeOfNode);
}
}
device.m_ControlTreeIndices = controlTreeIndicies;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Finish()
{
// Set up the list of just ButtonControls to quickly update press state.
var i = 0;
foreach (var control in device.allControls)
{
// Don't use .isButton here, since this can be called from tests with NULL controls
if (control is ButtonControl)
++i;
}
device.m_ButtonControlsCheckingPressState = new List<ButtonControl>(i);
#if UNITY_2020_1_OR_NEWER
device.m_UpdatedButtons = new HashSet<int>(i);
#else
// 2019 is too old to support setting HashSet capacity
device.m_UpdatedButtons = new HashSet<int>();
#endif
device.isSetupFinished = true;
}
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a8cfbe75f80984c1d9700ef41f6e2aeb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,2323 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
////TODO: *kill* variants!
////TODO: we really need proper verification to be in place to ensure that the resulting layout isn't coming out with a bad memory layout
////TODO: add code-generation that takes a layout and spits out C# code that translates it to a common value format
//// (this can be used, for example, to translate all the various gamepad formats into one single common gamepad format)
////TODO: allow layouts to set default device names
////TODO: allow creating generic controls as parents just to group child controls
////TODO: allow things like "-something" and "+something" for usages, processors, etc
////TODO: allow setting whether the device should automatically become current and whether it wants noise filtering
////TODO: ensure that if a layout sets a device description, it is indeed a device layout
////TODO: make offset on InputControlAttribute relative to field instead of relative to entire state struct
////REVIEW: common usages are on all layouts but only make sense for devices
////REVIEW: useStateFrom seems like a half-measure; it solves the problem of setting up state blocks but they often also
//// require a specific set of processors
////REVIEW: Can we allow aliases to be paths rather than just plain names? This would allow changing the hierarchy around while
//// keeping backwards-compatibility.
// Q: Why is there this layout system instead of just new'ing everything up in hand-written C# code?
// A: The current approach has a couple advantages.
//
// * Since it's data-driven, entire layouts can be represented as just data. They can be added to already deployed applications,
// can be sent over the wire, can be analyzed by tools, etc.
//
// * The layouts can be rearranged in powerful ways, even on the fly. Data can be inserted or modified all along the hierarchy
// both from within a layout itself as well as from outside through overrides. The resulting compositions would often be very
// hard/tedious to set up in a linear C# inheritance hierarchy and likely result in repeated reallocation and rearranging of
// already created setups.
//
// * Related to that, the data-driven layouts make it possible to significantly change the data model without requiring changes
// to existing layouts. This, too, would be more complicated if every device would simply new up everything directly.
//
// * We can generate code from them. Means we can, for example, generate code for the DOTS runtime from the same information
// that exists in the input system but without depending on its InputDevice C# implementation.
//
// The biggest drawback, other than code complexity, is that building an InputDevice from an InputControlLayout is slow.
// This is somewhat offset by having a code generator that can "freeze" a specific layout into simple C# code. For these,
// the result is code at least as efficient (but likely *more* efficient) than the equivalent in a code-only layout approach
// while at the same time offering all the advantages of the data-driven approach.
namespace UnityEngine.InputSystem.Layouts
{
/// <summary>
/// Delegate used by <see cref="InputSystem.onFindLayoutForDevice"/>.
/// </summary>
/// <param name="description">The device description supplied by the runtime or through <see
/// cref="InputSystem.AddDevice(InputDeviceDescription)"/>. This is passed by reference instead of
/// by value to allow the callback to fill out fields such as <see cref="InputDeviceDescription.capabilities"/>
/// on the fly based on information queried from external APIs or from the runtime.</param>
/// <param name="matchedLayout">Name of the layout that has been selected for the device or <c>null</c> if
/// no matching layout could be found. Matching is determined from the <see cref="InputDeviceMatcher"/>s for
/// layouts registered in the system.</param>
/// <param name="executeDeviceCommand">A delegate which can be invoked to execute <see cref="InputDeviceCommand"/>s
/// on the device.</param>
/// <returns> Return <c>null</c> or an empty string to indicate that </returns>
/// <remarks>
/// </remarks>
/// <seealso cref="InputSystem.onFindLayoutForDevice"/>
/// <seealso cref="InputSystem.RegisterLayoutBuilder"/>
/// <seealso cref="InputControlLayout"/>
public delegate string InputDeviceFindControlLayoutDelegate(ref InputDeviceDescription description,
string matchedLayout, InputDeviceExecuteCommandDelegate executeDeviceCommand);
/// <summary>
/// A control layout specifies the composition of an <see cref="InputControl"/> or
/// <see cref="InputDevice"/>.
/// </summary>
/// <remarks>
/// Control layouts can be created in three possible ways:
///
/// <list type="number">
/// <item><description>Loaded from JSON.</description></item>
/// <item><description>Constructed through reflection from <see cref="InputControl">InputControls</see> classes.</description></item>
/// <item><description>Through layout factories using <see cref="InputControlLayout.Builder"/>.</description></item>
/// </list>
///
/// Once constructed, control layouts are immutable (but you can always
/// replace a registered layout in the system and it will affect
/// everything constructed from the layout).
///
/// Control layouts can be for arbitrary control rigs or for entire
/// devices. Device layouts can be matched to <see cref="InputDeviceDescription">
/// device description</see> using associated <see cref="InputDeviceMatcher">
/// device matchers</see>.
///
/// InputControlLayout objects are considered temporaries. Except in the
/// editor, they are not kept around beyond device creation.
///
/// See the <a href="../manual/Layouts.html">manual</a> for more details on control layouts.
/// </remarks>
public class InputControlLayout
{
private static InternedString s_DefaultVariant = new InternedString("Default");
public static InternedString DefaultVariant => s_DefaultVariant;
public const string VariantSeparator = ";";
/// <summary>
/// Specification for the composition of a direct or indirect child control.
/// </summary>
public struct ControlItem
{
/// <summary>
/// Name of the control. Cannot be empty or <c>null</c>.
/// </summary>
/// <value>Name of the control.</value>
/// <remarks>
/// This may also be a path of the form <c>"parentName/childName..."</c>.
/// This can be used to reach inside another layout and modify properties of
/// a control inside of it. An example for this is adding a "leftStick" control
/// using the Stick layout and then adding two control layouts that refer to
/// "leftStick/x" and "leftStick/y" respectively to modify the state format used
/// by the stick.
///
/// This field is required.
/// </remarks>
/// <seealso cref="isModifyingExistingControl"/>
/// <seealso cref="InputControlAttribute.name"/>
public InternedString name { get; internal set; }
/// <summary>
/// Name of the layout to use for the control.
/// </summary>
/// <value>Name of layout to use.</value>
/// <remarks>
/// Must be the name of a control layout, not device layout.
///
/// An example would be "Stick".
/// </remarks>
/// <seealso cref="InputSystem.RegisterLayout(Type,string,Nullable{InputDeviceMatcher}"/>
public InternedString layout { get; internal set; }
public InternedString variants { get; internal set; }
public string useStateFrom { get; internal set; }
/// <summary>
/// Optional display name of the control.
/// </summary>
/// <seealso cref="InputControl.displayName"/>
public string displayName { get; internal set; }
/// <summary>
/// Optional abbreviated display name of the control.
/// </summary>
/// <seealso cref="InputControl.shortDisplayName"/>
public string shortDisplayName { get; internal set; }
public ReadOnlyArray<InternedString> usages { get; internal set; }
public ReadOnlyArray<InternedString> aliases { get; internal set; }
public ReadOnlyArray<NamedValue> parameters { get; internal set; }
public ReadOnlyArray<NameAndParameters> processors { get; internal set; }
public uint offset { get; internal set; }
public uint bit { get; internal set; }
public uint sizeInBits { get; internal set; }
public FourCC format { get; internal set; }
private Flags flags { get; set; }
public int arraySize { get; internal set; }
/// <summary>
/// Optional default value for the state memory associated with the control.
/// </summary>
public PrimitiveValue defaultState { get; internal set; }
public PrimitiveValue minValue { get; internal set; }
public PrimitiveValue maxValue { get; internal set; }
/// <summary>
/// If true, the item will not add a control but rather a modify a control
/// inside the hierarchy added by <see cref="layout"/>. This allows, for example, to modify
/// just the X axis control of the left stick directly from within a gamepad
/// layout instead of having to have a custom stick layout for the left stick
/// than in turn would have to make use of a custom axis layout for the X axis.
/// Instead, you can just have a control layout with the name <c>"leftStick/x"</c>.
/// </summary>
public bool isModifyingExistingControl
{
get => (flags & Flags.isModifyingExistingControl) == Flags.isModifyingExistingControl;
internal set
{
if (value)
flags |= Flags.isModifyingExistingControl;
else
flags &= ~Flags.isModifyingExistingControl;
}
}
/// <summary>
/// Get or set whether to mark the control as noisy.
/// </summary>
/// <value>Whether to mark the control as noisy.</value>
/// <remarks>
/// Noisy controls may generate varying input even without "proper" user interaction. For example,
/// a sensor may generate slightly different input values over time even if in fact the very thing
/// (such as the device orientation) that is being measured is not changing.
/// </remarks>
/// <seealso cref="InputControl.noisy"/>
/// <seealso cref="InputControlAttribute.noisy"/>
public bool isNoisy
{
get => (flags & Flags.IsNoisy) == Flags.IsNoisy;
internal set
{
if (value)
flags |= Flags.IsNoisy;
else
flags &= ~Flags.IsNoisy;
}
}
/// <summary>
/// Get or set whether to mark the control as "synthetic".
/// </summary>
/// <value>Whether to mark the control as synthetic.</value>
/// <remarks>
/// Synthetic controls are artificial controls that provide input but do not correspond to actual controls
/// on the hardware. An example is <see cref="Keyboard.anyKey"/> which is an artificial button that triggers
/// if any key on the keyboard is pressed.
/// </remarks>
/// <seealso cref="InputControl.synthetic"/>
/// <seealso cref="InputControlAttribute.synthetic"/>
public bool isSynthetic
{
get => (flags & Flags.IsSynthetic) == Flags.IsSynthetic;
internal set
{
if (value)
flags |= Flags.IsSynthetic;
else
flags &= ~Flags.IsSynthetic;
}
}
/// <summary>
/// Get or set whether the control should be excluded when performing a device reset.
/// </summary>
/// <value>If true, the control will not get reset in a device reset. Off by default.</value>
/// <remarks>
/// Some controls like, for example, mouse positions do not generally make sense to reset when a
/// device is reset. By setting this flag on, the control's state will be excluded in resets.
///
/// Note that a full reset can still be forced through <see cref="InputSystem.ResetDevice"/> in
/// which case controls that have this flag set will also get reset.
/// </remarks>
/// <seealso cref="InputSystem.ResetDevice"/>
/// <seealso cref="InputControlAttribute.dontReset"/>
public bool dontReset
{
get => (flags & Flags.DontReset) == Flags.DontReset;
internal set
{
if (value)
flags |= Flags.DontReset;
else
flags &= ~Flags.DontReset;
}
}
/// <summary>
/// Whether the control is introduced by the layout.
/// </summary>
/// <value>If true, the control is first introduced by this layout.</value>
/// <remarks>
/// The value of this property is automatically determined by the input system.
/// </remarks>
public bool isFirstDefinedInThisLayout
{
get => (flags & Flags.IsFirstDefinedInThisLayout) != 0;
internal set
{
if (value)
flags |= Flags.IsFirstDefinedInThisLayout;
else
flags &= ~Flags.IsFirstDefinedInThisLayout;
}
}
public bool isArray => (arraySize != 0);
/// <summary>
/// For any property not set on this control layout, take the setting from <paramref name="other"/>.
/// </summary>
/// <param name="other">Control layout providing settings.</param>
/// <remarks>
/// <see cref="name"/> will not be touched.
/// </remarks>
/// <seealso cref="InputControlLayout.MergeLayout"/>
public ControlItem Merge(ControlItem other)
{
var result = new ControlItem();
result.name = name;
Debug.Assert(!name.IsEmpty(), "Name must not be empty");
result.isModifyingExistingControl = isModifyingExistingControl;
result.displayName = string.IsNullOrEmpty(displayName) ? other.displayName : displayName;
result.shortDisplayName = string.IsNullOrEmpty(shortDisplayName) ? other.shortDisplayName : shortDisplayName;
result.layout = layout.IsEmpty() ? other.layout : layout;
result.variants = variants.IsEmpty() ? other.variants : variants;
result.useStateFrom = useStateFrom ?? other.useStateFrom;
result.arraySize = !isArray ? other.arraySize : arraySize;
////FIXME: allow overrides to unset this
result.isNoisy = isNoisy || other.isNoisy;
result.dontReset = dontReset || other.dontReset;
result.isSynthetic = isSynthetic || other.isSynthetic;
result.isFirstDefinedInThisLayout = false;
if (offset != InputStateBlock.InvalidOffset)
result.offset = offset;
else
result.offset = other.offset;
if (bit != InputStateBlock.InvalidOffset)
result.bit = bit;
else
result.bit = other.bit;
if (format != 0)
result.format = format;
else
result.format = other.format;
if (sizeInBits != 0)
result.sizeInBits = sizeInBits;
else
result.sizeInBits = other.sizeInBits;
if (aliases.Count > 0)
result.aliases = aliases;
else
result.aliases = other.aliases;
if (usages.Count > 0)
result.usages = usages;
else
result.usages = other.usages;
////FIXME: this should properly merge the parameters, not just pick one or the other
//// easiest thing may be to just concatenate the two strings
if (parameters.Count == 0)
result.parameters = other.parameters;
else
result.parameters = parameters;
if (processors.Count == 0)
result.processors = other.processors;
else
result.processors = processors;
if (!string.IsNullOrEmpty(displayName))
result.displayName = displayName;
else
result.displayName = other.displayName;
if (!defaultState.isEmpty)
result.defaultState = defaultState;
else
result.defaultState = other.defaultState;
if (!minValue.isEmpty)
result.minValue = minValue;
else
result.minValue = other.minValue;
if (!maxValue.isEmpty)
result.maxValue = maxValue;
else
result.maxValue = other.maxValue;
return result;
}
[Flags]
private enum Flags
{
isModifyingExistingControl = 1 << 0,
IsNoisy = 1 << 1,
IsSynthetic = 1 << 2,
IsFirstDefinedInThisLayout = 1 << 3,
DontReset = 1 << 4,
}
}
// Unique name of the layout.
// NOTE: Case-insensitive.
public InternedString name => m_Name;
public string displayName => m_DisplayName ?? m_Name;
public Type type => m_Type;
public InternedString variants => m_Variants;
public FourCC stateFormat => m_StateFormat;
public int stateSizeInBytes => m_StateSizeInBytes;
public IEnumerable<InternedString> baseLayouts => m_BaseLayouts;
public IEnumerable<InternedString> appliedOverrides => m_AppliedOverrides;
public ReadOnlyArray<InternedString> commonUsages => new ReadOnlyArray<InternedString>(m_CommonUsages);
/// <summary>
/// List of child controls defined for the layout.
/// </summary>
/// <value>Child controls defined for the layout.</value>
public ReadOnlyArray<ControlItem> controls => new ReadOnlyArray<ControlItem>(m_Controls);
////FIXME: this should be a `bool?`
public bool updateBeforeRender => m_UpdateBeforeRender ?? false;
public bool isDeviceLayout => typeof(InputDevice).IsAssignableFrom(m_Type);
public bool isControlLayout => !isDeviceLayout;
/// <summary>
/// Whether the layout is applies overrides to other layouts instead of
/// defining a layout by itself.
/// </summary>
/// <value>True if the layout acts as an override.</value>
/// <seealso cref="InputSystem.RegisterLayoutOverride"/>
public bool isOverride
{
get => (m_Flags & Flags.IsOverride) != 0;
internal set
{
if (value)
m_Flags |= Flags.IsOverride;
else
m_Flags &= ~Flags.IsOverride;
}
}
public bool isGenericTypeOfDevice
{
get => (m_Flags & Flags.IsGenericTypeOfDevice) != 0;
internal set
{
if (value)
m_Flags |= Flags.IsGenericTypeOfDevice;
else
m_Flags &= ~Flags.IsGenericTypeOfDevice;
}
}
public bool hideInUI
{
get => (m_Flags & Flags.HideInUI) != 0;
internal set
{
if (value)
m_Flags |= Flags.HideInUI;
else
m_Flags &= ~Flags.HideInUI;
}
}
/// <summary>
/// Mark the input device created from this layout as noisy, irrespective of whether or not any
/// of its controls have been marked as noisy.
/// </summary>
/// <seealso cref="InputControlLayoutAttribute.isNoisy"/>
public bool isNoisy
{
get => (m_Flags & Flags.IsNoisy) != 0;
internal set
{
if (value)
m_Flags |= Flags.IsNoisy;
else
m_Flags &= ~Flags.IsNoisy;
}
}
/// <summary>
/// Override value for <see cref="InputDevice.canRunInBackground"/>. If this is set by the
/// layout, it will prevent <see cref="QueryCanRunInBackground"/> from being issued. However, other
/// logic that affects <see cref="InputDevice.canRunInBackground"/> may still force a specific value
/// on a device regardless of what's set in the layout.
/// </summary>
/// <seealso cref="InputDevice.canRunInBackground"/>
/// <seealso cref="InputSettings.backgroundBehavior"/>
public bool? canRunInBackground
{
get => (m_Flags & Flags.CanRunInBackgroundIsSet) != 0 ? (bool?)((m_Flags & Flags.CanRunInBackground) != 0) : null;
internal set
{
if (!value.HasValue)
{
m_Flags &= ~Flags.CanRunInBackgroundIsSet;
}
else
{
m_Flags |= Flags.CanRunInBackgroundIsSet;
if (value.Value)
m_Flags |= Flags.CanRunInBackground;
else
m_Flags &= ~Flags.CanRunInBackground;
}
}
}
public ControlItem this[string path]
{
get
{
if (string.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));
// Does not use FindControl so that we don't force-intern the given path string.
if (m_Controls != null)
{
for (var i = 0; i < m_Controls.Length; ++i)
{
if (m_Controls[i].name == path)
return m_Controls[i];
}
}
throw new KeyNotFoundException($"Cannot find control '{path}' in layout '{name}'");
}
}
public ControlItem? FindControl(InternedString path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));
if (m_Controls == null)
return null;
for (var i = 0; i < m_Controls.Length; ++i)
{
if (m_Controls[i].name == path)
return m_Controls[i];
}
return null;
}
public ControlItem? FindControlIncludingArrayElements(string path, out int arrayIndex)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));
arrayIndex = -1;
if (m_Controls == null)
return null;
var arrayIndexAccumulated = 0;
var lastDigitIndex = path.Length;
while (lastDigitIndex > 0 && char.IsDigit(path[lastDigitIndex - 1]))
{
--lastDigitIndex;
arrayIndexAccumulated *= 10;
arrayIndexAccumulated += path[lastDigitIndex] - '0';
}
var arrayNameLength = 0;
if (lastDigitIndex < path.Length && lastDigitIndex > 0) // Protect against name being all digits.
arrayNameLength = lastDigitIndex;
for (var i = 0; i < m_Controls.Length; ++i)
{
ref var control = ref m_Controls[i];
if (string.Compare(control.name, path, StringComparison.InvariantCultureIgnoreCase) == 0)
return control;
////FIXME: what this can't handle is "outerArray4/innerArray5"; not sure we care, though
// NOTE: This will *not* match something like "touch4/tap". Which is what we want.
// In case there is a ControlItem
if (control.isArray && arrayNameLength > 0 && arrayNameLength == control.name.length &&
string.Compare(control.name.ToString(), 0, path, 0, arrayNameLength,
StringComparison.InvariantCultureIgnoreCase) == 0)
{
arrayIndex = arrayIndexAccumulated;
return control;
}
}
return null;
}
/// <summary>
/// Return the type of values produced by controls created from the layout.
/// </summary>
/// <returns>The value type of the control or null if it cannot be determined.</returns>
/// <remarks>
/// This method only returns the statically inferred value type. This type corresponds
/// to the type argument to <see cref="InputControl{TValue}"/> in the inheritance hierarchy
/// of <see cref="type"/>. As the type used by the layout may not inherit from
/// <see cref="InputControl{TValue}"/>, this may mean that the value type cannot be inferred
/// and the method will return null.
/// </remarks>
/// <seealso cref="InputControl.valueType"/>
public Type GetValueType()
{
return TypeHelpers.GetGenericTypeArgumentFromHierarchy(type, typeof(InputControl<>), 0);
}
/// <summary>
/// Build a layout programmatically. Primarily for use by layout builders
/// registered with the system.
/// </summary>
/// <seealso cref="InputSystem.RegisterLayoutBuilder"/>
public class Builder
{
/// <summary>
/// Name to assign to the layout.
/// </summary>
/// <value>Name to assign to the layout.</value>
/// <seealso cref="InputControlLayout.name"/>
public string name { get; set; }
/// <summary>
/// Display name to assign to the layout.
/// </summary>
/// <value>Display name to assign to the layout</value>
/// <seealso cref="InputControlLayout.displayName"/>
public string displayName { get; set; }
/// <summary>
/// <see cref="InputControl"/> type to instantiate for the layout.
/// </summary>
/// <value>Control type to instantiate for the layout.</value>
/// <seealso cref="InputControlLayout.type"/>
public Type type { get; set; }
/// <summary>
/// Memory format FourCC code to apply to state memory used by the
/// layout.
/// </summary>
/// <value>FourCC memory format tag.</value>
/// <seealso cref="InputControlLayout.stateFormat"/>
/// <seealso cref="InputStateBlock.format"/>
public FourCC stateFormat { get; set; }
/// <summary>
/// Total size of memory used by the layout.
/// </summary>
/// <value>Size of memory used by the layout.</value>
/// <seealso cref="InputControlLayout.stateSizeInBytes"/>
public int stateSizeInBytes { get; set; }
/// <summary>
/// Which layout to base this layout on.
/// </summary>
/// <value>Name of base layout.</value>
/// <seealso cref="InputControlLayout.baseLayouts"/>
public string extendsLayout
{
get => m_ExtendsLayout;
set
{
if (!string.IsNullOrEmpty(value))
m_ExtendsLayout = value;
else
m_ExtendsLayout = null;
}
}
private string m_ExtendsLayout;
/// <summary>
/// For device layouts, whether the device wants an extra update
/// before rendering.
/// </summary>
/// <value>True if before-render updates should be enabled for the device.</value>
/// <seealso cref="InputDevice.updateBeforeRender"/>
/// <seealso cref="InputControlLayout.updateBeforeRender"/>
public bool? updateBeforeRender { get; set; }
/// <summary>
/// List of control items set up by the layout.
/// </summary>
/// <value>Controls set up by the layout.</value>
/// <seealso cref="AddControl"/>
public ReadOnlyArray<ControlItem> controls => new ReadOnlyArray<ControlItem>(m_Controls, 0, m_ControlCount);
private int m_ControlCount;
private ControlItem[] m_Controls;
/// <summary>
/// Syntax for configuring an individual <see cref="ControlItem"/>.
/// </summary>
public struct ControlBuilder
{
internal Builder builder;
internal int index;
public ControlBuilder WithDisplayName(string displayName)
{
builder.m_Controls[index].displayName = displayName;
return this;
}
public ControlBuilder WithLayout(string layout)
{
if (string.IsNullOrEmpty(layout))
throw new ArgumentException("Layout name cannot be null or empty", nameof(layout));
builder.m_Controls[index].layout = new InternedString(layout);
return this;
}
public ControlBuilder WithFormat(FourCC format)
{
builder.m_Controls[index].format = format;
return this;
}
public ControlBuilder WithFormat(string format)
{
return WithFormat(new FourCC(format));
}
public ControlBuilder WithByteOffset(uint offset)
{
builder.m_Controls[index].offset = offset;
return this;
}
public ControlBuilder WithBitOffset(uint bit)
{
builder.m_Controls[index].bit = bit;
return this;
}
public ControlBuilder IsSynthetic(bool value)
{
builder.m_Controls[index].isSynthetic = value;
return this;
}
public ControlBuilder IsNoisy(bool value)
{
builder.m_Controls[index].isNoisy = value;
return this;
}
public ControlBuilder DontReset(bool value)
{
builder.m_Controls[index].dontReset = value;
return this;
}
public ControlBuilder WithSizeInBits(uint sizeInBits)
{
builder.m_Controls[index].sizeInBits = sizeInBits;
return this;
}
public ControlBuilder WithRange(float minValue, float maxValue)
{
builder.m_Controls[index].minValue = minValue;
builder.m_Controls[index].maxValue = maxValue;
return this;
}
public ControlBuilder WithUsages(params InternedString[] usages)
{
if (usages == null || usages.Length == 0)
return this;
for (var i = 0; i < usages.Length; ++i)
if (usages[i].IsEmpty())
throw new ArgumentException(
$"Empty usage entry at index {i} for control '{builder.m_Controls[index].name}' in layout '{builder.name}'",
nameof(usages));
builder.m_Controls[index].usages = new ReadOnlyArray<InternedString>(usages);
return this;
}
public ControlBuilder WithUsages(IEnumerable<string> usages)
{
var usagesArray = usages.Select(x => new InternedString(x)).ToArray();
return WithUsages(usagesArray);
}
public ControlBuilder WithUsages(params string[] usages)
{
return WithUsages((IEnumerable<string>)usages);
}
public ControlBuilder WithParameters(string parameters)
{
if (string.IsNullOrEmpty(parameters))
return this;
var parsed = NamedValue.ParseMultiple(parameters);
builder.m_Controls[index].parameters = new ReadOnlyArray<NamedValue>(parsed);
return this;
}
public ControlBuilder WithProcessors(string processors)
{
if (string.IsNullOrEmpty(processors))
return this;
var parsed = NameAndParameters.ParseMultiple(processors).ToArray();
builder.m_Controls[index].processors = new ReadOnlyArray<NameAndParameters>(parsed);
return this;
}
public ControlBuilder WithDefaultState(PrimitiveValue value)
{
builder.m_Controls[index].defaultState = value;
return this;
}
public ControlBuilder UsingStateFrom(string path)
{
if (string.IsNullOrEmpty(path))
return this;
builder.m_Controls[index].useStateFrom = path;
return this;
}
public ControlBuilder AsArrayOfControlsWithSize(int arraySize)
{
builder.m_Controls[index].arraySize = arraySize;
return this;
}
}
// This invalidates the ControlBuilders from previous calls! (our array may move)
/// <summary>
/// Add a new control to the layout.
/// </summary>
/// <param name="name">Name or path of the control. If it is a path (e.g. <c>"leftStick/x"</c>,
/// then the control either modifies the setup of a child control of another control in the layout
/// or adds a new child control to another control in the layout. Modifying child control is useful,
/// for example, to alter the state format of controls coming from the base layout. Likewise,
/// adding child controls to another control is useful to modify the setup of of the control layout
/// being used without having to create and register a custom control layout.</param>
/// <returns>A control builder that permits setting various parameters on the control.</returns>
/// <exception cref="ArgumentException"><paramref name="name"/> is null or empty.</exception>
public ControlBuilder AddControl(string name)
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException(name);
var index = ArrayHelpers.AppendWithCapacity(ref m_Controls, ref m_ControlCount,
new ControlItem
{
name = new InternedString(name),
isModifyingExistingControl = name.IndexOf('/') != -1,
offset = InputStateBlock.InvalidOffset,
bit = InputStateBlock.InvalidOffset
});
return new ControlBuilder
{
builder = this,
index = index
};
}
public Builder WithName(string name)
{
this.name = name;
return this;
}
public Builder WithDisplayName(string displayName)
{
this.displayName = displayName;
return this;
}
public Builder WithType<T>()
where T : InputControl
{
type = typeof(T);
return this;
}
public Builder WithFormat(FourCC format)
{
stateFormat = format;
return this;
}
public Builder WithFormat(string format)
{
return WithFormat(new FourCC(format));
}
public Builder WithSizeInBytes(int sizeInBytes)
{
stateSizeInBytes = sizeInBytes;
return this;
}
public Builder Extend(string baseLayoutName)
{
extendsLayout = baseLayoutName;
return this;
}
public InputControlLayout Build()
{
ControlItem[] controls = null;
if (m_ControlCount > 0)
{
controls = new ControlItem[m_ControlCount];
Array.Copy(m_Controls, controls, m_ControlCount);
}
// Allow layout to be unnamed. The system will automatically set the
// name that the layout has been registered under.
var layout =
new InputControlLayout(new InternedString(name),
type == null && string.IsNullOrEmpty(extendsLayout) ? typeof(InputDevice) : type)
{
m_DisplayName = displayName,
m_StateFormat = stateFormat,
m_StateSizeInBytes = stateSizeInBytes,
m_BaseLayouts = !string.IsNullOrEmpty(extendsLayout) ? new InlinedArray<InternedString>(new InternedString(extendsLayout)) : default,
m_Controls = controls,
m_UpdateBeforeRender = updateBeforeRender
};
return layout;
}
}
// Uses reflection to construct a layout from the given type.
// Can be used with both control classes and state structs.
public static InputControlLayout FromType(string name, Type type)
{
var controlLayouts = new List<ControlItem>();
var layoutAttribute = type.GetCustomAttribute<InputControlLayoutAttribute>(true);
// If there's an InputControlLayoutAttribute on the type that has 'stateType' set,
// add control layouts from its state (if present) instead of from the type.
var stateFormat = new FourCC();
if (layoutAttribute != null && layoutAttribute.stateType != null)
{
AddControlItems(layoutAttribute.stateType, controlLayouts, name);
// Get state type code from state struct.
if (typeof(IInputStateTypeInfo).IsAssignableFrom(layoutAttribute.stateType))
{
stateFormat = ((IInputStateTypeInfo)Activator.CreateInstance(layoutAttribute.stateType)).format;
}
}
else
{
// Add control layouts from type contents.
AddControlItems(type, controlLayouts, name);
}
if (layoutAttribute != null && !string.IsNullOrEmpty(layoutAttribute.stateFormat))
stateFormat = new FourCC(layoutAttribute.stateFormat);
// Determine variants (if any).
var variants = new InternedString();
if (layoutAttribute != null)
variants = new InternedString(layoutAttribute.variants);
////TODO: make sure all usages are unique (probably want to have a check method that we can run on json layouts as well)
////TODO: make sure all paths are unique (only relevant for JSON layouts?)
// Create layout object.
var layout = new InputControlLayout(name, type)
{
m_Controls = controlLayouts.ToArray(),
m_StateFormat = stateFormat,
m_Variants = variants,
m_UpdateBeforeRender = layoutAttribute?.updateBeforeRenderInternal,
isGenericTypeOfDevice = layoutAttribute?.isGenericTypeOfDevice ?? false,
hideInUI = layoutAttribute?.hideInUI ?? false,
m_Description = layoutAttribute?.description,
m_DisplayName = layoutAttribute?.displayName,
canRunInBackground = layoutAttribute?.canRunInBackgroundInternal,
isNoisy = layoutAttribute?.isNoisy ?? false
};
if (layoutAttribute?.commonUsages != null)
layout.m_CommonUsages =
ArrayHelpers.Select(layoutAttribute.commonUsages, x => new InternedString(x));
return layout;
}
public string ToJson()
{
var layout = LayoutJson.FromLayout(this);
return JsonUtility.ToJson(layout, true);
}
// Constructs a layout from the given JSON source.
public static InputControlLayout FromJson(string json)
{
var layoutJson = JsonUtility.FromJson<LayoutJson>(json);
return layoutJson.ToLayout();
}
////REVIEW: shouldn't state be split between input and output? how does output fit into the layout picture in general?
//// should the control layout alone determine the direction things are going in?
private InternedString m_Name;
private Type m_Type; // For extension chains, we can only discover types after loading multiple layouts, so we make this accessible to InputDeviceBuilder.
private InternedString m_Variants;
private FourCC m_StateFormat;
internal int m_StateSizeInBytes; // Note that this is the combined state size for input and output.
internal bool? m_UpdateBeforeRender;
internal InlinedArray<InternedString> m_BaseLayouts;
private InlinedArray<InternedString> m_AppliedOverrides;
private InternedString[] m_CommonUsages;
internal ControlItem[] m_Controls;
internal string m_DisplayName;
private string m_Description;
private Flags m_Flags;
[Flags]
private enum Flags
{
IsGenericTypeOfDevice = 1 << 0,
HideInUI = 1 << 1,
IsOverride = 1 << 2,
CanRunInBackground = 1 << 3,
CanRunInBackgroundIsSet = 1 << 4,
IsNoisy = 1 << 5
}
private InputControlLayout(string name, Type type)
{
m_Name = new InternedString(name);
m_Type = type;
}
private static void AddControlItems(Type type, List<ControlItem> controlLayouts, string layoutName)
{
AddControlItemsFromFields(type, controlLayouts, layoutName);
AddControlItemsFromProperties(type, controlLayouts, layoutName);
}
// Add ControlLayouts for every public property in the given type that has
// InputControlAttribute applied to it or has an InputControl-derived value type.
private static void AddControlItemsFromFields(Type type, List<ControlItem> controlLayouts, string layoutName)
{
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
AddControlItemsFromMembers(fields, controlLayouts, layoutName);
}
// Add ControlLayouts for every public property in the given type that has
// InputControlAttribute applied to it or has an InputControl-derived value type.
private static void AddControlItemsFromProperties(Type type, List<ControlItem> controlLayouts, string layoutName)
{
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
AddControlItemsFromMembers(properties, controlLayouts, layoutName);
}
// Add ControlLayouts for every member in the list that has InputControlAttribute applied to it
// or has an InputControl-derived value type.
private static void AddControlItemsFromMembers(MemberInfo[] members, List<ControlItem> controlItems, string layoutName)
{
foreach (var member in members)
{
// Skip anything declared inside InputControl itself.
// Filters out m_Device etc.
if (member.DeclaringType == typeof(InputControl))
continue;
var valueType = TypeHelpers.GetValueType(member);
// If the value type of the member is a struct type and implements the IInputStateTypeInfo
// interface, dive inside and look. This is useful for composing states of one another.
if (valueType != null && valueType.IsValueType && typeof(IInputStateTypeInfo).IsAssignableFrom(valueType))
{
var controlCountBefore = controlItems.Count;
AddControlItems(valueType, controlItems, layoutName);
// If the current member is a field that is embedding the state structure, add
// the field offset to all control layouts that were added from the struct.
var memberAsField = member as FieldInfo;
if (memberAsField != null)
{
var fieldOffset = Marshal.OffsetOf(member.DeclaringType, member.Name).ToInt32();
var controlCountAfter = controlItems.Count;
for (var i = controlCountBefore; i < controlCountAfter; ++i)
{
var controlLayout = controlItems[i];
if (controlItems[i].offset != InputStateBlock.InvalidOffset)
{
controlLayout.offset += (uint)fieldOffset;
controlItems[i] = controlLayout;
}
}
}
////TODO: allow attributes on the member to modify control layouts inside the struct
}
// Look for InputControlAttributes. If they aren't there, the member has to be
// of an InputControl-derived value type.
var attributes = member.GetCustomAttributes<InputControlAttribute>(false).ToArray();
if (attributes.Length == 0)
{
if (valueType == null || !typeof(InputControl).IsAssignableFrom(valueType))
continue;
// On properties, we require explicit [InputControl] attributes to
// pick them up. Doing it otherwise has proven to lead too easily to
// situations where you inadvertently add new controls to a layout
// just because you added an InputControl-type property to a class.
if (member is PropertyInfo)
continue;
}
AddControlItemsFromMember(member, attributes, controlItems);
}
}
private static void AddControlItemsFromMember(MemberInfo member,
InputControlAttribute[] attributes, List<ControlItem> controlItems)
{
// InputControlAttribute can be applied multiple times to the same member,
// generating a separate control for each occurrence. However, it can also
// generating a separate control for each occurrence. However, it can also
// not be applied at all in which case we still add a control layout (the
// logic that called us already made sure the member is eligible for this kind
// of operation).
if (attributes.Length == 0)
{
var controlItem = CreateControlItemFromMember(member, null);
controlItems.Add(controlItem);
}
else
{
foreach (var attribute in attributes)
{
var controlItem = CreateControlItemFromMember(member, attribute);
controlItems.Add(controlItem);
}
}
}
private static ControlItem CreateControlItemFromMember(MemberInfo member, InputControlAttribute attribute)
{
////REVIEW: make sure that the value type of the field and the value type of the control match?
// Determine name.
var name = attribute?.name;
if (string.IsNullOrEmpty(name))
name = member.Name;
var isModifyingChildControlByPath = name.IndexOf('/') != -1;
// Determine display name.
var displayName = attribute?.displayName;
var shortDisplayName = attribute?.shortDisplayName;
// Determine layout.
var layout = attribute?.layout;
if (string.IsNullOrEmpty(layout) && !isModifyingChildControlByPath &&
(!(member is FieldInfo) || member.GetCustomAttribute<FixedBufferAttribute>(false) == null)) // Ignore fixed buffer fields.
{
var valueType = TypeHelpers.GetValueType(member);
layout = InferLayoutFromValueType(valueType);
}
// Determine variants.
string variants = null;
if (attribute != null && !string.IsNullOrEmpty(attribute.variants))
variants = attribute.variants;
// Determine offset.
var offset = InputStateBlock.InvalidOffset;
if (attribute != null && attribute.offset != InputStateBlock.InvalidOffset)
offset = attribute.offset;
else if (member is FieldInfo && !isModifyingChildControlByPath)
offset = (uint)Marshal.OffsetOf(member.DeclaringType, member.Name).ToInt32();
// Determine bit offset.
var bit = InputStateBlock.InvalidOffset;
if (attribute != null)
bit = attribute.bit;
////TODO: if size is not set, determine from type of field
// Determine size.
var sizeInBits = 0u;
if (attribute != null)
sizeInBits = attribute.sizeInBits;
// Determine format.
var format = new FourCC();
if (attribute != null && !string.IsNullOrEmpty(attribute.format))
format = new FourCC(attribute.format);
else if (!isModifyingChildControlByPath && bit == InputStateBlock.InvalidOffset)
{
////REVIEW: this logic makes it hard to inherit settings from the base layout; if we do this stuff,
//// we should probably do it in InputDeviceBuilder and not directly on the layout
var valueType = TypeHelpers.GetValueType(member);
format = InputStateBlock.GetPrimitiveFormatFromType(valueType);
}
// Determine aliases.
InternedString[] aliases = null;
if (attribute != null)
{
var joined = ArrayHelpers.Join(attribute.alias, attribute.aliases);
if (joined != null)
aliases = joined.Select(x => new InternedString(x)).ToArray();
}
// Determine usages.
InternedString[] usages = null;
if (attribute != null)
{
var joined = ArrayHelpers.Join(attribute.usage, attribute.usages);
if (joined != null)
usages = joined.Select(x => new InternedString(x)).ToArray();
}
// Determine parameters.
NamedValue[] parameters = null;
if (attribute != null && !string.IsNullOrEmpty(attribute.parameters))
parameters = NamedValue.ParseMultiple(attribute.parameters);
// Determine processors.
NameAndParameters[] processors = null;
if (attribute != null && !string.IsNullOrEmpty(attribute.processors))
processors = NameAndParameters.ParseMultiple(attribute.processors).ToArray();
// Determine whether to use state from another control.
string useStateFrom = null;
if (attribute != null && !string.IsNullOrEmpty(attribute.useStateFrom))
useStateFrom = attribute.useStateFrom;
// Determine if it's a noisy control.
var isNoisy = false;
if (attribute != null)
isNoisy = attribute.noisy;
// Determine whether it's a dontReset control.
var dontReset = false;
if (attribute != null)
dontReset = attribute.dontReset;
// Determine if it's a synthetic control.
var isSynthetic = false;
if (attribute != null)
isSynthetic = attribute.synthetic;
// Determine array size.
var arraySize = 0;
if (attribute != null)
arraySize = attribute.arraySize;
// Determine default state.
var defaultState = new PrimitiveValue();
if (attribute != null)
defaultState = PrimitiveValue.FromObject(attribute.defaultState);
// Determine min and max value.
var minValue = new PrimitiveValue();
var maxValue = new PrimitiveValue();
if (attribute != null)
{
minValue = PrimitiveValue.FromObject(attribute.minValue);
maxValue = PrimitiveValue.FromObject(attribute.maxValue);
}
return new ControlItem
{
name = new InternedString(name),
displayName = displayName,
shortDisplayName = shortDisplayName,
layout = new InternedString(layout),
variants = new InternedString(variants),
useStateFrom = useStateFrom,
format = format,
offset = offset,
bit = bit,
sizeInBits = sizeInBits,
parameters = new ReadOnlyArray<NamedValue>(parameters),
processors = new ReadOnlyArray<NameAndParameters>(processors),
usages = new ReadOnlyArray<InternedString>(usages),
aliases = new ReadOnlyArray<InternedString>(aliases),
isModifyingExistingControl = isModifyingChildControlByPath,
isFirstDefinedInThisLayout = true,
isNoisy = isNoisy,
dontReset = dontReset,
isSynthetic = isSynthetic,
arraySize = arraySize,
defaultState = defaultState,
minValue = minValue,
maxValue = maxValue,
};
}
////REVIEW: this tends to cause surprises; is it worth its cost?
private static string InferLayoutFromValueType(Type type)
{
var layout = s_Layouts.TryFindLayoutForType(type);
if (layout.IsEmpty())
{
var typeName = new InternedString(type.Name);
if (s_Layouts.HasLayout(typeName))
layout = typeName;
else if (type.Name.EndsWith("Control"))
{
typeName = new InternedString(type.Name.Substring(0, type.Name.Length - "Control".Length));
if (s_Layouts.HasLayout(typeName))
layout = typeName;
}
}
return layout;
}
/// <summary>
/// Merge the settings from <paramref name="other"/> into the layout such that they become
/// the base settings.
/// </summary>
/// <param name="other"></param>
/// <remarks>
/// This is the central method for allowing layouts to 'inherit' settings from their
/// base layout. It will merge the information in <paramref name="other"/> into the current
/// layout such that the existing settings in the current layout acts as if applied on top
/// of the settings in the base layout.
/// </remarks>
public void MergeLayout(InputControlLayout other)
{
if (other == null)
throw new ArgumentNullException(nameof(other));
m_UpdateBeforeRender = m_UpdateBeforeRender ?? other.m_UpdateBeforeRender;
if (m_Variants.IsEmpty())
m_Variants = other.m_Variants;
// Determine type. Basically, if the other layout's type is more specific
// than our own, we switch to that one. Otherwise we stay on our own type.
if (m_Type == null)
m_Type = other.m_Type;
else if (m_Type.IsAssignableFrom(other.m_Type))
m_Type = other.m_Type;
// If the layout has variants set on it, we want to merge away information coming
// from 'other' than isn't relevant to those variants.
var layoutIsTargetingSpecificVariants = !m_Variants.IsEmpty();
if (m_StateFormat == new FourCC())
m_StateFormat = other.m_StateFormat;
// Combine common usages.
m_CommonUsages = ArrayHelpers.Merge(other.m_CommonUsages, m_CommonUsages);
// Retain list of overrides.
m_AppliedOverrides.Merge(other.m_AppliedOverrides);
// Inherit display name.
if (string.IsNullOrEmpty(m_DisplayName))
m_DisplayName = other.m_DisplayName;
// Merge controls.
if (m_Controls == null)
{
m_Controls = other.m_Controls;
}
else if (other.m_Controls != null)
{
var baseControls = other.m_Controls;
// Even if the counts match we don't know how many controls are in the
// set until we actually gone through both control lists and looked at
// the names.
var controls = new List<ControlItem>();
var baseControlVariants = new List<string>();
////REVIEW: should setting variants directly on a layout force that variant to automatically
//// be set on every control item directly defined in that layout?
var baseControlTable = CreateLookupTableForControls(baseControls, baseControlVariants);
var thisControlTable = CreateLookupTableForControls(m_Controls);
// First go through every control we have in this layout. Add every control from
// `thisControlTable` while removing corresponding control items from `baseControlTable`.
foreach (var pair in thisControlTable)
{
if (baseControlTable.TryGetValue(pair.Key, out var baseControlItem))
{
var mergedLayout = pair.Value.Merge(baseControlItem);
controls.Add(mergedLayout);
// Remove the entry so we don't hit it again in the pass through
// baseControlTable below.
baseControlTable.Remove(pair.Key);
}
////REVIEW: is this really the most useful behavior?
// We may be looking at a control that is using variants on the base layout but
// isn't targeting specific variants on the derived layout. In that case, we
// want to take each of the variants from the base layout and merge them with
// the control layout in the derived layout.
else if (pair.Value.variants.IsEmpty() || pair.Value.variants == DefaultVariant)
{
var isTargetingVariants = false;
if (layoutIsTargetingSpecificVariants)
{
// We're only looking for specific variants so try only that those.
for (var i = 0; i < baseControlVariants.Count; ++i)
{
if (VariantsMatch(m_Variants.ToLower(), baseControlVariants[i]))
{
var key = $"{pair.Key}@{baseControlVariants[i]}";
if (baseControlTable.TryGetValue(key, out baseControlItem))
{
var mergedLayout = pair.Value.Merge(baseControlItem);
controls.Add(mergedLayout);
baseControlTable.Remove(key);
isTargetingVariants = true;
}
}
}
}
else
{
// Try each variants present in the base layout.
foreach (var variant in baseControlVariants)
{
var key = $"{pair.Key}@{variant}";
if (baseControlTable.TryGetValue(key, out baseControlItem))
{
var mergedLayout = pair.Value.Merge(baseControlItem);
controls.Add(mergedLayout);
baseControlTable.Remove(key);
isTargetingVariants = true;
}
}
}
// Okay, this control item isn't corresponding to anything in the base layout
// so just add it as is.
if (!isTargetingVariants)
controls.Add(pair.Value);
}
// We may be looking at a control that is targeting a specific variant
// in this layout but not targeting a variant in the base layout. We still want to
// merge information from that non-targeted base control.
else if (baseControlTable.TryGetValue(pair.Value.name.ToLower(), out baseControlItem))
{
var mergedLayout = pair.Value.Merge(baseControlItem);
controls.Add(mergedLayout);
baseControlTable.Remove(pair.Value.name.ToLower());
}
// Seems like we can't match it to a control in the base layout. We already know it
// must have a variants setting (because we checked above) so if the variants setting
// doesn't prevent us, just include the control. It's most likely a path-modifying
// control (e.g. "rightStick/x").
else if (VariantsMatch(m_Variants, pair.Value.variants))
{
controls.Add(pair.Value);
}
}
// And then go through all the controls in the base and take the
// ones we're missing. We've already removed all the ones that intersect
// and had to be merged so the rest we can just slurp into the list as is.
if (!layoutIsTargetingSpecificVariants)
{
var indexStart = controls.Count;
controls.AddRange(baseControlTable.Values);
// Mark the controls as being inherited.
for (var i = indexStart; i < controls.Count; ++i)
{
var control = controls[i];
control.isFirstDefinedInThisLayout = false;
controls[i] = control;
}
}
else
{
// Filter out controls coming from the base layout which are targeting variants
// that we're not interested in.
var indexStart = controls.Count;
controls.AddRange(
baseControlTable.Values.Where(x => VariantsMatch(m_Variants, x.variants)));
// Mark the controls as being inherited.
for (var i = indexStart; i < controls.Count; ++i)
{
var control = controls[i];
control.isFirstDefinedInThisLayout = false;
controls[i] = control;
}
}
m_Controls = controls.ToArray();
}
}
private static Dictionary<string, ControlItem> CreateLookupTableForControls(
ControlItem[] controlItems, List<string> variants = null)
{
var table = new Dictionary<string, ControlItem>();
for (var i = 0; i < controlItems.Length; ++i)
{
var key = controlItems[i].name.ToLower();
// Need to take variants into account as well. Otherwise two variants for
// "leftStick", for example, will overwrite each other.
var itemVariants = controlItems[i].variants;
if (!itemVariants.IsEmpty() && itemVariants != DefaultVariant)
{
// If there's multiple variants on the control, we add it to the table multiple times.
if (itemVariants.ToString().IndexOf(VariantSeparator[0]) != -1)
{
var itemVariantArray = itemVariants.ToLower().Split(VariantSeparator[0]);
foreach (var name in itemVariantArray)
{
variants?.Add(name);
key = $"{key}@{name}";
table[key] = controlItems[i];
}
continue;
}
key = $"{key}@{itemVariants.ToLower()}";
variants?.Add(itemVariants.ToLower());
}
table[key] = controlItems[i];
}
return table;
}
internal static bool VariantsMatch(InternedString expected, InternedString actual)
{
return VariantsMatch(expected.ToLower(), actual.ToLower());
}
internal static bool VariantsMatch(string expected, string actual)
{
////REVIEW: does this make sense?
// Default variant works with any other expected variant.
if (actual != null &&
StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(DefaultVariant, actual, VariantSeparator[0]))
return true;
// If we don't expect a specific variant, we accept any variant.
if (expected == null)
return true;
// If we there's no variant set on what we actual got, then it matches even if we
// expect specific variants.
if (actual == null)
return true;
// Match if the two variant sets intersect on at least one element.
return StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(expected, actual, VariantSeparator[0]);
}
internal static void ParseHeaderFieldsFromJson(string json, out InternedString name,
out InlinedArray<InternedString> baseLayouts, out InputDeviceMatcher deviceMatcher)
{
var header = JsonUtility.FromJson<LayoutJsonNameAndDescriptorOnly>(json);
name = new InternedString(header.name);
baseLayouts = new InlinedArray<InternedString>();
if (!string.IsNullOrEmpty(header.extend))
baseLayouts.Append(new InternedString(header.extend));
if (header.extendMultiple != null)
foreach (var item in header.extendMultiple)
baseLayouts.Append(new InternedString(item));
deviceMatcher = header.device.ToMatcher();
}
[Serializable]
internal struct LayoutJsonNameAndDescriptorOnly
{
public string name;
public string extend;
public string[] extendMultiple;
public InputDeviceMatcher.MatcherJson device;
}
[Serializable]
private struct LayoutJson
{
// Disable warnings that these fields are never assigned to. They are set
// by JsonUtility.
#pragma warning disable 0649
// ReSharper disable MemberCanBePrivate.Local
public string name;
public string extend;
public string[] extendMultiple;
public string format;
public string beforeRender; // Can't be simple bool as otherwise we can't tell whether it was set or not.
public string runInBackground;
public string[] commonUsages;
public string displayName;
public string description;
public string type; // This is mostly for when we turn arbitrary InputControlLayouts into JSON; less for layouts *coming* from JSON.
public string variant;
public bool isGenericTypeOfDevice;
public bool hideInUI;
public ControlItemJson[] controls;
// ReSharper restore MemberCanBePrivate.Local
#pragma warning restore 0649
public InputControlLayout ToLayout()
{
// By default, the type of the layout is determined from the first layout
// in its 'extend' property chain that has a type set. However, if the layout
// extends nothing, we can't know what type to use for it so we default to
// InputDevice.
Type type = null;
if (!string.IsNullOrEmpty(this.type))
{
type = Type.GetType(this.type, false);
if (type == null)
{
Debug.Log(
$"Cannot find type '{this.type}' used by layout '{name}'; falling back to using InputDevice");
type = typeof(InputDevice);
}
else if (!typeof(InputControl).IsAssignableFrom(type))
{
throw new InvalidOperationException($"'{this.type}' used by layout '{name}' is not an InputControl");
}
}
else if (string.IsNullOrEmpty(extend))
type = typeof(InputDevice);
// Create layout.
var layout = new InputControlLayout(name, type)
{
m_DisplayName = displayName,
m_Description = description,
isGenericTypeOfDevice = isGenericTypeOfDevice,
hideInUI = hideInUI,
m_Variants = new InternedString(variant),
m_CommonUsages = ArrayHelpers.Select(commonUsages, x => new InternedString(x)),
};
if (!string.IsNullOrEmpty(format))
layout.m_StateFormat = new FourCC(format);
// Base layout.
if (!string.IsNullOrEmpty(extend))
layout.m_BaseLayouts.Append(new InternedString(extend));
if (extendMultiple != null)
foreach (var element in extendMultiple)
layout.m_BaseLayouts.Append(new InternedString(element));
// Before render behavior.
if (!string.IsNullOrEmpty(beforeRender))
{
var beforeRenderLowerCase = beforeRender.ToLowerInvariant();
if (beforeRenderLowerCase == "ignore")
layout.m_UpdateBeforeRender = false;
else if (beforeRenderLowerCase == "update")
layout.m_UpdateBeforeRender = true;
else
throw new InvalidOperationException($"Invalid beforeRender setting '{beforeRender}' (should be 'ignore' or 'update')");
}
// CanRunInBackground flag.
if (!string.IsNullOrEmpty(runInBackground))
{
var runInBackgroundLowerCase = runInBackground.ToLowerInvariant();
if (runInBackgroundLowerCase == "enabled")
layout.canRunInBackground = true;
else if (runInBackgroundLowerCase == "disabled")
layout.canRunInBackground = false;
else
throw new InvalidOperationException($"Invalid runInBackground setting '{beforeRender}' (should be 'enabled' or 'disabled')");
}
// Add controls.
if (controls != null)
{
var controlLayouts = new List<ControlItem>();
foreach (var control in controls)
{
if (string.IsNullOrEmpty(control.name))
throw new InvalidOperationException($"Control with no name in layout '{name}");
var controlLayout = control.ToLayout();
controlLayouts.Add(controlLayout);
}
layout.m_Controls = controlLayouts.ToArray();
}
return layout;
}
public static LayoutJson FromLayout(InputControlLayout layout)
{
return new LayoutJson
{
name = layout.m_Name,
type = layout.type?.AssemblyQualifiedName,
variant = layout.m_Variants,
displayName = layout.m_DisplayName,
description = layout.m_Description,
isGenericTypeOfDevice = layout.isGenericTypeOfDevice,
hideInUI = layout.hideInUI,
extend = layout.m_BaseLayouts.length == 1 ? layout.m_BaseLayouts[0].ToString() : null,
extendMultiple = layout.m_BaseLayouts.length > 1 ? layout.m_BaseLayouts.ToArray(x => x.ToString()) : null,
format = layout.stateFormat.ToString(),
commonUsages = ArrayHelpers.Select(layout.m_CommonUsages, x => x.ToString()),
controls = ControlItemJson.FromControlItems(layout.m_Controls),
beforeRender = layout.m_UpdateBeforeRender != null ? (layout.m_UpdateBeforeRender.Value ? "Update" : "Ignore") : null,
};
}
}
// This is a class instead of a struct so that we can assign 'offset' a custom
// default value. Otherwise we can't tell whether the user has actually set it
// or not (0 is a valid offset). Sucks, though, as we now get lots of allocations
// from the control array.
[Serializable]
private class ControlItemJson
{
// Disable warnings that these fields are never assigned to. They are set
// by JsonUtility.
#pragma warning disable 0649
// ReSharper disable MemberCanBePrivate.Local
public string name;
public string layout;
public string variants;
public string usage; // Convenience to not have to create array for single usage.
public string alias; // Same.
public string useStateFrom;
public uint offset;
public uint bit;
public uint sizeInBits;
public string format;
public int arraySize;
public string[] usages;
public string[] aliases;
public string parameters;
public string processors;
public string displayName;
public string shortDisplayName;
public bool noisy;
public bool dontReset;
public bool synthetic;
// This should be an object type field and allow any JSON primitive value type as well
// as arrays of those. Unfortunately, the Unity JSON serializer, given it uses Unity serialization
// and thus doesn't support polymorphism, can do no such thing. Hopefully we do get support
// for this later but for now, we use a string-based value fallback instead.
public string defaultState;
public string minValue;
public string maxValue;
// ReSharper restore MemberCanBePrivate.Local
#pragma warning restore 0649
public ControlItemJson()
{
offset = InputStateBlock.InvalidOffset;
bit = InputStateBlock.InvalidOffset;
}
public ControlItem ToLayout()
{
var layout = new ControlItem
{
name = new InternedString(name),
layout = new InternedString(this.layout),
variants = new InternedString(variants),
displayName = displayName,
shortDisplayName = shortDisplayName,
offset = offset,
useStateFrom = useStateFrom,
bit = bit,
sizeInBits = sizeInBits,
isModifyingExistingControl = name.IndexOf('/') != -1,
isNoisy = noisy,
dontReset = dontReset,
isSynthetic = synthetic,
isFirstDefinedInThisLayout = true,
arraySize = arraySize,
};
if (!string.IsNullOrEmpty(format))
layout.format = new FourCC(format);
if (!string.IsNullOrEmpty(usage) || usages != null)
{
var usagesList = new List<string>();
if (!string.IsNullOrEmpty(usage))
usagesList.Add(usage);
if (usages != null)
usagesList.AddRange(usages);
layout.usages = new ReadOnlyArray<InternedString>(usagesList.Select(x => new InternedString(x)).ToArray());
}
if (!string.IsNullOrEmpty(alias) || aliases != null)
{
var aliasesList = new List<string>();
if (!string.IsNullOrEmpty(alias))
aliasesList.Add(alias);
if (aliases != null)
aliasesList.AddRange(aliases);
layout.aliases = new ReadOnlyArray<InternedString>(aliasesList.Select(x => new InternedString(x)).ToArray());
}
if (!string.IsNullOrEmpty(parameters))
layout.parameters = new ReadOnlyArray<NamedValue>(NamedValue.ParseMultiple(parameters));
if (!string.IsNullOrEmpty(processors))
layout.processors = new ReadOnlyArray<NameAndParameters>(NameAndParameters.ParseMultiple(processors).ToArray());
if (defaultState != null)
layout.defaultState = PrimitiveValue.FromObject(defaultState);
if (minValue != null)
layout.minValue = PrimitiveValue.FromObject(minValue);
if (maxValue != null)
layout.maxValue = PrimitiveValue.FromObject(maxValue);
return layout;
}
public static ControlItemJson[] FromControlItems(ControlItem[] items)
{
if (items == null)
return null;
var count = items.Length;
var result = new ControlItemJson[count];
for (var i = 0; i < count; ++i)
{
var item = items[i];
result[i] = new ControlItemJson
{
name = item.name,
layout = item.layout,
variants = item.variants,
displayName = item.displayName,
shortDisplayName = item.shortDisplayName,
bit = item.bit,
offset = item.offset,
sizeInBits = item.sizeInBits,
format = item.format.ToString(),
parameters = string.Join(",", item.parameters.Select(x => x.ToString()).ToArray()),
processors = string.Join(",", item.processors.Select(x => x.ToString()).ToArray()),
usages = item.usages.Select(x => x.ToString()).ToArray(),
aliases = item.aliases.Select(x => x.ToString()).ToArray(),
noisy = item.isNoisy,
dontReset = item.dontReset,
synthetic = item.isSynthetic,
arraySize = item.arraySize,
defaultState = item.defaultState.ToString(),
minValue = item.minValue.ToString(),
maxValue = item.maxValue.ToString(),
};
}
return result;
}
}
internal struct Collection
{
public const float kBaseScoreForNonGeneratedLayouts = 1.0f;
public struct LayoutMatcher
{
public InternedString layoutName;
public InputDeviceMatcher deviceMatcher;
}
public struct PrecompiledLayout
{
public Func<InputDevice> factoryMethod;
public string metadata;
}
public Dictionary<InternedString, Type> layoutTypes;
public Dictionary<InternedString, string> layoutStrings;
public Dictionary<InternedString, Func<InputControlLayout>> layoutBuilders;
public Dictionary<InternedString, InternedString> baseLayoutTable;
public Dictionary<InternedString, InternedString[]> layoutOverrides;
public HashSet<InternedString> layoutOverrideNames;
public Dictionary<InternedString, PrecompiledLayout> precompiledLayouts;
////TODO: find a smarter approach that doesn't require linearly scanning through all matchers
//// (also ideally shouldn't be a List but with Collection being a struct and given how it's
//// stored by InputManager.m_Layouts and in s_Layouts; we can't make it a plain array)
public List<LayoutMatcher> layoutMatchers;
public void Allocate()
{
layoutTypes = new Dictionary<InternedString, Type>();
layoutStrings = new Dictionary<InternedString, string>();
layoutBuilders = new Dictionary<InternedString, Func<InputControlLayout>>();
baseLayoutTable = new Dictionary<InternedString, InternedString>();
layoutOverrides = new Dictionary<InternedString, InternedString[]>();
layoutOverrideNames = new HashSet<InternedString>();
layoutMatchers = new List<LayoutMatcher>();
precompiledLayouts = new Dictionary<InternedString, PrecompiledLayout>();
}
public InternedString TryFindLayoutForType(Type layoutType)
{
foreach (var entry in layoutTypes)
if (entry.Value == layoutType)
return entry.Key;
return new InternedString();
}
public InternedString TryFindMatchingLayout(InputDeviceDescription deviceDescription)
{
var highestScore = 0f;
var highestScoringLayout = new InternedString();
var layoutMatcherCount = layoutMatchers.Count;
for (var i = 0; i < layoutMatcherCount; ++i)
{
var matcher = layoutMatchers[i].deviceMatcher;
var score = matcher.MatchPercentage(deviceDescription);
// We want auto-generated layouts to take a backseat compared to manually created
// layouts. We do this by boosting the score of every layout that isn't coming from
// a layout builder.
if (score > 0 && !layoutBuilders.ContainsKey(layoutMatchers[i].layoutName))
score += kBaseScoreForNonGeneratedLayouts;
if (score > highestScore)
{
highestScore = score;
highestScoringLayout = layoutMatchers[i].layoutName;
}
}
return highestScoringLayout;
}
public bool HasLayout(InternedString name)
{
return layoutTypes.ContainsKey(name) || layoutStrings.ContainsKey(name) ||
layoutBuilders.ContainsKey(name);
}
private InputControlLayout TryLoadLayoutInternal(InternedString name)
{
// See if we have a string layout for it. These
// always take precedence over ones from type so that we can
// override what's in the code using data.
if (layoutStrings.TryGetValue(name, out var json))
return FromJson(json);
// No, but maybe we have a type layout for it.
if (layoutTypes.TryGetValue(name, out var type))
return FromType(name, type);
// Finally, check builders. Always the last ones to get a shot at
// providing layouts.
if (layoutBuilders.TryGetValue(name, out var builder))
{
var layout = builder();
if (layout == null)
throw new InvalidOperationException($"Layout builder '{name}' returned null when invoked");
return layout;
}
return null;
}
public InputControlLayout TryLoadLayout(InternedString name, Dictionary<InternedString, InputControlLayout> table = null)
{
// See if we have it cached.
if (table != null && table.TryGetValue(name, out var layout))
return layout;
layout = TryLoadLayoutInternal(name);
if (layout != null)
{
layout.m_Name = name;
if (layoutOverrideNames.Contains(name))
layout.isOverride = true;
// If the layout extends another layout, we need to merge the
// base layout into the final layout.
// NOTE: We go through the baseLayoutTable here instead of looking at
// the baseLayouts property so as to make this work for all types
// of layouts (FromType() does not set the property, for example).
var baseLayoutName = new InternedString();
if (!layout.isOverride && baseLayoutTable.TryGetValue(name, out baseLayoutName))
{
Debug.Assert(!baseLayoutName.IsEmpty());
////TODO: catch cycles
var baseLayout = TryLoadLayout(baseLayoutName, table);
if (baseLayout == null)
throw new LayoutNotFoundException(
$"Cannot find base layout '{baseLayoutName}' of layout '{name}'");
layout.MergeLayout(baseLayout);
if (layout.m_BaseLayouts.length == 0)
layout.m_BaseLayouts.Append(baseLayoutName);
}
// If there's overrides for the layout, apply them now.
if (layoutOverrides.TryGetValue(name, out var overrides))
{
for (var i = 0; i < overrides.Length; ++i)
{
var overrideName = overrides[i];
// NOTE: We do *NOT* pass `table` into TryLoadLayout here so that
// the override we load will not get cached. The reason is that
// we use MergeLayout which is destructive and thus should not
// end up in the table.
var overrideLayout = TryLoadLayout(overrideName);
overrideLayout.MergeLayout(layout);
// We're switching the layout we initially to the layout with
// the overrides applied. Make sure we get rid of information here
// from the override that we don't want to come through once the
// override is applied.
overrideLayout.m_BaseLayouts.Clear();
overrideLayout.isOverride = false;
overrideLayout.isGenericTypeOfDevice = layout.isGenericTypeOfDevice;
overrideLayout.m_Name = layout.name;
overrideLayout.m_BaseLayouts = layout.m_BaseLayouts;
layout = overrideLayout;
layout.m_AppliedOverrides.Append(overrideName);
}
}
if (table != null)
table[name] = layout;
}
return layout;
}
public InternedString GetBaseLayoutName(InternedString layoutName)
{
if (baseLayoutTable.TryGetValue(layoutName, out var baseLayoutName))
return baseLayoutName;
return default;
}
// Return name of layout at root of "extend" chain of given layout.
public InternedString GetRootLayoutName(InternedString layoutName)
{
while (baseLayoutTable.TryGetValue(layoutName, out var baseLayout))
layoutName = baseLayout;
return layoutName;
}
public bool ComputeDistanceInInheritanceHierarchy(InternedString firstLayout, InternedString secondLayout, out int distance)
{
distance = 0;
// First try, assume secondLayout is based on firstLayout.
var secondDistanceToFirst = 0;
var current = secondLayout;
while (!current.IsEmpty() && current != firstLayout)
{
current = GetBaseLayoutName(current);
++secondDistanceToFirst;
}
if (current == firstLayout)
{
distance = secondDistanceToFirst;
return true;
}
// Second try, assume firstLayout is based on secondLayout.
var firstDistanceToSecond = 0;
current = firstLayout;
while (!current.IsEmpty() && current != secondLayout)
{
current = GetBaseLayoutName(current);
++firstDistanceToSecond;
}
if (current == secondLayout)
{
distance = firstDistanceToSecond;
return true;
}
return false;
}
public InternedString FindLayoutThatIntroducesControl(InputControl control, Cache cache)
{
// Find the topmost child control on the device. A device layout can only
// add children that sit directly underneath it (e.g. "leftStick"). Children of children
// are indirectly added by other layouts (e.g. "leftStick/x" which is added by "Stick").
// To determine which device contributes the control as a whole, we have to be looking
// at the topmost child of the device.
var topmostChild = control;
while (topmostChild.parent != control.device)
topmostChild = topmostChild.parent;
// Find the layout in the device's base layout chain that first mentions the given control.
// If we don't find it, we know it's first defined directly in the layout of the given device,
// i.e. it's not an inherited control.
var deviceLayoutName = control.device.m_Layout;
var baseLayoutName = deviceLayoutName;
while (baseLayoutTable.TryGetValue(baseLayoutName, out baseLayoutName))
{
var layout = cache.FindOrLoadLayout(baseLayoutName);
var controlItem = layout.FindControl(topmostChild.m_Name);
if (controlItem != null)
deviceLayoutName = baseLayoutName;
}
return deviceLayoutName;
}
// Get the type which will be instantiated for the given layout.
// Returns null if no layout with the given name exists.
public Type GetControlTypeForLayout(InternedString layoutName)
{
// Try layout strings.
while (layoutStrings.ContainsKey(layoutName))
{
if (baseLayoutTable.TryGetValue(layoutName, out var baseLayout))
{
// Work our way up the inheritance chain.
layoutName = baseLayout;
}
else
{
// Layout doesn't extend anything and ATM we don't support setting
// types explicitly from JSON layouts. So has to be InputDevice.
return typeof(InputDevice);
}
}
// Try layout types.
layoutTypes.TryGetValue(layoutName, out var result);
return result;
}
// Return true if the given control layout has a value type whose values
// can be assigned to variables of type valueType.
public bool ValueTypeIsAssignableFrom(InternedString layoutName, Type valueType)
{
var controlType = GetControlTypeForLayout(layoutName);
if (controlType == null)
return false;
var valueTypOfControl =
TypeHelpers.GetGenericTypeArgumentFromHierarchy(controlType, typeof(InputControl<>), 0);
if (valueTypOfControl == null)
return false;
return valueType.IsAssignableFrom(valueTypOfControl);
}
public bool IsGeneratedLayout(InternedString layout)
{
return layoutBuilders.ContainsKey(layout);
}
public IEnumerable<InternedString> GetBaseLayouts(InternedString layout, bool includeSelf = true)
{
if (includeSelf)
yield return layout;
while (baseLayoutTable.TryGetValue(layout, out layout))
yield return layout;
}
public bool IsBasedOn(InternedString parentLayout, InternedString childLayout)
{
var layout = childLayout;
while (baseLayoutTable.TryGetValue(layout, out layout))
{
if (layout == parentLayout)
return true;
}
return false;
}
public void AddMatcher(InternedString layout, InputDeviceMatcher matcher)
{
// Ignore if already added.
var layoutMatcherCount = layoutMatchers.Count;
for (var i = 0; i < layoutMatcherCount; ++i)
if (layoutMatchers[i].deviceMatcher == matcher)
return;
// Append.
layoutMatchers.Add(new LayoutMatcher {layoutName = layout, deviceMatcher = matcher});
}
}
// This collection is owned and managed by InputManager.
internal static Collection s_Layouts;
public class LayoutNotFoundException : Exception
{
public string layout { get; }
public LayoutNotFoundException()
{
}
public LayoutNotFoundException(string name, string message)
: base(message)
{
layout = name;
}
public LayoutNotFoundException(string name)
: base($"Cannot find control layout '{name}'")
{
layout = name;
}
public LayoutNotFoundException(string message, Exception innerException) :
base(message, innerException)
{
}
protected LayoutNotFoundException(SerializationInfo info,
StreamingContext context) : base(info, context)
{
}
}
// Constructs InputControlLayout instances and caches them.
internal struct Cache
{
public Dictionary<InternedString, InputControlLayout> table;
public void Clear()
{
table = null;
}
public InputControlLayout FindOrLoadLayout(string name, bool throwIfNotFound = true)
{
var internedName = new InternedString(name);
if (table == null)
table = new Dictionary<InternedString, InputControlLayout>();
var layout = s_Layouts.TryLoadLayout(internedName, table);
if (layout != null)
return layout;
// Nothing.
if (throwIfNotFound)
throw new LayoutNotFoundException(name);
return null;
}
}
internal static Cache s_CacheInstance;
internal static int s_CacheInstanceRef;
// Constructing InputControlLayouts is very costly as it tends to involve lots of reflection and
// piecing data together. Thus, wherever possible, we want to keep layouts around for as long as
// we need them yet at the same time not keep them needlessly around while we don't.
//
// This property makes a cache of layouts available globally yet implements a resource acquisition
// based pattern to make sure we keep the cache alive only within specific execution scopes.
internal static ref Cache cache
{
get
{
Debug.Assert(s_CacheInstanceRef > 0, "Must hold an instance reference");
return ref s_CacheInstance;
}
}
internal static CacheRefInstance CacheRef()
{
++s_CacheInstanceRef;
return new CacheRefInstance {valid = true};
}
internal struct CacheRefInstance : IDisposable
{
public bool valid; // Make sure we can distinguish default-initialized instances.
public void Dispose()
{
if (!valid)
return;
--s_CacheInstanceRef;
if (s_CacheInstanceRef <= 0)
{
s_CacheInstance = default;
s_CacheInstanceRef = 0;
}
valid = false;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5a2515c58b39ea24c8154ebc84f7c157
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,118 @@
using System;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.Scripting;
////REVIEW: should this *not* be inherited? inheritance can lead to surprises
namespace UnityEngine.InputSystem.Layouts
{
/// <summary>
/// Attribute to control layout settings of a type used to generate an <see cref="InputControlLayout"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class InputControlLayoutAttribute : Attribute
{
/// <summary>
/// Associates a state representation with an input device and drives
/// the control layout generated for the device from its state rather
/// than from the device class.
/// </summary>
/// <remarks>This is *only* useful if you have a state struct dictating a specific
/// state layout and you want the device layout to automatically take offsets from
/// the fields annotated with <see cref="InputControlAttribute"/>.
///
/// <example>
/// <code>
/// public struct MyStateStruct : IInputStateTypeInfo
/// {
/// public FourCC format => new FourCC('M', 'Y', 'D', 'V');
///
/// [InputControl(name = "button1", layout = "Button", bit = 0)]
/// [InputControl(name = "button2", layout = "Button", bit = 0)]
/// public int buttons;
/// }
///
/// [InputControlLayout(stateType = typeof(MyStateStruct)]
/// public class MyDevice : InputDevice
/// {
/// }
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="LowLevel.InputStateBlock"/>
/// <seealso cref="LowLevel.MouseState"/>
public Type stateType { get; set; }
/// <summary>
/// <see cref="FourCC"/> identifier for the memory format associated with the layout.
/// </summary>
/// <seealso cref="LowLevel.InputStateBlock.format"/>
public string stateFormat { get; set; }
////TODO: rename this to just "usages"; "commonUsages" is such a weird name
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "According to MSDN, this message can be ignored for attribute parameters, as there are no better alternatives.")]
public string[] commonUsages { get; set; }
public string variants { get; set; }
/// <summary>
/// Allows marking a device as noisy regardless of control layout.
/// </summary>
/// <remarks>
/// Controls can be individually marked as noisy using the <see cref="InputControlAttribute.noisy"/>
/// attribute, but this property can be used to mark a device as noisy even when no control has been
/// marked as such. This can be useful when a device state layout has only been partially implemented
/// i.e. some data in the state memory has not been mapped to a control, and the unimplemented controls
/// are noisy. Without doing this, the device will constantly be made current as the system has no way
/// to know that the event data contains only noise.
/// </remarks>
public bool isNoisy { get; set; }
internal bool? canRunInBackgroundInternal;
public bool canRunInBackground
{
get => canRunInBackgroundInternal.Value;
set => canRunInBackgroundInternal = value;
}
internal bool? updateBeforeRenderInternal;
/// <summary>
/// Whether the device should receive events in <see cref="LowLevel.InputUpdateType.BeforeRender"/> updates.
/// </summary>
/// <seealso cref="InputDevice.updateBeforeRender"/>
public bool updateBeforeRender
{
get => updateBeforeRenderInternal.Value;
set => updateBeforeRenderInternal = value;
}
/// <summary>
/// If true, the layout describes a generic class of devices such as "gamepads" or "mice".
/// </summary>
/// <remarks>
/// This property also determines how the layout is presented in the UI. All the device layouts
/// that are marked as generic kinds of devices are displayed with their own entry at the root level of
/// the control picker (<see cref="UnityEngine.InputSystem.Editor.InputControlPicker"/>), for example.
/// </remarks>
public bool isGenericTypeOfDevice { get; set; }
/// <summary>
/// Gives a name to display in the UI. By default, the name is the same as the class the attribute
/// is applied to.
/// </summary>
public string displayName { get; set; }
public string description { get; set; }
/// <summary>
/// If true, don't include the layout when presenting picking options in the UI.
/// </summary>
/// <remarks>
/// This will keep device layouts out of the control picker and will keep control layouts out of
/// action type dropdowns.
/// </remarks>
public bool hideInUI { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b5256e0d1ebecc84d98a4001ab63d5b8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,12 @@
namespace UnityEngine.InputSystem
{
/// <summary>
/// Enum used to identity the change type for the <see cref="InputSystem.onLayoutChange"/> event.
/// </summary>
public enum InputControlLayoutChange
{
Added,
Removed,
Replaced
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f5899dcacd4e3004eaa9107c97e75e09
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,614 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.InputSystem.Utilities;
////TODO: add a device setup version to InputManager and add version check here to ensure we're not going out of sync
////REVIEW: can we have a read-only version of this
////REVIEW: move this to .LowLevel? this one is pretty peculiar to use and doesn't really work like what you'd expect given C#'s List<>
namespace UnityEngine.InputSystem
{
/// <summary>
/// Keep a list of <see cref="InputControl"/>s without allocating managed memory.
/// </summary>
/// <remarks>
/// This struct is mainly used by methods such as <see cref="InputSystem.FindControls(string)"/>
/// or <see cref="InputControlPath.TryFindControls{TControl}"/> to store an arbitrary length
/// list of resulting matches without having to allocate GC heap memory.
///
/// Requires the control setup in the system to not change while the list is being used. If devices are
/// removed from the system, the list will no longer be valid. Also, only works with controls of devices that
/// have been added to the system (<see cref="InputDevice.added"/>). The reason for these constraints is
/// that internally, the list only stores integer indices that are translates to <see cref="InputControl"/>
/// references on the fly. If the device setup in the system changes, the indices may become invalid.
///
/// This struct allocates unmanaged memory and thus must be disposed or it will leak memory. By default
/// allocates <c>Allocator.Persistent</c> memory. You can direct it to use another allocator by
/// passing an <see cref="Allocator"/> value to one of the constructors.
///
/// <example>
/// <code>
/// // Find all controls with the "Submit" usage in the system.
/// // By wrapping it in a `using` block, the list of controls will automatically be disposed at the end.
/// using (var controls = InputSystem.FindControls("*/{Submit}"))
/// /* ... */;
/// </code>
/// </example>
/// </remarks>
/// <typeparam name="TControl">Type of <see cref="InputControl"/> to store in the list.</typeparam>
[DebuggerDisplay("Count = {Count}")]
#if UNITY_EDITOR || DEVELOPMENT_BUILD
[DebuggerTypeProxy(typeof(InputControlListDebugView<>))]
#endif
public unsafe struct InputControlList<TControl> : IList<TControl>, IReadOnlyList<TControl>, IDisposable
where TControl : InputControl
{
/// <summary>
/// Current number of controls in the list.
/// </summary>
/// <value>Number of controls currently in the list.</value>
public int Count => m_Count;
/// <summary>
/// Total number of controls that can currently be stored in the list.
/// </summary>
/// <value>Total size of array as currently allocated.</value>
/// <remarks>
/// This can be set ahead of time to avoid repeated allocations.
///
/// <example>
/// <code>
/// // Add all keys from the keyboard to a list.
/// var keys = Keyboard.current.allKeys;
/// var list = new InputControlList&lt;KeyControl&gt;(keys.Count);
/// list.AddRange(keys);
/// </code>
/// </example>
/// </remarks>
public int Capacity
{
get
{
if (!m_Indices.IsCreated)
return 0;
return m_Indices.Length;
}
set
{
if (value < 0)
throw new ArgumentException("Capacity cannot be negative", nameof(value));
if (value == 0)
{
if (m_Count != 0)
m_Indices.Dispose();
m_Count = 0;
return;
}
var newSize = value;
var allocator = m_Allocator != Allocator.Invalid ? m_Allocator : Allocator.Persistent;
ArrayHelpers.Resize(ref m_Indices, newSize, allocator);
}
}
/// <summary>
/// This is always false.
/// </summary>
/// <value>Always false.</value>
public bool IsReadOnly => false;
/// <summary>
/// Return the control at the given index.
/// </summary>
/// <param name="index">Index of control.</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is less than 0 or greater than or equal to <see cref="Count"/>
/// </exception>
/// <remarks>
/// Internally, the list only stores indices. Resolution to <see cref="InputControl">controls</see> happens
/// dynamically by looking them up globally.
/// </remarks>
public TControl this[int index]
{
get
{
if (index < 0 || index >= m_Count)
throw new ArgumentOutOfRangeException(
nameof(index), $"Index {index} is out of range in list with {m_Count} entries");
return FromIndex(m_Indices[index]);
}
set
{
if (index < 0 || index >= m_Count)
throw new ArgumentOutOfRangeException(
nameof(index), $"Index {index} is out of range in list with {m_Count} entries");
m_Indices[index] = ToIndex(value);
}
}
/// <summary>
/// Construct a list that allocates unmanaged memory from the given allocator.
/// </summary>
/// <param name="allocator">Allocator to use for requesting unmanaged memory.</param>
/// <param name="initialCapacity">If greater than zero, will immediately allocate
/// memory and set <see cref="Capacity"/> accordingly.</param>
/// <example>
/// <code>
/// // Create a control list that allocates from the temporary memory allocator.
/// using (var list = new InputControlList(Allocator.Temp))
/// {
/// // Add all gamepads to the list.
/// InputSystem.FindControls("&lt;Gamepad&gt;", ref list);
/// }
/// </code>
/// </example>
public InputControlList(Allocator allocator, int initialCapacity = 0)
{
m_Allocator = allocator;
m_Indices = new NativeArray<ulong>();
m_Count = 0;
if (initialCapacity != 0)
Capacity = initialCapacity;
}
/// <summary>
/// Construct a list and populate it with the given values.
/// </summary>
/// <param name="values">Sequence of values to populate the list with.</param>
/// <param name="allocator">Allocator to use for requesting unmanaged memory.</param>
/// <exception cref="ArgumentNullException"><paramref name="values"/> is <c>null</c>.</exception>
public InputControlList(IEnumerable<TControl> values, Allocator allocator = Allocator.Persistent)
: this(allocator)
{
if (values == null)
throw new ArgumentNullException(nameof(values));
foreach (var value in values)
Add(value);
}
/// <summary>
/// Construct a list and add the given values to it.
/// </summary>
/// <param name="values">Sequence of controls to add to the list.</param>
/// <exception cref="ArgumentNullException"><paramref name="values"/> is null.</exception>
public InputControlList(params TControl[] values)
: this()
{
if (values == null)
throw new ArgumentNullException(nameof(values));
var count = values.Length;
Capacity = Mathf.Max(count, 10);
for (var i = 0; i < count; ++i)
Add(values[i]);
}
/// <summary>
/// Resizes the list to be exactly <paramref name="size"/> entries. If this is less than the
/// current <see cref="Count"/>, additional entries are dropped. If it is more than the
/// current <see cref="Count"/>, additional <c>null</c> entries are appended to the list.
/// </summary>
/// <param name="size">The new value for <see cref="Count"/>.</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="size"/> is negative.</exception>
/// <remarks>
/// <see cref="Capacity"/> is increased if necessary. It will, however, not be decreased if it
/// is larger than <paramref name="size"/> entries.
/// </remarks>
public void Resize(int size)
{
if (size < 0)
throw new ArgumentOutOfRangeException(nameof(size), "Size cannot be negative");
if (Capacity < size)
Capacity = size;
// Initialize newly added entries (if any) such that they produce NULL entries.
if (size > Count)
UnsafeUtility.MemSet((byte*)m_Indices.GetUnsafePtr() + Count * sizeof(ulong), Byte.MaxValue, size - Count);
m_Count = size;
}
/// <summary>
/// Add a control to the list.
/// </summary>
/// <param name="item">Control to add. Allowed to be <c>null</c>.</param>
/// <remarks>
/// If necessary, <see cref="Capacity"/> will be increased.
///
/// It is allowed to add nulls to the list. This can be useful, for example, when
/// specific indices in the list correlate with specific matches and a given match
/// needs to be marked as "matches nothing".
/// </remarks>
/// <seealso cref="Remove"/>
public void Add(TControl item)
{
var index = ToIndex(item);
var allocator = m_Allocator != Allocator.Invalid ? m_Allocator : Allocator.Persistent;
ArrayHelpers.AppendWithCapacity(ref m_Indices, ref m_Count, index, allocator: allocator);
}
/// <summary>
/// Add a slice of elements taken from the given list.
/// </summary>
/// <param name="list">List to take the slice of values from.</param>
/// <param name="count">Number of elements to copy from <paramref name="list"/>.</param>
/// <param name="destinationIndex">Starting index in the current control list to copy to.
/// This can be beyond <see cref="Count"/> or even <see cref="Capacity"/>. Memory is allocated
/// as needed.</param>
/// <param name="sourceIndex">Source index in <paramref name="list"/> to start copying from.
/// <paramref name="count"/> elements are copied starting at <paramref name="sourceIndex"/>.</param>
/// <typeparam name="TList">Type of list. This is a type parameter to avoid boxing in case the
/// given list is a struct (such as InputControlList itself).</typeparam>
/// <exception cref="ArgumentOutOfRangeException">The range of <paramref name="count"/>
/// and <paramref name="sourceIndex"/> is at least partially outside the range of values
/// available in <paramref name="list"/>.</exception>
public void AddSlice<TList>(TList list, int count = -1, int destinationIndex = -1, int sourceIndex = 0)
where TList : IReadOnlyList<TControl>
{
if (count < 0)
count = list.Count;
if (destinationIndex < 0)
destinationIndex = Count;
if (count == 0)
return;
if (sourceIndex + count > list.Count)
throw new ArgumentOutOfRangeException(nameof(count),
$"Count of {count} elements starting at index {sourceIndex} exceeds length of list of {list.Count}");
// Make space in the list.
if (Capacity < m_Count + count)
Capacity = Math.Max(m_Count + count, 10);
if (destinationIndex < Count)
NativeArray<ulong>.Copy(m_Indices, destinationIndex, m_Indices, destinationIndex + count,
Count - destinationIndex);
// Add elements.
for (var i = 0; i < count; ++i)
m_Indices[destinationIndex + i] = ToIndex(list[sourceIndex + i]);
m_Count += count;
}
/// <summary>
/// Add a sequence of controls to the list.
/// </summary>
/// <param name="list">Sequence of controls to add.</param>
/// <param name="count">Number of controls from <paramref name="list"/> to add. If negative
/// (default), all controls from <paramref name="list"/> will be added.</param>
/// <param name="destinationIndex">Index in the control list to start inserting controls
/// at. If negative (default), controls will be appended to the end of the control list.</param>
/// <exception cref="ArgumentNullException"><paramref name="list"/> is <c>null</c>.</exception>
/// <remarks>
/// If <paramref name="count"/> is not supplied, <paramref name="list"/> will be iterated
/// over twice.
/// </remarks>
public void AddRange(IEnumerable<TControl> list, int count = -1, int destinationIndex = -1)
{
if (list == null)
throw new ArgumentNullException(nameof(list));
if (count < 0)
count = list.Count();
if (destinationIndex < 0)
destinationIndex = Count;
if (count == 0)
return;
// Make space in the list.
if (Capacity < m_Count + count)
Capacity = Math.Max(m_Count + count, 10);
if (destinationIndex < Count)
NativeArray<ulong>.Copy(m_Indices, destinationIndex, m_Indices, destinationIndex + count,
Count - destinationIndex);
// Add elements.
foreach (var element in list)
{
m_Indices[destinationIndex++] = ToIndex(element);
++m_Count;
--count;
if (count == 0)
break;
}
}
/// <summary>
/// Remove a control from the list.
/// </summary>
/// <param name="item">Control to remove. Can be null.</param>
/// <returns>True if the control was found in the list and removed, false otherwise.</returns>
/// <seealso cref="Add"/>
public bool Remove(TControl item)
{
if (m_Count == 0)
return false;
var index = ToIndex(item);
for (var i = 0; i < m_Count; ++i)
{
if (m_Indices[i] == index)
{
ArrayHelpers.EraseAtWithCapacity(m_Indices, ref m_Count, i);
return true;
}
}
return false;
}
/// <summary>
/// Remove the control at the given index.
/// </summary>
/// <param name="index">Index of control to remove.</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is negative or equal
/// or greater than <see cref="Count"/>.</exception>
public void RemoveAt(int index)
{
if (index < 0 || index >= m_Count)
throw new ArgumentOutOfRangeException(
nameof(index), $"Index {index} is out of range in list with {m_Count} elements");
ArrayHelpers.EraseAtWithCapacity(m_Indices, ref m_Count, index);
}
public void CopyTo(TControl[] array, int arrayIndex)
{
throw new NotImplementedException();
}
public int IndexOf(TControl item)
{
return IndexOf(item, 0);
}
public int IndexOf(TControl item, int startIndex, int count = -1)
{
if (startIndex < 0)
throw new ArgumentOutOfRangeException(nameof(startIndex), "startIndex cannot be negative");
if (m_Count == 0)
return -1;
if (count < 0)
count = Mathf.Max(m_Count - startIndex, 0);
if (startIndex + count > m_Count)
throw new ArgumentOutOfRangeException(nameof(count));
var index = ToIndex(item);
var indices = (ulong*)m_Indices.GetUnsafeReadOnlyPtr();
for (var i = 0; i < count; ++i)
if (indices[startIndex + i] == index)
return startIndex + i;
return -1;
}
public void Insert(int index, TControl item)
{
throw new NotImplementedException();
}
public void Clear()
{
m_Count = 0;
}
public bool Contains(TControl item)
{
return IndexOf(item) != -1;
}
public bool Contains(TControl item, int startIndex, int count = -1)
{
return IndexOf(item, startIndex, count) != -1;
}
public void SwapElements(int index1, int index2)
{
if (index1 < 0 || index1 >= m_Count)
throw new ArgumentOutOfRangeException(nameof(index1));
if (index2 < 0 || index2 >= m_Count)
throw new ArgumentOutOfRangeException(nameof(index2));
if (index1 != index2)
m_Indices.SwapElements(index1, index2);
}
public void Sort<TCompare>(int startIndex, int count, TCompare comparer)
where TCompare : IComparer<TControl>
{
if (startIndex < 0 || startIndex >= Count)
throw new ArgumentOutOfRangeException(nameof(startIndex));
if (startIndex + count >= Count)
throw new ArgumentOutOfRangeException(nameof(count));
// Simple insertion sort.
for (var i = 1; i < count; ++i)
for (var j = i; j > 0 && comparer.Compare(this[j - 1], this[j]) < 0; --j)
SwapElements(j, j - 1);
}
/// <summary>
/// Convert the contents of the list to an array.
/// </summary>
/// <param name="dispose">If true, the control list will be disposed of as part of the operation, i.e.
/// <see cref="Dispose"/> will be called as a side-effect.</param>
/// <returns>An array mirroring the contents of the list. Not null.</returns>
public TControl[] ToArray(bool dispose = false)
{
// Somewhat pointless to allocate an empty array if we have no elements instead
// of returning null, but other ToArray() implementations work that way so we do
// the same to avoid surprises.
var result = new TControl[m_Count];
for (var i = 0; i < m_Count; ++i)
result[i] = this[i];
if (dispose)
Dispose();
return result;
}
internal void AppendTo(ref TControl[] array, ref int count)
{
for (var i = 0; i < m_Count; ++i)
ArrayHelpers.AppendWithCapacity(ref array, ref count, this[i]);
}
public void Dispose()
{
if (m_Indices.IsCreated)
m_Indices.Dispose();
}
public IEnumerator<TControl> GetEnumerator()
{
return new Enumerator(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public override string ToString()
{
if (Count == 0)
return "()";
var builder = new StringBuilder();
builder.Append('(');
for (var i = 0; i < Count; ++i)
{
if (i != 0)
builder.Append(',');
builder.Append(this[i]);
}
builder.Append(')');
return builder.ToString();
}
private int m_Count;
private NativeArray<ulong> m_Indices;
private readonly Allocator m_Allocator;
private const ulong kInvalidIndex = 0xffffffffffffffff;
private static ulong ToIndex(TControl control)
{
if (control == null)
return kInvalidIndex;
var device = control.device;
var deviceId = device.m_DeviceId;
var controlIndex = !ReferenceEquals(device, control)
? device.m_ChildrenForEachControl.IndexOfReference<InputControl, InputControl>(control) + 1
: 0;
// There is a known documented bug with the new Rosyln
// compiler where it warns on casts with following line that
// was perfectly legal in previous CSC compiler.
// Below is silly conversion to get rid of warning, or we can pragma
// out the warning.
//return ((ulong)deviceId << 32) | (ulong)controlIndex;
var shiftedDeviceId = (ulong)deviceId << 32;
var unsignedControlIndex = (ulong)controlIndex;
return shiftedDeviceId | unsignedControlIndex;
}
private static TControl FromIndex(ulong index)
{
if (index == kInvalidIndex)
return null;
var deviceId = (int)(index >> 32);
var controlIndex = (int)(index & 0xFFFFFFFF);
var device = InputSystem.GetDeviceById(deviceId);
if (device == null)
return null;
if (controlIndex == 0)
return (TControl)(InputControl)device;
return (TControl)device.m_ChildrenForEachControl[controlIndex - 1];
}
private struct Enumerator : IEnumerator<TControl>
{
private readonly ulong* m_Indices;
private readonly int m_Count;
private int m_Current;
public Enumerator(InputControlList<TControl> list)
{
m_Count = list.m_Count;
m_Current = -1;
m_Indices = m_Count > 0 ? (ulong*)list.m_Indices.GetUnsafeReadOnlyPtr() : null;
}
public bool MoveNext()
{
if (m_Current >= m_Count)
return false;
++m_Current;
return (m_Current != m_Count);
}
public void Reset()
{
m_Current = -1;
}
public TControl Current
{
get
{
if (m_Indices == null)
throw new InvalidOperationException("Enumerator is not valid");
return FromIndex(m_Indices[m_Current]);
}
}
object IEnumerator.Current => Current;
public void Dispose()
{
}
}
}
#if UNITY_EDITOR || DEVELOPMENT_BUILD
internal struct InputControlListDebugView<TControl>
where TControl : InputControl
{
private readonly TControl[] m_Controls;
public InputControlListDebugView(InputControlList<TControl> list)
{
m_Controls = list.ToArray();
}
public TControl[] controls => m_Controls;
}
#endif
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 543792e79adf14478b4a00527bbefce1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,1612 @@
using System;
using System.Text;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Unity.Collections;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
////TODO: allow stuff like "/gamepad/**/<button>"
////TODO: add support for | (e.g. "<Gamepad>|<Joystick>/{PrimaryMotion}"
////TODO: handle arrays
////TODO: add method to extract control path
////REVIEW: change "*/{PrimaryAction}" to "*/**/{PrimaryAction}" so that the hierarchy crawling becomes explicit?
////REVIEW: rename to `InputPath`?
namespace UnityEngine.InputSystem
{
/// <summary>
/// Functions for working with control path specs (like "/gamepad/*stick").
/// </summary>
/// <remarks>
/// Control paths are a mini-language similar to regular expressions. They are used throughout
/// the input system as string "addresses" of input controls. At runtime, they can be matched
/// against the devices and controls present in the system to retrieve the actual endpoints to
/// receive input from.
///
/// Like on a file system, a path is made up of components that are each separated by a
/// forward slash (<c>/</c>). Each such component in turn is made up of a set of fields that are
/// individually optional. However, one of the fields must be present (e.g. at least a name or
/// a wildcard).
///
/// <example>
/// Field structure of each path component
/// <code>
/// &lt;Layout&gt;{Usage}#(DisplayName)Name
/// </code>
/// </example>
///
/// * <c>Layout</c>: The name of the layout that the control must be based on (either directly or indirectly).
/// * <c>Usage</c>: The usage that the control or device has to have, i.e. must be found in <see
/// cref="InputControl.usages"/> This field can be repeated several times to require
/// multiple usages (e.g. <c>"{LeftHand}{Vertical}"</c>).
/// * <c>DisplayName</c>: The name that <see cref="InputControl.displayName"/> of the control or device
/// must match.
/// * <c>Name</c>: The name that <see cref="InputControl.name"/> or one of the entries in
/// <see cref="InputControl.aliases"/> must match. Alternatively, this can be a
/// wildcard (<c>*</c>) to match any name.
///
/// Note that all matching is case-insensitive.
///
/// <example>
/// Various examples of control paths
/// <code>
/// // Matches all gamepads (also gamepads *based* on the Gamepad layout):
/// "&lt;Gamepad&gt;"
///
/// // Matches the "Submit" control on all devices:
/// "*/{Submit}"
///
/// // Matches the key that prints the "a" character on the current keyboard layout:
/// "&lt;Keyboard&gt;/#(a)"
///
/// // Matches the X axis of the left stick on a gamepad.
/// "&lt;Gamepad&gt;/leftStick/x"
///
/// // Matches the orientation control of the right-hand XR controller:
/// "&lt;XRController&gt;{RightHand}/orientation"
///
/// // Matches all buttons on a gamepad.
/// "&lt;Gamepad&gt;/&lt;Button&gt;"
/// </code>
/// </example>
///
/// The structure of the API of this class is similar in spirit to <c>System.IO.Path</c>, i.e. it offers
/// a range of static methods that perform various operations on path strings.
///
/// To query controls on devices present in the system using control paths, use
/// <see cref="InputSystem.FindControls"/>. Also, control paths can be used with
/// <see cref="InputControl.this[string]"/> on every control. This makes it possible
/// to do things like:
///
/// <example>
/// Find key that prints "t" on current keyboard:
/// <code>
/// Keyboard.current["#(t)"]
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputControl.path"/>
/// <seealso cref="InputSystem.FindControls"/>
public static class InputControlPath
{
public const string Wildcard = "*";
public const string DoubleWildcard = "**";
public const char Separator = '/';
// We consider / a reserved character in control names. So, when this character does creep
// in there (e.g. from a device product name), we need to do something about it. We replace
// it with this character here.
// NOTE: Display names have no such restriction.
// NOTE: There are some Unicode characters that look sufficiently like a slash (e.g. FULLWIDTH SOLIDUS)
// but that only makes for rather confusing output. So we just replace with a blank.
internal const char SeparatorReplacement = ' ';
internal static string CleanSlashes(this String pathComponent)
{
return pathComponent.Replace(Separator, SeparatorReplacement);
}
public static string Combine(InputControl parent, string path)
{
if (parent == null)
{
if (string.IsNullOrEmpty(path))
return string.Empty;
if (path[0] != Separator)
return Separator + path;
return path;
}
if (string.IsNullOrEmpty(path))
return parent.path;
return $"{parent.path}/{path}";
}
/// <summary>
/// Options for customizing the behavior of <see cref="ToHumanReadableString(string,HumanReadableStringOptions,InputControl)"/>.
/// </summary>
[Flags]
public enum HumanReadableStringOptions
{
/// <summary>
/// The default behavior.
/// </summary>
None = 0,
/// <summary>
/// Do not mention the device of the control. For example, instead of "A [Gamepad]",
/// return just "A".
/// </summary>
OmitDevice = 1 << 1,
/// <summary>
/// When available, use short display names instead of long ones. For example, instead of "Left Button",
/// return "LMB".
/// </summary>
UseShortNames = 1 << 2,
}
////TODO: factor out the part that looks up an InputControlLayout.ControlItem from a given path
//// and make that available as a stand-alone API
////TODO: add option to customize path separation character
/// <summary>
/// Create a human readable string from the given control path.
/// </summary>
/// <param name="path">A control path such as "&lt;XRController>{LeftHand}/position".</param>
/// <param name="options">Customize the resulting string.</param>
/// <param name="control">An optional control. If supplied and the control or one of its children
/// matches the given <paramref name="path"/>, display names will be based on the matching control
/// rather than on static information available from <see cref="InputControlLayout"/>s.</param>
/// <returns>A string such as "Left Stick/X [Gamepad]".</returns>
/// <remarks>
/// This function is most useful for turning binding paths (see <see cref="InputBinding.path"/>)
/// into strings that can be displayed in UIs (such as rebinding screens). It is used by
/// the Unity editor itself to display binding paths in the UI.
///
/// The method uses display names (see <see cref="InputControlAttribute.displayName"/>,
/// <see cref="InputControlLayoutAttribute.displayName"/>, and <see cref="InputControlLayout.ControlItem.displayName"/>)
/// where possible. For example, "&lt;XInputController&gt;/buttonSouth" will be returned as
/// "A [Xbox Controller]" as the display name of <see cref="XInput.XInputController"/> is "XBox Controller"
/// and the display name of its "buttonSouth" control is "A".
///
/// Note that these lookups depend on the currently registered control layouts (see <see
/// cref="InputControlLayout"/>) and different strings may thus be returned for the same control
/// path depending on the layouts registered with the system.
///
/// <example>
/// <code>
/// InputControlPath.ToHumanReadableString("*/{PrimaryAction"); // -> "PrimaryAction [Any]"
/// InputControlPath.ToHumanReadableString("&lt;Gamepad&gt;/buttonSouth"); // -> "Button South [Gamepad]"
/// InputControlPath.ToHumanReadableString("&lt;XInputController&gt;/buttonSouth"); // -> "A [Xbox Controller]"
/// InputControlPath.ToHumanReadableString("&lt;Gamepad&gt;/leftStick/x"); // -> "Left Stick/X [Gamepad]"
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputBinding.path"/>
/// <seealso cref="InputBinding.ToDisplayString(InputBinding.DisplayStringOptions,InputControl)"/>
/// <seealso cref="InputActionRebindingExtensions.GetBindingDisplayString(InputAction,int,InputBinding.DisplayStringOptions)"/>
public static string ToHumanReadableString(string path,
HumanReadableStringOptions options = HumanReadableStringOptions.None,
InputControl control = null)
{
return ToHumanReadableString(path, out _, out _, options, control);
}
/// <summary>
/// Create a human readable string from the given control path.
/// </summary>
/// <param name="path">A control path such as "&lt;XRController>{LeftHand}/position".</param>
/// <param name="deviceLayoutName">Receives the name of the device layout that the control path was resolved to.
/// This is useful </param>
/// <param name="controlPath">Receives the path to the referenced control on the device or <c>null</c> if not applicable.
/// For example, with a <paramref name="path"/> of <c>"&lt;Gamepad&gt;/dpad/up"</c>, the resulting control path
/// will be <c>"dpad/up"</c>. This is useful when trying to look up additional resources (such as images) based on the
/// control that is being referenced.</param>
/// <param name="options">Customize the resulting string.</param>
/// <param name="control">An optional control. If supplied and the control or one of its children
/// matches the given <paramref name="path"/>, display names will be based on the matching control
/// rather than on static information available from <see cref="InputControlLayout"/>s.</param>
/// <returns>A string such as "Left Stick/X [Gamepad]".</returns>
/// <remarks>
/// This function is most useful for turning binding paths (see <see cref="InputBinding.path"/>)
/// into strings that can be displayed in UIs (such as rebinding screens). It is used by
/// the Unity editor itself to display binding paths in the UI.
///
/// The method uses display names (see <see cref="InputControlAttribute.displayName"/>,
/// <see cref="InputControlLayoutAttribute.displayName"/>, and <see cref="InputControlLayout.ControlItem.displayName"/>)
/// where possible. For example, "&lt;XInputController&gt;/buttonSouth" will be returned as
/// "A [Xbox Controller]" as the display name of <see cref="XInput.XInputController"/> is "XBox Controller"
/// and the display name of its "buttonSouth" control is "A".
///
/// Note that these lookups depend on the currently registered control layouts (see <see
/// cref="InputControlLayout"/>) and different strings may thus be returned for the same control
/// path depending on the layouts registered with the system.
///
/// <example>
/// <code>
/// InputControlPath.ToHumanReadableString("*/{PrimaryAction"); // -> "PrimaryAction [Any]"
/// InputControlPath.ToHumanReadableString("&lt;Gamepad&gt;/buttonSouth"); // -> "Button South [Gamepad]"
/// InputControlPath.ToHumanReadableString("&lt;XInputController&gt;/buttonSouth"); // -> "A [Xbox Controller]"
/// InputControlPath.ToHumanReadableString("&lt;Gamepad&gt;/leftStick/x"); // -> "Left Stick/X [Gamepad]"
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputBinding.path"/>
/// <seealso cref="InputBinding.ToDisplayString(InputBinding.DisplayStringOptions,InputControl)"/>
/// <seealso cref="InputActionRebindingExtensions.GetBindingDisplayString(InputAction,int,InputBinding.DisplayStringOptions)"/>
public static string ToHumanReadableString(string path,
out string deviceLayoutName,
out string controlPath,
HumanReadableStringOptions options = HumanReadableStringOptions.None,
InputControl control = null)
{
deviceLayoutName = null;
controlPath = null;
if (string.IsNullOrEmpty(path))
return string.Empty;
// If we have a control, see if the path matches something in its hierarchy. If so,
// don't both parsing the path and just use the matched control for creating a display
// string.
if (control != null)
{
var childControl = TryFindControl(control, path);
var matchedControl = childControl ?? (Matches(path, control) ? control : null);
if (matchedControl != null)
{
var text = (options & HumanReadableStringOptions.UseShortNames) != 0 &&
!string.IsNullOrEmpty(matchedControl.shortDisplayName)
? matchedControl.shortDisplayName
: matchedControl.displayName;
if ((options & HumanReadableStringOptions.OmitDevice) == 0)
text = $"{text} [{matchedControl.device.displayName}]";
deviceLayoutName = matchedControl.device.layout;
if (!(matchedControl is InputDevice))
controlPath = matchedControl.path.Substring(matchedControl.device.path.Length + 1);
return text;
}
}
var buffer = new StringBuilder();
var parser = new PathParser(path);
// For display names of controls and devices, we need to look at InputControlLayouts.
// If none is in place here, we establish a temporary layout cache while we go through
// the path. If one is in place already, we reuse what's already there.
using (InputControlLayout.CacheRef())
{
// First level is taken to be device.
if (parser.MoveToNextComponent())
{
// Keep track of which control layout we're on (if any) as we're crawling
// down the path.
var device = parser.current.ToHumanReadableString(null, null, out var currentLayoutName, out var _, options);
deviceLayoutName = currentLayoutName;
// Any additional levels (if present) are taken to form a control path on the device.
var isFirstControlLevel = true;
while (parser.MoveToNextComponent())
{
if (!isFirstControlLevel)
buffer.Append('/');
buffer.Append(parser.current.ToHumanReadableString(
currentLayoutName, controlPath, out currentLayoutName, out controlPath, options));
isFirstControlLevel = false;
}
if ((options & HumanReadableStringOptions.OmitDevice) == 0 && !string.IsNullOrEmpty(device))
{
buffer.Append(" [");
buffer.Append(device);
buffer.Append(']');
}
}
// If we didn't manage to figure out a display name, default to displaying
// the path as is.
if (buffer.Length == 0)
return path;
return buffer.ToString();
}
}
public static string[] TryGetDeviceUsages(string path)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
var parser = new PathParser(path);
if (!parser.MoveToNextComponent())
return null;
if (parser.current.m_Usages.length > 0)
return parser.current.m_Usages.ToArray(x => x.ToString());
return null;
}
/// <summary>
/// From the given control path, try to determine the device layout being used.
/// </summary>
/// <remarks>
/// This function will only use information available in the path itself or
/// in layouts referenced by the path. It will not look at actual devices
/// in the system. This is to make the behavior predictable and not dependent
/// on whether you currently have the right device connected or not.
/// </remarks>
/// <param name="path">A control path (like "/&lt;gamepad&gt;/leftStick")</param>
/// <returns>The name of the device layout used by the given control path or null
/// if the path does not specify a device layout or does so in a way that is not
/// supported by the function.</returns>
/// <exception cref="ArgumentNullException"><paramref name="path"/> is null</exception>
/// <example>
/// <code>
/// InputControlPath.TryGetDeviceLayout("/&lt;gamepad&gt;/leftStick"); // Returns "gamepad".
/// InputControlPath.TryGetDeviceLayout("/*/leftStick"); // Returns "*".
/// InputControlPath.TryGetDeviceLayout("/gamepad/leftStick"); // Returns null. "gamepad" is a device name here.
/// </code>
/// </example>
public static string TryGetDeviceLayout(string path)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
var parser = new PathParser(path);
if (!parser.MoveToNextComponent())
return null;
if (parser.current.m_Layout.length > 0)
return parser.current.m_Layout.ToString().Unescape();
if (parser.current.isWildcard)
return Wildcard;
return null;
}
////TODO: return Substring and use path parser; should get rid of allocations
// From the given control path, try to determine the control layout being used.
// NOTE: Allocates!
public static string TryGetControlLayout(string path)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
var pathLength = path.Length;
var indexOfLastSlash = path.LastIndexOf('/');
if (indexOfLastSlash == -1 || indexOfLastSlash == 0)
{
// If there's no '/' at all in the path, it surely does not mention
// a control. Same if the '/' is the first thing in the path.
return null;
}
// Simplest case where control layout is mentioned explicitly with '<..>'.
// Note this will only catch if the control is *only* referenced by layout and not by anything else
// in addition (like usage or name).
if (pathLength > indexOfLastSlash + 2 && path[indexOfLastSlash + 1] == '<' && path[pathLength - 1] == '>')
{
var layoutNameStart = indexOfLastSlash + 2;
var layoutNameLength = pathLength - layoutNameStart - 1;
return path.Substring(layoutNameStart, layoutNameLength);
}
// Have to actually look at the path in detail.
var parser = new PathParser(path);
if (!parser.MoveToNextComponent())
return null;
if (parser.current.isWildcard)
throw new NotImplementedException();
if (parser.current.m_Layout.length == 0)
return null;
var deviceLayoutName = parser.current.m_Layout.ToString();
if (!parser.MoveToNextComponent())
return null; // No control component.
if (parser.current.isWildcard)
return Wildcard;
return FindControlLayoutRecursive(ref parser, deviceLayoutName.Unescape());
}
private static string FindControlLayoutRecursive(ref PathParser parser, string layoutName)
{
using (InputControlLayout.CacheRef())
{
// Load layout.
var layout = InputControlLayout.cache.FindOrLoadLayout(new InternedString(layoutName), throwIfNotFound: false);
if (layout == null)
return null;
// Search for control layout. May have to jump to other layouts
// and search in them.
return FindControlLayoutRecursive(ref parser, layout);
}
}
private static string FindControlLayoutRecursive(ref PathParser parser, InputControlLayout layout)
{
string currentResult = null;
var controlCount = layout.controls.Count;
for (var i = 0; i < controlCount; ++i)
{
////TODO: shortcut the search if we have a match and there's no wildcards to consider
// Skip control layout if it doesn't match.
if (!ControlLayoutMatchesPathComponent(ref layout.m_Controls[i], ref parser))
continue;
var controlLayoutName = layout.m_Controls[i].layout;
// If there's more in the path, try to dive into children by jumping to the
// control's layout.
if (!parser.isAtEnd)
{
var childPathParser = parser;
if (childPathParser.MoveToNextComponent())
{
var childControlLayoutName = FindControlLayoutRecursive(ref childPathParser, controlLayoutName);
if (childControlLayoutName != null)
{
if (currentResult != null && childControlLayoutName != currentResult)
return null;
currentResult = childControlLayoutName;
}
}
}
else if (currentResult != null && controlLayoutName != currentResult)
return null;
else
currentResult = controlLayoutName.ToString();
}
return currentResult;
}
private static bool ControlLayoutMatchesPathComponent(ref InputControlLayout.ControlItem controlItem, ref PathParser parser)
{
// Match layout.
var layout = parser.current.m_Layout;
if (layout.length > 0)
{
if (!StringMatches(layout, controlItem.layout))
return false;
}
// Match usage.
if (parser.current.m_Usages.length > 0)
{
// All of usages should match to the one of usage in the control
for (int usageIndex = 0; usageIndex < parser.current.m_Usages.length; ++usageIndex)
{
var usage = parser.current.m_Usages[usageIndex];
if (usage.length > 0)
{
var usageCount = controlItem.usages.Count;
var anyUsageMatches = false;
for (var i = 0; i < usageCount; ++i)
{
if (StringMatches(usage, controlItem.usages[i]))
{
anyUsageMatches = true;
break;
}
}
if (!anyUsageMatches)
return false;
}
}
}
// Match name.
var name = parser.current.m_Name;
if (name.length > 0)
{
if (!StringMatches(name, controlItem.name))
return false;
}
return true;
}
// Match two name strings allowing for wildcards.
// 'str' may contain wildcards. 'matchTo' may not.
private static bool StringMatches(Substring str, InternedString matchTo)
{
var strLength = str.length;
var matchToLength = matchTo.length;
// Can't compare lengths here because str may contain wildcards and
// thus be shorter than matchTo and still match.
var matchToLowerCase = matchTo.ToLower();
// We manually walk the string here so that we can deal with "normal"
// comparisons as well as with wildcards.
var posInMatchTo = 0;
var posInStr = 0;
while (posInStr < strLength && posInMatchTo < matchToLength)
{
var nextChar = str[posInStr];
if (nextChar == '\\' && posInStr + 1 < strLength)
nextChar = str[++posInStr];
if (nextChar == '*')
{
////TODO: make sure we don't end up with ** here
if (posInStr == strLength - 1)
return true; // Wildcard at end of string so rest is matched.
++posInStr;
nextChar = char.ToLowerInvariant(str[posInStr]);
while (posInMatchTo < matchToLength && matchToLowerCase[posInMatchTo] != nextChar)
++posInMatchTo;
if (posInMatchTo == matchToLength)
return false; // Matched all the way to end of matchTo but there's more in str after the wildcard.
}
else if (char.ToLowerInvariant(nextChar) != matchToLowerCase[posInMatchTo])
{
return false;
}
++posInMatchTo;
++posInStr;
}
return posInMatchTo == matchToLength && posInStr == strLength; // Check if we have consumed all input. Prevent prefix-only match.
}
public static InputControl TryFindControl(InputControl control, string path, int indexInPath = 0)
{
return TryFindControl<InputControl>(control, path, indexInPath);
}
public static InputControl[] TryFindControls(InputControl control, string path, int indexInPath = 0)
{
var matches = new InputControlList<InputControl>(Allocator.Temp);
try
{
TryFindControls(control, path, indexInPath, ref matches);
return matches.ToArray();
}
finally
{
matches.Dispose();
}
}
public static int TryFindControls(InputControl control, string path, ref InputControlList<InputControl> matches, int indexInPath = 0)
{
return TryFindControls(control, path, indexInPath, ref matches);
}
/// <summary>
/// Return the first child control that matches the given path.
/// </summary>
/// <param name="control">Control root at which to start the search.</param>
/// <param name="path">Path of the control to find. Can be <c>null</c> or empty, in which case <c>null</c>
/// is returned.</param>
/// <param name="indexInPath">Index in <paramref name="path"/> at which to start parsing. Defaults to
/// 0, i.e. parsing starts at the first character in the path.</param>
/// <returns>The first (direct or indirect) child control of <paramref name="control"/> that matches
/// <paramref name="path"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="control"/> is <c>null</c>.</exception>
/// <remarks>
/// Does not allocate.
///
/// Note that if multiple child controls match the given path, which one is returned depends on the
/// ordering of controls. The result should be considered indeterministic in this case.
///
/// <example>
/// <code>
/// // Find X control of left stick on current gamepad.
/// InputControlPath.TryFindControl(Gamepad.current, "leftStick/x");
///
/// // Find control with PrimaryAction usage on current mouse.
/// InputControlPath.TryFindControl(Mouse.current, "{PrimaryAction}");
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputControl.this[string]"/>
public static TControl TryFindControl<TControl>(InputControl control, string path, int indexInPath = 0)
where TControl : InputControl
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (string.IsNullOrEmpty(path))
return null;
if (indexInPath == 0 && path[0] == '/')
++indexInPath;
var none = new InputControlList<TControl>();
return MatchControlsRecursive(control, path, indexInPath, ref none, matchMultiple: false);
}
/// <summary>
/// Perform a search for controls starting with the given control as root and matching
/// the given path from the given position. Puts all matching controls on the list and
/// returns the number of controls that have been matched.
/// </summary>
/// <param name="control">Control at which the given path is rooted.</param>
/// <param name="path"></param>
/// <param name="indexInPath"></param>
/// <param name="matches"></param>
/// <typeparam name="TControl"></typeparam>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
/// <remarks>
/// Matching is case-insensitive.
///
/// Does not allocate managed memory.
/// </remarks>
public static int TryFindControls<TControl>(InputControl control, string path, int indexInPath,
ref InputControlList<TControl> matches)
where TControl : InputControl
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (path == null)
throw new ArgumentNullException(nameof(path));
if (indexInPath == 0 && path[0] == '/')
++indexInPath;
var countBefore = matches.Count;
MatchControlsRecursive(control, path, indexInPath, ref matches, matchMultiple: true);
return matches.Count - countBefore;
}
////REVIEW: what's the difference between TryFindChild and TryFindControl??
public static InputControl TryFindChild(InputControl control, string path, int indexInPath = 0)
{
return TryFindChild<InputControl>(control, path, indexInPath);
}
public static TControl TryFindChild<TControl>(InputControl control, string path, int indexInPath = 0)
where TControl : InputControl
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (path == null)
throw new ArgumentNullException(nameof(path));
var children = control.children;
var childCount = children.Count;
for (var i = 0; i < childCount; ++i)
{
var child = children[i];
var match = TryFindControl<TControl>(child, path, indexInPath);
if (match != null)
return match;
}
return null;
}
////REVIEW: probably would be good to have a Matches(string,string) version
public static bool Matches(string expected, InputControl control)
{
if (string.IsNullOrEmpty(expected))
throw new ArgumentNullException(nameof(expected));
if (control == null)
throw new ArgumentNullException(nameof(control));
var parser = new PathParser(expected);
return MatchesRecursive(ref parser, control);
}
internal static bool MatchControlComponent(ref ParsedPathComponent expectedControlComponent, ref InputControlLayout.ControlItem controlItem, bool matchAlias = false)
{
bool controlItemNameMatched = false;
var anyUsageMatches = false;
// Check to see that there is a match with the name or alias if specified
// Exit early if we can't create a match.
if (!expectedControlComponent.m_Name.isEmpty)
{
if (StringMatches(expectedControlComponent.m_Name, controlItem.name))
controlItemNameMatched = true;
else if (matchAlias)
{
var aliases = controlItem.aliases;
for (var i = 0; i < aliases.Count; i++)
{
if (StringMatches(expectedControlComponent.m_Name, aliases[i]))
{
controlItemNameMatched = true;
break;
}
}
}
else
return false;
}
// All of usages should match to the one of usage in the control
foreach (var usage in expectedControlComponent.m_Usages)
{
if (!usage.isEmpty)
{
var usageCount = controlItem.usages.Count;
for (var i = 0; i < usageCount; ++i)
{
if (StringMatches(usage, controlItem.usages[i]))
{
anyUsageMatches = true;
break;
}
}
}
}
// Return whether or not we were able to match an alias or a usage
return controlItemNameMatched || anyUsageMatches;
}
/// <summary>
/// Check whether the given path matches <paramref name="control"/> or any of its parents.
/// </summary>
/// <param name="expected">A control path.</param>
/// <param name="control">An input control.</param>
/// <returns>True if the given path matches at least a partial path to <paramref name="control"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="expected"/> is <c>null</c> or empty -or-
/// <paramref name="control"/> is <c>null</c>.</exception>
/// <remarks>
/// <example>
/// <code>
/// // True as the path matches the Keyboard device itself, i.e. the parent of
/// // Keyboard.aKey.
/// InputControlPath.MatchesPrefix("&lt;Keyboard&gt;", Keyboard.current.aKey);
///
/// // False as the path matches none of the controls leading to Keyboard.aKey.
/// InputControlPath.MatchesPrefix("&lt;Gamepad&gt;", Keyboard.current.aKey);
///
/// // True as the path matches Keyboard.aKey itself.
/// InputControlPath.MatchesPrefix("&lt;Keyboard&gt;/a", Keyboard.current.aKey);
/// </code>
/// </example>
/// </remarks>
public static bool MatchesPrefix(string expected, InputControl control)
{
if (string.IsNullOrEmpty(expected))
throw new ArgumentNullException(nameof(expected));
if (control == null)
throw new ArgumentNullException(nameof(control));
var parser = new PathParser(expected);
if (MatchesRecursive(ref parser, control, prefixOnly: true) && parser.isAtEnd)
return true;
return false;
}
private static bool MatchesRecursive(ref PathParser parser, InputControl currentControl, bool prefixOnly = false)
{
// Recurse into parent before looking at the current control. This
// will advance the parser to where our control is in the path.
var parent = currentControl.parent;
if (parent != null && !MatchesRecursive(ref parser, parent, prefixOnly))
return false;
// Stop if there's no more path left.
if (!parser.MoveToNextComponent())
return prefixOnly; // Failure if we match full path, success if we match prefix only.
// Match current path component against current control.
return parser.current.Matches(currentControl);
}
////TODO: refactor this to use the new PathParser
/// <summary>
/// Recursively match path elements in <paramref name="path"/>.
/// </summary>
/// <param name="control">Current control we're at.</param>
/// <param name="path">Control path we are matching against.</param>
/// <param name="indexInPath">Index of current component in <paramref name="path"/>.</param>
/// <param name="matches"></param>
/// <param name="matchMultiple"></param>
/// <typeparam name="TControl"></typeparam>
/// <returns></returns>
private static TControl MatchControlsRecursive<TControl>(InputControl control, string path, int indexInPath,
ref InputControlList<TControl> matches, bool matchMultiple)
where TControl : InputControl
{
var pathLength = path.Length;
// Try to get a match. A path spec has three components:
// "<layout>{usage}name"
// All are optional but at least one component must be present.
// Names can be aliases, too.
// We don't tap InputControl.path strings of controls so as to not create a
// bunch of string objects while feeling our way down the hierarchy.
var controlIsMatch = true;
// Match by layout.
if (path[indexInPath] == '<')
{
++indexInPath;
controlIsMatch =
MatchPathComponent(control.layout, path, ref indexInPath, PathComponentType.Layout);
// If the layout isn't a match, walk up the base layout
// chain and match each base layout.
if (!controlIsMatch)
{
var baseLayout = control.m_Layout;
while (InputControlLayout.s_Layouts.baseLayoutTable.TryGetValue(baseLayout, out baseLayout))
{
controlIsMatch = MatchPathComponent(baseLayout, path, ref indexInPath,
PathComponentType.Layout);
if (controlIsMatch)
break;
}
}
}
// Match by usage.
while (indexInPath < pathLength && path[indexInPath] == '{' && controlIsMatch)
{
++indexInPath;
for (var i = 0; i < control.usages.Count; ++i)
{
controlIsMatch = MatchPathComponent(control.usages[i], path, ref indexInPath, PathComponentType.Usage);
if (controlIsMatch)
break;
}
}
// Match by display name.
if (indexInPath < pathLength - 1 && controlIsMatch && path[indexInPath] == '#' &&
path[indexInPath + 1] == '(')
{
indexInPath += 2;
controlIsMatch = MatchPathComponent(control.displayName, path, ref indexInPath,
PathComponentType.DisplayName);
}
// Match by name.
if (indexInPath < pathLength && controlIsMatch && path[indexInPath] != '/')
{
// Normal name match.
controlIsMatch = MatchPathComponent(control.name, path, ref indexInPath, PathComponentType.Name);
// Alternative match by alias.
if (!controlIsMatch)
{
for (var i = 0; i < control.aliases.Count && !controlIsMatch; ++i)
{
controlIsMatch = MatchPathComponent(control.aliases[i], path, ref indexInPath,
PathComponentType.Name);
}
}
}
// If we have a match, return it or, if there's children, recurse into them.
if (controlIsMatch)
{
// If we ended up on a wildcard, we've successfully matched it.
if (indexInPath < pathLength && path[indexInPath] == '*')
++indexInPath;
// If we've reached the end of the path, we have a match.
if (indexInPath == pathLength)
{
// Check type.
if (!(control is TControl match))
return null;
if (matchMultiple)
matches.Add(match);
return match;
}
// If we've reached a separator, dive into our children.
if (path[indexInPath] == '/')
{
++indexInPath;
// Silently accept trailing slashes.
if (indexInPath == pathLength)
{
// Check type.
if (!(control is TControl match))
return null;
if (matchMultiple)
matches.Add(match);
return match;
}
// See if we want to match children by usage or by name.
TControl lastMatch;
if (path[indexInPath] == '{')
{
// Usages are kind of like entry points that can route to anywhere else
// on a device's control hierarchy and then we keep going from that re-routed
// point.
lastMatch = MatchByUsageAtDeviceRootRecursive(control.device, path, indexInPath, ref matches, matchMultiple);
}
else
{
// Go through children and see what we can match.
lastMatch = MatchChildrenRecursive(control, path, indexInPath, ref matches, matchMultiple);
}
return lastMatch;
}
}
return null;
}
private static TControl MatchByUsageAtDeviceRootRecursive<TControl>(InputDevice device, string path, int indexInPath,
ref InputControlList<TControl> matches, bool matchMultiple)
where TControl : InputControl
{
// NOTE: m_UsagesForEachControl includes usages for the device. m_UsageToControl does not.
var usages = device.m_UsagesForEachControl;
if (usages == null)
return null;
var usageCount = device.m_UsageToControl.LengthSafe();
var startIndex = indexInPath + 1;
var pathCanMatchMultiple = PathComponentCanYieldMultipleMatches(path, indexInPath);
var pathLength = path.Length;
Debug.Assert(path[indexInPath] == '{');
++indexInPath;
if (indexInPath == pathLength)
throw new ArgumentException($"Invalid path spec '{path}'; trailing '{{'", nameof(path));
TControl lastMatch = null;
for (var i = 0; i < usageCount; ++i)
{
var usage = usages[i];
Debug.Assert(!string.IsNullOrEmpty(usage), "Usage entry is empty");
// Match usage against path.
var usageIsMatch = MatchPathComponent(usage, path, ref indexInPath, PathComponentType.Usage);
// If it isn't a match, go to next usage.
if (!usageIsMatch)
{
indexInPath = startIndex;
continue;
}
var controlMatchedByUsage = device.m_UsageToControl[i];
// If there's more to go in the path, dive into the children of the control.
if (indexInPath < pathLength && path[indexInPath] == '/')
{
lastMatch = MatchChildrenRecursive(controlMatchedByUsage, path, indexInPath + 1,
ref matches, matchMultiple);
// We can stop going through usages if we matched something and the
// path component covering usage does not contain wildcards.
if (lastMatch != null && !pathCanMatchMultiple)
break;
// We can stop going through usages if we have a match and are only
// looking for a single one.
if (lastMatch != null && !matchMultiple)
break;
}
else
{
lastMatch = controlMatchedByUsage as TControl;
if (lastMatch != null)
{
if (matchMultiple)
matches.Add(lastMatch);
else
{
// Only looking for single match and we have one.
break;
}
}
}
}
return lastMatch;
}
private static TControl MatchChildrenRecursive<TControl>(InputControl control, string path, int indexInPath,
ref InputControlList<TControl> matches, bool matchMultiple)
where TControl : InputControl
{
var children = control.children;
var childCount = children.Count;
TControl lastMatch = null;
var pathCanMatchMultiple = PathComponentCanYieldMultipleMatches(path, indexInPath);
for (var i = 0; i < childCount; ++i)
{
var child = children[i];
var childMatch = MatchControlsRecursive(child, path, indexInPath, ref matches, matchMultiple);
if (childMatch == null)
continue;
// If the child matched something an there's no wildcards in the child
// portion of the path, we can stop searching.
if (!pathCanMatchMultiple)
return childMatch;
// If we are only looking for the first match and a child matched,
// we can also stop.
if (!matchMultiple)
return childMatch;
// Otherwise we have to go hunting through the hierarchy in case there are
// more matches.
lastMatch = childMatch;
}
return lastMatch;
}
private enum PathComponentType
{
Name,
DisplayName,
Usage,
Layout
}
private static bool MatchPathComponent(string component, string path, ref int indexInPath, PathComponentType componentType, int startIndexInComponent = 0)
{
Debug.Assert(component != null, "Component string is null");
Debug.Assert(path != null, "Path is null");
var componentLength = component.Length;
var pathLength = path.Length;
var startIndex = indexInPath;
// Try to walk the name as far as we can.
var indexInComponent = startIndexInComponent;
while (indexInPath < pathLength)
{
// Check if we've reached a terminator in the path.
var nextCharInPath = path[indexInPath];
if (nextCharInPath == '\\' && indexInPath + 1 < pathLength)
{
// Escaped character. Bypass treatment of special characters below.
++indexInPath;
nextCharInPath = path[indexInPath];
}
else
{
if (nextCharInPath == '/' && componentType == PathComponentType.Name)
break;
if ((nextCharInPath == '>' && componentType == PathComponentType.Layout)
|| (nextCharInPath == '}' && componentType == PathComponentType.Usage)
|| (nextCharInPath == ')' && componentType == PathComponentType.DisplayName))
{
++indexInPath;
break;
}
////TODO: allow only single '*' and recognize '**'
// If we've reached a '*' in the path, skip character in name.
if (nextCharInPath == '*')
{
// But first let's see if we have something after the wildcard that matches the rest of the component.
// This could be when, for example, we hit "T" on matching "leftTrigger" against "*Trigger". We have to stop
// gobbling up characters for the wildcard when reaching "Trigger" in the component name.
//
// NOTE: Just looking at the very next character only is *NOT* enough. We need to match the entire rest of
// the path. Otherwise, in the example above, we would stop on seeing the lowercase 't' and then be left
// trying to match "tTrigger" against "Trigger".
var indexAfterWildcard = indexInPath + 1;
if (indexInPath < (pathLength - 1) &&
indexInComponent < componentLength &&
MatchPathComponent(component, path, ref indexAfterWildcard, componentType, indexInComponent))
{
indexInPath = indexAfterWildcard;
return true;
}
if (indexInComponent < componentLength)
++indexInComponent;
else
return true;
continue;
}
}
// If we've reached the end of the component name, we did so before
// we've reached a terminator
if (indexInComponent == componentLength)
{
indexInPath = startIndex;
return false;
}
var charInComponent = component[indexInComponent];
if (charInComponent == nextCharInPath || char.ToLowerInvariant(charInComponent) == char.ToLowerInvariant(nextCharInPath))
{
++indexInComponent;
++indexInPath;
}
else
{
// Name isn't a match.
indexInPath = startIndex;
return false;
}
}
if (indexInComponent == componentLength)
return true;
indexInPath = startIndex;
return false;
}
private static bool PathComponentCanYieldMultipleMatches(string path, int indexInPath)
{
var indexOfNextSlash = path.IndexOf('/', indexInPath);
if (indexOfNextSlash == -1)
return path.IndexOf('*', indexInPath) != -1 || path.IndexOf('<', indexInPath) != -1;
var length = indexOfNextSlash - indexInPath;
return path.IndexOf('*', indexInPath, length) != -1 || path.IndexOf('<', indexInPath, length) != -1;
}
/// <summary>
/// A single component of a parsed control path as returned by <see cref="Parse"/>. For example, in the
/// control path <c>"&lt;Gamepad&gt;/buttonSouth"</c>, there are two parts: <c>"&lt;Gamepad&gt;"</c>
/// and <c>"buttonSouth"</c>.
/// </summary>
/// <seealso cref="Parse"/>
public struct ParsedPathComponent
{
// Accessing these means no allocations (except when there are multiple usages).
internal Substring m_Layout;
internal InlinedArray<Substring> m_Usages;
internal Substring m_Name;
internal Substring m_DisplayName;
/// <summary>
/// Name of the layout (the part between '&lt;' and '&gt;') referenced in the component or <c>null</c> if no layout
/// is specified. In <c>"&lt;Gamepad&gt;/buttonSouth"</c> the first component will return
/// <c>"Gamepad"</c> from this property and the second component will return <c>null</c>.
/// </summary>
/// <seealso cref="InputControlLayout"/>
/// <seealso cref="InputSystem.LoadLayout"/>
/// <seealso cref="InputControl.layout"/>
public string layout => m_Layout.ToString();
/// <summary>
/// List of device or control usages (the part between '{' and '}') referenced in the component or an empty
/// enumeration. In <c>"&lt;XRController&gt;{RightHand}/trigger"</c>, for example, the
/// first component will have a single element <c>"RightHand"</c> in the enumeration
/// and the second component will have an empty enumeration.
/// </summary>
/// <seealso cref="InputControl.usages"/>
/// <seealso cref="InputSystem.AddDeviceUsage(InputDevice,string)"/>
public IEnumerable<string> usages => m_Usages.Select(x => x.ToString());
/// <summary>
/// Name of the device or control referenced in the component or <c>null</c> In
/// <c>"&lt;Gamepad&gt;/buttonSouth"</c>, for example, the first component will
/// have a <c>null</c> name and the second component will have <c>"buttonSouth"</c>
/// in the name.
/// </summary>
/// <seealso cref="InputControl.name"/>
public string name => m_Name.ToString();
/// <summary>
/// Display name of the device or control (the part inside of '#(...)') referenced in the component
/// or <c>null</c>. In <c>"&lt;Keyboard&gt;/#(*)"</c>, for example, the first component will
/// have a null displayName and the second component will have a displayName of <c>"*"</c>.
/// </summary>
/// <seealso cref="InputControl.displayName"/>
public string displayName => m_DisplayName.ToString();
////REVIEW: This one isn't well-designed enough yet to be exposed. And double-wildcards are not yet supported.
internal bool isWildcard => m_Name == Wildcard;
internal bool isDoubleWildcard => m_Name == DoubleWildcard;
internal string ToHumanReadableString(string parentLayoutName, string parentControlPath, out string referencedLayoutName,
out string controlPath, HumanReadableStringOptions options)
{
referencedLayoutName = null;
controlPath = null;
var result = string.Empty;
if (isWildcard)
result += "Any";
if (m_Usages.length > 0)
{
var combinedUsages = string.Empty;
for (var i = 0; i < m_Usages.length; ++i)
{
if (m_Usages[i].isEmpty)
continue;
if (combinedUsages != string.Empty)
combinedUsages += " & " + ToHumanReadableString(m_Usages[i]);
else
combinedUsages = ToHumanReadableString(m_Usages[i]);
}
if (combinedUsages != string.Empty)
{
if (result != string.Empty)
result += ' ' + combinedUsages;
else
result += combinedUsages;
}
}
if (!m_Layout.isEmpty)
{
referencedLayoutName = m_Layout.ToString();
// Where possible, use the displayName of the given layout rather than
// just the internal layout name.
string layoutString;
var referencedLayout = InputControlLayout.cache.FindOrLoadLayout(referencedLayoutName, throwIfNotFound: false);
if (referencedLayout != null && !string.IsNullOrEmpty(referencedLayout.m_DisplayName))
layoutString = referencedLayout.m_DisplayName;
else
layoutString = ToHumanReadableString(m_Layout);
if (!string.IsNullOrEmpty(result))
result += ' ' + layoutString;
else
result += layoutString;
}
if (!m_Name.isEmpty && !isWildcard)
{
// If we have a layout from a preceding path component, try to find
// the control by name on the layout. If we find it, use its display
// name rather than the name referenced in the binding.
string nameString = null;
if (!string.IsNullOrEmpty(parentLayoutName))
{
// NOTE: This produces a fully merged layout. We should thus pick up display names
// from base layouts automatically wherever applicable.
var parentLayout =
InputControlLayout.cache.FindOrLoadLayout(new InternedString(parentLayoutName), throwIfNotFound: false);
if (parentLayout != null)
{
var controlName = new InternedString(m_Name.ToString());
var control = parentLayout.FindControlIncludingArrayElements(controlName, out var arrayIndex);
if (control != null)
{
// Synthesize path of control.
if (string.IsNullOrEmpty(parentControlPath))
{
if (arrayIndex != -1)
controlPath = $"{control.Value.name}{arrayIndex}";
else
controlPath = control.Value.name;
}
else
{
if (arrayIndex != -1)
controlPath = $"{parentControlPath}/{control.Value.name}{arrayIndex}";
else
controlPath = $"{parentControlPath}/{control.Value.name}";
}
var shortDisplayName = (options & HumanReadableStringOptions.UseShortNames) != 0
? control.Value.shortDisplayName
: null;
var displayName = !string.IsNullOrEmpty(shortDisplayName)
? shortDisplayName
: control.Value.displayName;
if (!string.IsNullOrEmpty(displayName))
{
if (arrayIndex != -1)
nameString = $"{displayName} #{arrayIndex}";
else
nameString = displayName;
}
// If we don't have an explicit <layout> part in the component,
// remember the name of the layout referenced by the control name so
// that path components further down the line can keep looking up their
// display names.
if (string.IsNullOrEmpty(referencedLayoutName))
referencedLayoutName = control.Value.layout;
}
}
}
if (nameString == null)
nameString = ToHumanReadableString(m_Name);
if (!string.IsNullOrEmpty(result))
result += ' ' + nameString;
else
result += nameString;
}
if (!m_DisplayName.isEmpty)
{
var str = $"\"{ToHumanReadableString(m_DisplayName)}\"";
if (!string.IsNullOrEmpty(result))
result += ' ' + str;
else
result += str;
}
return result;
}
private static string ToHumanReadableString(Substring substring)
{
return substring.ToString().Unescape("/*{<", "/*{<");
}
/// <summary>
/// Whether the given control matches the constraints of this path component.
/// </summary>
/// <param name="control">Control to match against the path spec.</param>
/// <returns>True if <paramref name="control"/> matches the constraints.</returns>
public bool Matches(InputControl control)
{
// Match layout.
if (!m_Layout.isEmpty)
{
// Check for direct match.
var layoutMatches = ComparePathElementToString(m_Layout, control.layout);
if (!layoutMatches)
{
// No direct match but base layout may match.
var baseLayout = control.m_Layout;
while (InputControlLayout.s_Layouts.baseLayoutTable.TryGetValue(baseLayout, out baseLayout) && !layoutMatches)
layoutMatches = ComparePathElementToString(m_Layout, baseLayout.ToString());
}
if (!layoutMatches)
return false;
}
// Match usage.
if (m_Usages.length > 0)
{
for (var i = 0; i < m_Usages.length; ++i)
{
if (!m_Usages[i].isEmpty)
{
var controlUsages = control.usages;
var haveUsageMatch = false;
for (var ci = 0; ci < controlUsages.Count; ++ci)
if (ComparePathElementToString(m_Usages[i], controlUsages[ci]))
{
haveUsageMatch = true;
break;
}
if (!haveUsageMatch)
return false;
}
}
}
// Match name.
if (!m_Name.isEmpty && !isWildcard)
{
////FIXME: unlike the matching path we have in MatchControlsRecursive, this does not take aliases into account
if (!ComparePathElementToString(m_Name, control.name))
return false;
}
// Match display name.
if (!m_DisplayName.isEmpty)
{
if (!ComparePathElementToString(m_DisplayName, control.displayName))
return false;
}
return true;
}
// In a path, characters may be escaped so in those cases, we can't just compare
// character-by-character.
private static bool ComparePathElementToString(Substring pathElement, string element)
{
var pathElementLength = pathElement.length;
var elementLength = element.Length;
for (int i = 0, j = 0;; i++, j++)
{
var pathElementDone = i == pathElementLength;
var elementDone = j == elementLength;
if (pathElementDone || elementDone)
return pathElementDone == elementDone;
var ch = pathElement[i];
if (ch == '\\' && i + 1 < pathElementLength)
ch = pathElement[++i];
if (char.ToLowerInvariant(ch) != char.ToLowerInvariant(element[j]))
return false;
}
}
}
/// <summary>
/// Splits a control path into its separate components.
/// </summary>
/// <param name="path">A control path such as <c>"&lt;Gamepad&gt;/buttonSouth"</c>.</param>
/// <returns>An enumeration of the parsed components. The enumeration is empty if the given
/// <paramref name="path"/> is empty.</returns>
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c> or empty.</exception>
/// <remarks>
/// You can use this method, for example, to separate out the components in a binding's <see cref="InputBinding.path"/>.
///
/// <example>
/// <code>
/// var parsed = InputControlPath.Parse("&lt;XRController&gt;{LeftHand}/trigger").ToArray();
///
/// Debug.Log(parsed.Length); // Prints 2.
/// Debug.Log(parsed[0].layout); // Prints "XRController".
/// Debug.Log(parsed[0].name); // Prints an empty string.
/// Debug.Log(parsed[0].usages.First()); // Prints "LeftHand".
/// Debug.Log(parsed[1].layout); // Prints null.
/// Debug.Log(parsed[1].name); // Prints "trigger".
///
/// // Find out if the given device layout is based on "TrackedDevice".
/// Debug.Log(InputSystem.IsFirstLayoutBasedOnSecond(parsed[0].layout, "TrackedDevice")); // Prints true.
///
/// // Load the device layout referenced by the path.
/// var layout = InputSystem.LoadLayout(parsed[0].layout);
/// Debug.Log(layout.baseLayouts.First()); // Prints "TrackedDevice".
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputBinding.path"/>
/// <seealso cref="InputSystem.FindControl"/>
public static IEnumerable<ParsedPathComponent> Parse(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));
var parser = new PathParser(path);
while (parser.MoveToNextComponent())
yield return parser.current;
}
// NOTE: Must not allocate!
private struct PathParser
{
private string path;
private int length;
private int leftIndexInPath;
private int rightIndexInPath; // Points either to a '/' character or one past the end of the path string.
public ParsedPathComponent current;
public bool isAtEnd => rightIndexInPath == length;
public PathParser(string path)
{
Debug.Assert(path != null);
this.path = path;
length = path.Length;
leftIndexInPath = 0;
rightIndexInPath = 0;
current = new ParsedPathComponent();
}
// Update parsing state and 'current' to next component in path.
// Returns true if the was another component or false if the end of the path was reached.
public bool MoveToNextComponent()
{
// See if we've the end of the path string.
if (rightIndexInPath == length)
return false;
// Make our current right index our new left index and find
// a new right index from there.
leftIndexInPath = rightIndexInPath;
if (path[leftIndexInPath] == '/')
{
++leftIndexInPath;
rightIndexInPath = leftIndexInPath;
if (leftIndexInPath == length)
return false;
}
// Parse <...> layout part, if present.
var layout = new Substring();
if (rightIndexInPath < length && path[rightIndexInPath] == '<')
layout = ParseComponentPart('>');
////FIXME: with multiple usages, this will allocate
////FIXME: Why the heck is this allocating? Should not allocate here! Worse yet, we do ToArray() down there.
// Parse {...} usage part, if present.
var usages = new InlinedArray<Substring>();
while (rightIndexInPath < length && path[rightIndexInPath] == '{')
usages.AppendWithCapacity(ParseComponentPart('}'));
// Parse display name part, if present.
var displayName = new Substring();
if (rightIndexInPath < length - 1 && path[rightIndexInPath] == '#' && path[rightIndexInPath + 1] == '(')
{
++rightIndexInPath;
displayName = ParseComponentPart(')');
}
// Parse name part, if present.
var name = new Substring();
if (rightIndexInPath < length && path[rightIndexInPath] != '/')
name = ParseComponentPart('/');
current = new ParsedPathComponent
{
m_Layout = layout,
m_Usages = usages,
m_Name = name,
m_DisplayName = displayName
};
return leftIndexInPath != rightIndexInPath;
}
private Substring ParseComponentPart(char terminator)
{
if (terminator != '/') // Name has no corresponding left side terminator.
++rightIndexInPath;
var partStartIndex = rightIndexInPath;
while (rightIndexInPath < length && path[rightIndexInPath] != terminator)
{
if (path[rightIndexInPath] == '\\' && rightIndexInPath + 1 < length)
++rightIndexInPath;
++rightIndexInPath;
}
var partLength = rightIndexInPath - partStartIndex;
if (rightIndexInPath < length && terminator != '/')
++rightIndexInPath; // Skip past terminator.
return new Substring(path, partStartIndex, partLength);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ce150b9c1acd40e0a087ca3f3257831d
timeCreated: 1507092303

View File

@@ -0,0 +1,208 @@
using System;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.Scripting;
////TODO: come up with a mechanism to allow (certain) processors to be stateful
////TODO: cache processors globally; there's no need to instantiate the same processor with the same parameters multiple times
//// (except if they do end up being stateful)
namespace UnityEngine.InputSystem
{
/// <summary>
/// A processor that conditions/transforms input values.
/// </summary>
/// <remarks>
/// To define a custom processor, it is usable best to derive from <see cref="InputProcessor{TValue}"/>
/// instead of from this class. Doing so will avoid having to deal with things such as the raw memory
/// buffers of <see cref="Process"/>.
///
/// Note, however, that if you do want to define a processor that can process more than one type of
/// value, you can derive directly from this class.
/// </remarks>
/// <seealso cref="InputBinding.processors"/>
/// <seealso cref="InputControlLayout.ControlItem.processors"/>
/// <seealso cref="InputSystem.RegisterProcessor{T}"/>
/// <seealso cref="InputActionRebindingExtensions.GetParameterValue(InputAction,string,InputBinding)"/>
/// <seealso cref="InputActionRebindingExtensions.ApplyParameterOverride(InputActionMap,string,PrimitiveValue,InputBinding)"/>
public abstract class InputProcessor
{
/// <summary>
/// Process an input value, given as an object, and return the processed value as an object.
/// </summary>
/// <param name="value">A value matching the processor's value type.</param>
/// <param name="control">Optional control that the value originated from. Must have the same value type
/// that the processor has.</param>
/// <returns>A processed value based on <paramref name="value"/>.</returns>
/// <remarks>
/// This method allocates GC heap memory. To process values without allocating GC memory, it is necessary to either know
/// the value type of a processor at compile time and call <see cref="InputProcessor{TValue}.Process(TValue,UnityEngine.InputSystem.InputControl)"/>
/// directly or to use <see cref="Process"/> instead and process values in raw memory buffers.
/// </remarks>
public abstract object ProcessAsObject(object value, InputControl control);
/// <summary>
/// Process an input value stored in the given memory buffer.
/// </summary>
/// <param name="buffer">Memory buffer containing the input value. Must be at least large enough
/// to hold one full value as indicated by <paramref name="bufferSize"/>.</param>
/// <param name="bufferSize">Size (in bytes) of the value inside <paramref name="buffer"/>.</param>
/// <param name="control">Optional control that the value originated from. Must have the same value type
/// that the processor has.</param>
/// <remarks>
/// This method allows processing values of arbitrary size without allocating memory on the GC heap.
/// </remarks>
public abstract unsafe void Process(void* buffer, int bufferSize, InputControl control);
internal static TypeTable s_Processors;
/// <summary>
/// Get the value type of a processor without having to instantiate it and use <see cref="valueType"/>.
/// </summary>
/// <param name="processorType"></param>
/// <returns>Value type of the given processor or null if it could not be determined statically.</returns>
/// <exception cref="ArgumentNullException"><paramref name="processorType"/> is null.</exception>
/// <remarks>
/// This method is reliant on the processor being based on <see cref="InputProcessor{TValue}"/>. It will return
/// the <c>TValue</c> argument used with the class. If the processor is not based on <see cref="InputProcessor{TValue}"/>,
/// this method returns null.
/// </remarks>
internal static Type GetValueTypeFromType(Type processorType)
{
if (processorType == null)
throw new ArgumentNullException(nameof(processorType));
return TypeHelpers.GetGenericTypeArgumentFromHierarchy(processorType, typeof(InputProcessor<>), 0);
}
/// <summary>
/// Caching policy regarding usage of return value from processors.
/// </summary>
public enum CachingPolicy
{
/// <summary>
/// Cache result value if unprocessed value has not been changed.
/// </summary>
CacheResult = 0,
/// <summary>
/// Process value every call to <see cref="InputControl{TValue}.ReadValue()"/> even if unprocessed value has not been changed.
/// </summary>
EvaluateOnEveryRead = 1
}
/// <summary>
/// Caching policy of the processor. Override this property to provide a different value.
/// </summary>
public virtual CachingPolicy cachingPolicy => CachingPolicy.CacheResult;
}
/// <summary>
/// A processor that conditions/transforms input values.
/// </summary>
/// <typeparam name="TValue">Type of value to be processed. Only InputControls that use the
/// same value type will be compatible with the processor.</typeparam>
/// <remarks>
/// Each <see cref="InputControl"/> can have a stack of processors assigned to it.
///
/// Note that processors CANNOT be stateful. If you need processing that requires keeping
/// mutating state over time, use InputActions. All mutable state needs to be
/// kept in the central state buffers.
///
/// However, processors can have configurable parameters. Every public field on a processor
/// object can be set using "parameters" in JSON or by supplying parameters through the
/// <see cref="InputControlAttribute.processors"/> field.
///
/// <example>
/// <code>
/// public class ScalingProcessor : InputProcessor&lt;float&gt;
/// {
/// // This field can be set as a parameter. See examples below.
/// // If not explicitly configured, will have its default value.
/// public float factor = 2.0f;
///
/// public float Process(float value, InputControl control)
/// {
/// return value * factor;
/// }
/// }
///
/// // Use processor in JSON:
/// const string json = @"
/// {
/// ""name"" : ""MyDevice"",
/// ""controls"" : [
/// { ""name"" : ""axis"", ""layout"" : ""Axis"", ""processors"" : ""scale(factor=4)"" }
/// ]
/// }
/// ";
///
/// // Use processor on C# state struct:
/// public struct MyDeviceState : IInputStateTypeInfo
/// {
/// [InputControl(layout = "Axis", processors = "scale(factor=4)"]
/// public float axis;
/// }
/// </code>
/// </example>
///
/// See <see cref="Editor.InputParameterEditor{T}"/> for how to define custom parameter
/// editing UIs for processors.
/// </remarks>
/// <seealso cref="InputSystem.RegisterProcessor"/>
public abstract class InputProcessor<TValue> : InputProcessor
where TValue : struct
{
/// <summary>
/// Process the given value and return the result.
/// </summary>
/// <remarks>
/// The implementation of this method must not be stateful.
/// </remarks>
/// <param name="value">Input value to process.</param>
/// <param name="control">Control that the value originally came from. This can be null if the value did
/// not originate from a control. This can be the case, for example, if the processor sits on a composite
/// binding (<see cref="InputBindingComposite"/>) as composites are not directly associated with controls
/// but rather source their values through their child bindings.</param>
/// <returns>Processed input value.</returns>
public abstract TValue Process(TValue value, InputControl control);
public override object ProcessAsObject(object value, InputControl control)
{
if (value == null)
throw new ArgumentNullException(nameof(value));
if (!(value is TValue))
throw new ArgumentException(
$"Expecting value of type '{typeof(TValue).Name}' but got value '{value}' of type '{value.GetType().Name}'",
nameof(value));
var valueOfType = (TValue)value;
return Process(valueOfType, control);
}
public override unsafe void Process(void* buffer, int bufferSize, InputControl control)
{
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
var valueSize = UnsafeUtility.SizeOf<TValue>();
if (bufferSize < valueSize)
throw new ArgumentException(
$"Expected buffer of at least {valueSize} bytes but got buffer with just {bufferSize} bytes",
nameof(bufferSize));
var value = default(TValue);
var valuePtr = UnsafeUtility.AddressOf(ref value);
UnsafeUtility.MemCpy(valuePtr, buffer, valueSize);
value = Process(value, control);
valuePtr = UnsafeUtility.AddressOf(ref value);
UnsafeUtility.MemCpy(buffer, valuePtr, valueSize);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c79fd316c31a2402692c908865404608
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,56 @@
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
////TODO: this or the layout system needs to detect when the format isn't supported by the control
namespace UnityEngine.InputSystem.Controls
{
/// <summary>
/// A generic input control reading integer values.
/// </summary>
public class IntegerControl : InputControl<int>
{
/// <summary>
/// Default-initialize an integer control.
/// </summary>
public IntegerControl()
{
m_StateBlock.format = InputStateBlock.FormatInt;
}
/// <inheritdoc/>
public override unsafe int ReadUnprocessedValueFromState(void* statePtr)
{
switch (m_OptimizedControlDataType)
{
case InputStateBlock.kFormatInt:
return *(int*)((byte*)statePtr + (int)m_StateBlock.byteOffset);
default:
return m_StateBlock.ReadInt(statePtr);
}
}
/// <inheritdoc/>
public override unsafe void WriteValueIntoState(int value, void* statePtr)
{
switch (m_OptimizedControlDataType)
{
case InputStateBlock.kFormatInt:
*(int*)((byte*)statePtr + (int)m_StateBlock.byteOffset) = value;
break;
default:
m_StateBlock.WriteInt(statePtr, value);
break;
}
}
protected override FourCC CalculateOptimizedControlDataType()
{
if (m_StateBlock.format == InputStateBlock.FormatInt &&
m_StateBlock.sizeInBits == 32 &&
m_StateBlock.bitOffset == 0)
return InputStateBlock.FormatInt;
return InputStateBlock.FormatInvalid;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7009ea2cce278c14d852537c8f40c8a0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,100 @@
using System.Globalization;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.Scripting;
namespace UnityEngine.InputSystem.Controls
{
/// <summary>
/// A key on a <see cref="Keyboard"/>.
/// </summary>
/// <remarks>
/// This is an extended button control which adds various features to account for the fact that keys
/// have symbols associated with them which may change depending on keyboard layout as well as in combination
/// with other keys.
///
/// Note:
/// Unity input system key codes and input manager key codes are designed with game controls in mind.
///
/// This means the way they are assigned is intended to preserve the location of keys on keyboards,
/// so that pressing a key in the same location on different keyboards should result in the same action
/// regardless of what is printed on a key or what current system language is set.
///
/// This means, for example, that <see cref="Key.A"/> is always the key to the right of <see cref="Key.CapsLock"/>,
/// regardless of which key (if any) produces the "a" character on the current keyboard layout.
///
/// Unity relies on physical hardware in the keyboards to report same USB HID "usage" for the keys in
/// the same location.This puts a practical limit on what can be achieved, because different keyboards
/// might report different data, and this is outside of Unity's control.
///
/// For this reason, you should not use key codes to read text input.
/// Instead, you should use the <see cref="Keyboard.onTextInput"/> callback.
/// The `onTextInput` callback provides you with the actual text characters which correspond
/// to the symbols printed on a keyboard, based on the end user's current system language layout.
///
/// To find the text character (if any) generated by a key according to the currently active keyboard
/// layout, use the <see cref="InputControl.displayName"/> property of <see cref="KeyControl"/>.
/// </remarks>
public class KeyControl : ButtonControl
{
/// <summary>
/// The code used in Unity to identify the key.
/// </summary>
/// <remarks>
/// This property must be initialized by <see cref="InputControl.FinishSetup"/> of
/// the device owning the control.
/// You should not use `keyCode` to read text input. For more information, <see cref="KeyControl"/>
/// </remarks>
public Key keyCode { get; set; }
////REVIEW: rename this to something like platformKeyCode? We're not really dealing with scan code here.
/// <summary>
/// The code that the underlying platform uses to identify the key.
/// </summary>
public int scanCode
{
get
{
RefreshConfigurationIfNeeded();
return m_ScanCode;
}
}
/// <inheritdoc/>
protected override void RefreshConfiguration()
{
// Wipe our last cached set of data (if any).
displayName = null;
m_ScanCode = 0;
var command = QueryKeyNameCommand.Create(keyCode);
if (device.ExecuteCommand(ref command) > 0)
{
m_ScanCode = command.scanOrKeyCode;
var rawKeyName = command.ReadKeyName();
if (string.IsNullOrEmpty(rawKeyName))
{
displayName = rawKeyName;
return;
}
// We need to lower case first because ToTitleCase preserves upper casing.
// For example on Swedish Windows layout right shift display name is "HÖGER SKIFT".
// Just passing it to ToTitleCase won't change anything. But passing "höger skift" will return "Höger Skift".
var keyNameLowerCase = rawKeyName.ToLowerInvariant();
if (string.IsNullOrEmpty(keyNameLowerCase))
{
displayName = rawKeyName;
return;
}
var textInfo = CultureInfo.InvariantCulture.TextInfo;
displayName = textInfo.ToTitleCase(keyNameLowerCase);
}
}
// Cached configuration data for the key. We fetch this from the
// device on demand.
private int m_ScanCode;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7d7a42e3bb8b4c0f877cbe19718a4103
timeCreated: 1510949927

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 6fde91b0a4ee45cd86be18a8d829dc87
folderAsset: yes
timeCreated: 1506735469
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,116 @@
using System;
using UnityEngine.Scripting;
#if UNITY_EDITOR
using UnityEngine.InputSystem.Editor;
using UnityEngine.UIElements;
#endif
namespace UnityEngine.InputSystem.Processors
{
/// <summary>
/// Clamps values to the range given by <see cref="min"/> and <see cref="max"/> and re-normalizes the resulting
/// value to [0..1].
/// </summary>
/// <remarks>
/// This processor is registered (see <see cref="InputSystem.RegisterProcessor{T}"/>) under the name "AxisDeadzone".
///
/// It acts like a combination of <see cref="ClampProcessor"/> and <see cref="NormalizeProcessor"/>.
///
/// <example>
/// <code>
/// </code>
/// // Bind to right trigger on gamepad such that the value is clamped and normalized between
/// // 0.3 and 0.7.
/// new InputAction(binding: "&lt;Gamepad&gt;/rightTrigger", processors: "axisDeadzone(min=0.3,max=0.7)");
/// </example>
/// </remarks>
/// <seealso cref="StickDeadzoneProcessor"/>
public class AxisDeadzoneProcessor : InputProcessor<float>
{
/// <summary>
/// Lower bound (inclusive) below which input values get clamped. Corresponds to 0 in the normalized range.
/// </summary>
/// <remarks>
/// If this is equal to 0 (the default), <see cref="InputSettings.defaultDeadzoneMin"/> is used instead.
/// </remarks>
public float min;
/// <summary>
/// Upper bound (inclusive) beyond which input values get clamped. Corresponds to 1 in the normalized range.
/// </summary>
/// <remarks>
/// If this is equal to 0 (the default), <see cref="InputSettings.defaultDeadzoneMax"/> is used instead.
/// </remarks>
public float max;
private float minOrDefault => min == default ? InputSystem.settings.defaultDeadzoneMin : min;
private float maxOrDefault => max == default ? InputSystem.settings.defaultDeadzoneMax : max;
/// <summary>
/// Normalize <paramref name="value"/> according to <see cref="min"/> and <see cref="max"/>.
/// </summary>
/// <param name="value">Input value.</param>
/// <param name="control">Ignored.</param>
/// <returns>Normalized value.</returns>
public override float Process(float value, InputControl control = null)
{
var min = minOrDefault;
var max = maxOrDefault;
var absValue = Mathf.Abs(value);
if (absValue < min)
return 0;
if (absValue > max)
return Mathf.Sign(value);
return Mathf.Sign(value) * ((absValue - min) / (max - min));
}
/// <inheritdoc/>
public override string ToString()
{
return $"AxisDeadzone(min={minOrDefault},max={maxOrDefault})";
}
}
#if UNITY_EDITOR
internal class AxisDeadzoneProcessorEditor : InputParameterEditor<AxisDeadzoneProcessor>
{
protected override void OnEnable()
{
m_MinSetting.Initialize("Min",
"Value below which input values will be clamped. After clamping, values will be renormalized to [0..1] between min and max.",
"Default Deadzone Min",
() => target.min, v => target.min = v,
() => InputSystem.settings.defaultDeadzoneMin);
m_MaxSetting.Initialize("Max",
"Value above which input values will be clamped. After clamping, values will be renormalized to [0..1] between min and max.",
"Default Deadzone Max",
() => target.max, v => target.max = v,
() => InputSystem.settings.defaultDeadzoneMax);
}
public override void OnGUI()
{
#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
if (!InputSystem.settings.useIMGUIEditorForAssets) return;
#endif
m_MinSetting.OnGUI();
m_MaxSetting.OnGUI();
}
#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
public override void OnDrawVisualElements(VisualElement root, Action onChangedCallback)
{
m_MinSetting.OnDrawVisualElements(root, onChangedCallback);
m_MaxSetting.OnDrawVisualElements(root, onChangedCallback);
}
#endif
private CustomOrDefaultSetting m_MinSetting;
private CustomOrDefaultSetting m_MaxSetting;
}
#endif
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 110e42b335c6744e482ada733cbda674
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,55 @@
using UnityEngine.Scripting;
////TODO: move clamping settings into struct and add process function; then embed both here and in AxisControl
namespace UnityEngine.InputSystem.Processors
{
/// <summary>
/// Clamp a floating-point input to between <see cref="min"/> and <see cref="max"/>. This is equivalent
/// to <c>Mathf.Clamp(value, min, max)</c>.
/// </summary>
/// <remarks>
/// This processor is registered (see <see cref="InputSystem.RegisterProcessor{T}"/>) under the name "clamp" by default.
///
/// Note that no normalization is performed. If you want to re-normalize the input value after clamping,
/// add a <see cref="NormalizeProcessor"/>. Alternatively, add a <see cref="AxisDeadzoneProcessor"/> which
/// both clamps and normalizes.
///
/// <example>
/// <code>
/// </code>
/// // Bind to right trigger on gamepad such that the value never drops below 0.3 and never goes
/// // above 0.7.
/// new InputAction(binding: "&lt;Gamepad&gt;/rightTrigger", processors: "clamp(min=0.3,max=0.7)");
/// </example>
/// </remarks>
public class ClampProcessor : InputProcessor<float>
{
/// <summary>
/// Minimum value (inclusive!) of the accepted value range.
/// </summary>
public float min;
/// <summary>
/// Maximum value (inclusive!) of the accepted value range.
/// </summary>
public float max;
/// <summary>
/// Clamp <paramref name="value"/> to the range of <see cref="min"/> and <see cref="max"/>.
/// </summary>
/// <param name="value">Input value.</param>
/// <param name="control">Ignored.</param>
/// <returns>Clamped value.</returns>
public override float Process(float value, InputControl control)
{
return Mathf.Clamp(value, min, max);
}
/// <inheritdoc/>
public override string ToString()
{
return $"Clamp(min={min},max={max})";
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6887ac562f464a329bc468158dbbf18f
timeCreated: 1506740041

View File

@@ -0,0 +1,31 @@
using System.ComponentModel;
using UnityEngine.InputSystem.LowLevel;
namespace UnityEngine.InputSystem.Processors
{
[DesignTimeVisible(false)]
internal class CompensateDirectionProcessor : InputProcessor<Vector3>
{
public override Vector3 Process(Vector3 value, InputControl control)
{
if (!InputSystem.settings.compensateForScreenOrientation)
return value;
var rotation = Quaternion.identity;
switch (InputRuntime.s_Instance.screenOrientation)
{
case ScreenOrientation.PortraitUpsideDown: rotation = Quaternion.Euler(0, 0, 180); break;
case ScreenOrientation.LandscapeLeft: rotation = Quaternion.Euler(0, 0, 90); break;
case ScreenOrientation.LandscapeRight: rotation = Quaternion.Euler(0, 0, 270); break;
}
return rotation * value;
}
public override string ToString()
{
return "CompensateDirection()";
}
public override CachingPolicy cachingPolicy => CachingPolicy.EvaluateOnEveryRead;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dbce6ac3ce95e4e28af8b2212171619a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,34 @@
using System.ComponentModel;
using UnityEngine.InputSystem.LowLevel;
namespace UnityEngine.InputSystem.Processors
{
[DesignTimeVisible(false)]
internal class CompensateRotationProcessor : InputProcessor<Quaternion>
{
public override Quaternion Process(Quaternion value, InputControl control)
{
if (!InputSystem.settings.compensateForScreenOrientation)
return value;
const float kSqrtOfTwo = 1.4142135623731f;
var q = Quaternion.identity;
switch (InputRuntime.s_Instance.screenOrientation)
{
case ScreenOrientation.PortraitUpsideDown: q = new Quaternion(0.0f, 0.0f, 1.0f /*sin(pi/2)*/, 0.0f /*cos(pi/2)*/); break;
case ScreenOrientation.LandscapeLeft: q = new Quaternion(0.0f, 0.0f, kSqrtOfTwo * 0.5f /*sin(pi/4)*/, -kSqrtOfTwo * 0.5f /*cos(pi/4)*/); break;
case ScreenOrientation.LandscapeRight: q = new Quaternion(0.0f, 0.0f, -kSqrtOfTwo * 0.5f /*sin(3pi/4)*/, -kSqrtOfTwo * 0.5f /*cos(3pi/4)*/); break;
}
return value * q;
}
public override string ToString()
{
return "CompensateRotation()";
}
public override CachingPolicy cachingPolicy => CachingPolicy.EvaluateOnEveryRead;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6ef7b744d03d44f2a82c38ac2f25b21f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,56 @@
#if UNITY_EDITOR || PACKAGE_DOCS_GENERATION
using System.ComponentModel;
using UnityEngine.InputSystem.LowLevel;
using UnityEditor;
namespace UnityEngine.InputSystem.Processors
{
/// <summary>
/// If Unity is currently in an <see cref="EditorWindow"/> callback, transforms a 2D coordinate from
/// player window space into window space of the current EditorWindow.
/// </summary>
/// <remarks>
/// This processor is only available in the editor. Also, it only works on devices that
/// support the <see cref="QueryEditorWindowCoordinatesCommand"/> request.
///
/// Outside of <see cref="EditorWindow"/> callbacks, this processor does nothing and just passes through
/// the coordinates it receives.
/// </remarks>
/// <seealso cref="Pointer.position"/>
[DesignTimeVisible(false)]
public class EditorWindowSpaceProcessor : InputProcessor<Vector2>
{
/// <summary>
/// Transform the given player screen-space coordinate into the coordinate space of the current
/// <c>EditorWindow</c>.
/// </summary>
/// <param name="value">GameView screen space coordinate.</param>
/// <param name="control">Ignored.</param>
/// <returns>The given coordinate transformed into <c>EditorWindow</c> space.</returns>
/// <remarks>
/// This method will only succeed if the editor is currently in an <c>EditorWindow</c> callback such
/// as <c>OnGUI</c>.
/// </remarks>
public override Vector2 Process(Vector2 value, InputControl control)
{
// We go and fire trigger QueryEditorWindowCoordinatesCommand regardless
// of whether we are currently in EditorWindow code or not. The expectation
// here is that the underlying editor code is in a better position than us
// to judge whether the conversion should be performed or not. In native code,
// the IOCTL implementations will early out if they detect that the current
// EditorWindow is in fact a game view.
if (Mouse.s_PlatformMouseDevice != null)
{
var command = QueryEditorWindowCoordinatesCommand.Create(value);
// Not all pointer devices implement the editor window position IOCTL,
// so we try the global mouse device if available.
if (Mouse.s_PlatformMouseDevice.ExecuteCommand(ref command) > 0)
return command.inOutCoordinates;
}
return value;
}
}
}
#endif // UNITY_EDITOR

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 87ce818774f4c544882b249f32619300
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,37 @@
using UnityEngine.Scripting;
namespace UnityEngine.InputSystem.Processors
{
/// <summary>
/// An input processor that inverts its input value.
/// </summary>
/// <remarks>
/// This process is registered (see <see cref="InputSystem.RegisterProcessor{T}"/> as "invert" by default.
///
/// <example>
/// <code>
/// // Bind to the gamepad's left trigger such that it returns inverted values.
/// new InputAction(binding: "&lt;Gamepad&gt;/leftTrigger", processors="invert");
/// </code>
/// </example>
/// </remarks>
public class InvertProcessor : InputProcessor<float>
{
/// <summary>
/// Return the inverted value of <paramref name="value"/>.
/// </summary>
/// <param name="value">Input value.</param>
/// <param name="control">Ignored.</param>
/// <returns>Invert value.</returns>
public override float Process(float value, InputControl control)
{
return value * -1.0f;
}
/// <inheritdoc/>
public override string ToString()
{
return "Invert()";
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 84e410cdded84dd3971c9dbe94605503
timeCreated: 1506740035

View File

@@ -0,0 +1,52 @@
using UnityEngine.Scripting;
namespace UnityEngine.InputSystem.Processors
{
/// <summary>
/// Inverts the <c>x</c> and/or <c>y</c> channel of a <c>Vector2</c>.
/// </summary>
/// <remarks>
/// This process is registered (see <see cref="InputSystem.RegisterProcessor{T}"/> as "invertVector2" by default.
///
/// <example>
/// <code>
/// // Bind to the left stick on the gamepad such that its Y channel is inverted.
/// new InputAction(binding: "&lt;Gamepad&gt;/leftStick", processors="invertVector2(invertY,invertX=false)");
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InvertVector3Processor"/>
public class InvertVector2Processor : InputProcessor<Vector2>
{
/// <summary>
/// If true, the <c>x</c> channel of the <c>Vector2</c> input value is inverted. True by default.
/// </summary>
public bool invertX = true;
/// <summary>
/// If true, the <c>y</c> channel of the <c>Vector2</c> input value is inverted. True by default.
/// </summary>
public bool invertY = true;
/// <summary>
/// Invert the <c>x</c> and/or <c>y</c> channel of the given <paramref name="value"/>.
/// </summary>
/// <param name="value">Input value.</param>
/// <param name="control">Ignored.</param>
/// <returns>Vector2 with inverted channels.</returns>
public override Vector2 Process(Vector2 value, InputControl control)
{
if (invertX)
value.x *= -1;
if (invertY)
value.y *= -1;
return value;
}
/// <inheritdoc/>
public override string ToString()
{
return $"InvertVector2(invertX={invertX},invertY={invertY})";
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4ab0ce1980d384014be67a2d135ed92c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,59 @@
using UnityEngine.Scripting;
namespace UnityEngine.InputSystem.Processors
{
/// <summary>
/// Inverts the <c>x</c> and/or <c>y</c> and/or <c>z</c> channel of a <c>Vector3</c>.
/// </summary>
/// <remarks>
/// This process is registered (see <see cref="InputSystem.RegisterProcessor{T}"/> as "invertVector3" by default.
///
/// <example>
/// <code>
/// // Bind to gravity sensor such that its Y value is inverted.
/// new InputAction(binding: "&lt;GravitySensor&gt;/gravity", processors="invertVector3(invertX=false,invertY,invertZ=false)");
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InvertVector2Processor"/>
public class InvertVector3Processor : InputProcessor<Vector3>
{
/// <summary>
/// If true, the <c>x</c> channel of the <c>Vector3</c> input value is inverted. True by default.
/// </summary>
public bool invertX = true;
/// <summary>
/// If true, the <c>y</c> channel of the <c>Vector3</c> input value is inverted. True by default.
/// </summary>
public bool invertY = true;
/// <summary>
/// If true, the <c>z</c> channel of the <c>Vector3</c> input value is inverted. True by default.
/// </summary>
public bool invertZ = true;
/// <summary>
/// Return the given vector with the respective channels being inverted.
/// </summary>
/// <param name="value">Input value.</param>
/// <param name="control">Ignored.</param>
/// <returns>Vector with channels inverted according to <see cref="invertX"/>, <see cref="invertY"/>, and <see cref="invertZ"/>.</returns>
public override Vector3 Process(Vector3 value, InputControl control)
{
if (invertX)
value.x *= -1;
if (invertY)
value.y *= -1;
if (invertZ)
value.z *= -1;
return value;
}
/// <inheritdoc/>
public override string ToString()
{
return $"InvertVector3(invertX={invertX},invertY={invertY},invertZ={invertZ})";
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7938861606c2845d19cf2e8a683ddba3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,121 @@
using UnityEngine.Scripting;
////REVIEW: handle values dropping below min and above max?
namespace UnityEngine.InputSystem.Processors
{
/// <summary>
/// Normalizes input values in the range <see cref="min"/> and <see cref="max"/> to
/// unsigned normalized form [0..1] if <see cref="zero"/> is placed at (or below) <see cref="min"/>
/// or to signed normalized form [-1..1] if <see cref="zero"/> is placed in-between
/// <see cref="min"/> and <see cref="max"/>.
/// </summary>
/// <remarks>
/// This processor is registered (see <see cref="InputSystem.RegisterProcessor{T}"/>) under the name "normalize".
///
/// Note that this processor does not clamp the incoming value to <see cref="min"/> and <see cref="max"/>.
/// To achieve this, either add a <see cref="ClampProcessor"/> or use <see cref="AxisDeadzoneProcessor"/>
/// which combines clamping and normalizing.
///
/// <example>
/// <code>
/// </code>
/// // Bind to right trigger on gamepad such that the value values below 0.3 and above 0.7 get normalized
/// // to values between [0..1].
/// new InputAction(binding: "&lt;Gamepad&gt;/rightTrigger", processors: "normalize(min=0.3,max=0.7)");
/// </example>
/// </remarks>
/// <seealso cref="NormalizeVector2Processor"/>
/// <seealso cref="NormalizeVector3Processor"/>
public class NormalizeProcessor : InputProcessor<float>
{
/// <summary>
/// Input value (inclusive) that corresponds to 0 or -1 (depending on <see cref="zero"/>), the lower bound.
/// </summary>
/// <remarks>
/// If the input value drops below min, the result is undefined.
/// </remarks>
public float min;
/// <summary>
/// Input value (inclusive) that corresponds to 1, the upper bound.
/// </summary>
/// <remarks>
/// If the input value goes beyond max, the result is undefined.
/// </remarks>
public float max;
/// <summary>
/// Input value that corresponds to 0. If this is placed at or below <see cref="min"/>, the resulting normalization
/// returns a [0..1] value. If this is placed in-between <see cref="min"/> and <see cref="max"/>, the resulting
/// normalization returns a [-1..1] value.
/// </summary>
public float zero;
/// <summary>
/// Normalize <paramref name="value"/> with respect to <see cref="min"/> and <see cref="max"/>.
/// </summary>
/// <param name="value">Input value.</param>
/// <param name="control">Ignored.</param>
/// <returns>Normalized value.</returns>
public override float Process(float value, InputControl control)
{
return Normalize(value, min, max, zero);
}
/// <summary>
/// Normalize <paramref name="value"/> with respect to <paramref name="min"/> and <paramref name="max"/>.
/// </summary>
/// <param name="value">Input value.</param>
/// <param name="min">Lower bound. See <see cref="min"/>.</param>
/// <param name="max">Upper bound. See <see cref="max"/>.</param>
/// <param name="zero">Zero point. See <see cref="zero"/>.</param>
/// <returns>Normalized value.</returns>
/// <remarks>
/// This method performs the same function as <see cref="Process"/>.
/// <example>
/// <code>
/// // Normalize 2 against a [1..5] range. Returns 0.25.
/// NormalizeProcessor.Normalize(2, 1, 5, 1)
/// </code>
/// </example>
/// </remarks>
public static float Normalize(float value, float min, float max, float zero)
{
if (zero < min)
zero = min;
// Prevent NaN/Inf from dividing 0 by something.
if (Mathf.Approximately(value, min))
{
if (min < zero)
return -1f;
return 0f;
}
var percentage = (value - min) / (max - min);
if (min < zero)
return 2 * percentage - 1;
return percentage;
}
internal static float Denormalize(float value, float min, float max, float zero)
{
if (zero < min)
zero = min;
if (min < zero)
{
if (value < 0)
return min + (zero - min) * (value * -1f);
return zero + (max - zero) * value;
}
return min + (max - min) * value;
}
/// <inheritdoc/>
public override string ToString()
{
return $"Normalize(min={min},max={max},zero={zero})";
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 94995588a5664878be30498a8ace6ba2
timeCreated: 1506740059

View File

@@ -0,0 +1,31 @@
using UnityEngine.Scripting;
namespace UnityEngine.InputSystem.Processors
{
/// <summary>
/// Normalizes a <c>Vector2</c> input value.
/// </summary>
/// <remarks>
/// This processor is registered (see <see cref="InputSystem.RegisterProcessor{T}"/>) under the name "normalizeVector2".
/// </remarks>
/// <seealso cref="NormalizeVector3Processor"/>
public class NormalizeVector2Processor : InputProcessor<Vector2>
{
/// <summary>
/// Normalize <paramref name="value"/>. Performs the equivalent of <c>value.normalized</c>.
/// </summary>
/// <param name="value">Input vector.</param>
/// <param name="control">Ignored.</param>
/// <returns>Normalized vector.</returns>
public override Vector2 Process(Vector2 value, InputControl control)
{
return value.normalized;
}
/// <inheritdoc/>
public override string ToString()
{
return "NormalizeVector2()";
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6ae082cf4f6634e9d8cdd8526dd816a3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,31 @@
using UnityEngine.Scripting;
namespace UnityEngine.InputSystem.Processors
{
/// <summary>
/// Normalizes a <c>Vector3</c> input value.
/// </summary>
/// <remarks>
/// This processor is registered (see <see cref="InputSystem.RegisterProcessor{T}"/>) under the name "normalizeVector3".
/// </remarks>
/// <seealso cref="NormalizeVector2Processor"/>
public class NormalizeVector3Processor : InputProcessor<Vector3>
{
/// <summary>
/// Normalize <paramref name="value"/>. Performs the equivalent of <c>value.normalized</c>.
/// </summary>
/// <param name="value">Input vector.</param>
/// <param name="control">Ignored.</param>
/// <returns>Normalized vector.</returns>
public override Vector3 Process(Vector3 value, InputControl control)
{
return value.normalized;
}
/// <inheritdoc/>
public override string ToString()
{
return "NormalizeVector3()";
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8b78cdd0ba4454fd69b2be04e825d921
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,45 @@
using UnityEngine.Scripting;
namespace UnityEngine.InputSystem.Processors
{
/// <summary>
/// Scale a float value by a constant factor.
/// </summary>
/// <remarks>
/// This processor is registered (see <see cref="InputSystem.RegisterProcessor{T}"/>) under the name "scale".
///
/// <example>
/// <code>
/// </code>
/// // Bind to left trigger on the gamepad such that its values are scaled by a factor of 2.
/// new InputAction(binding: "&lt;Gamepad&gt;/leftTrigger", processors: "scale(factor=2)");
/// </example>
/// </remarks>
/// <seealso cref="ScaleVector2Processor"/>
/// <seealso cref="ScaleVector3Processor"/>
public class ScaleProcessor : InputProcessor<float>
{
/// <summary>
/// Scale factor to apply to incoming input values. Defaults to 1 (no scaling).
/// </summary>
[Tooltip("Scale factor to multiply incoming float values by.")]
public float factor = 1;
/// <summary>
/// Scale the given <paramref name="value"/> by <see cref="factor"/>.
/// </summary>
/// <param name="value">Input value.</param>
/// <param name="control">Ignored.</param>
/// <returns>Scaled value.</returns>
public override float Process(float value, InputControl control)
{
return value * factor;
}
/// <inheritdoc/>
public override string ToString()
{
return $"Scale(factor={factor})";
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 98590516d066443e58df0deff4a95b21
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,49 @@
namespace UnityEngine.InputSystem.Processors
{
/// <summary>
/// Scale the components of a <see cref="Vector2"/> by constant factors.
/// </summary>
/// <remarks>
/// This processor is registered (see <see cref="InputSystem.RegisterProcessor{T}"/>) under the name "scaleVector2".
///
/// <example>
/// <code>
/// // Double the length of the vector produced by leftStick on gamepad.
/// myAction.AddBinding("&lt;Gamepad&gt;/leftStick").WithProcessor("scaleVector2(x=2,y=2)");
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="ScaleProcessor"/>
/// <seealso cref="ScaleVector3Processor"/>
public class ScaleVector2Processor : InputProcessor<Vector2>
{
/// <summary>
/// Scale factor to apply to the vector's <c>x</c> axis. Defaults to 1.
/// </summary>
[Tooltip("Scale factor to multiply the incoming Vector2's X component by.")]
public float x = 1;
/// <summary>
/// Scale factor to apply to the vector's <c>y</c> axis. Defaults to 1.
/// </summary>
[Tooltip("Scale factor to multiply the incoming Vector2's Y component by.")]
public float y = 1;
/// <summary>
/// Return <paramref name="value"/> scaled by <see cref="x"/> and <see cref="y"/>.
/// </summary>
/// <param name="value">Input value.</param>
/// <param name="control">Ignored.</param>
/// <returns>Scaled vector.</returns>
public override Vector2 Process(Vector2 value, InputControl control)
{
return new Vector2(value.x * x, value.y * y);
}
/// <inheritdoc/>
public override string ToString()
{
return $"ScaleVector2(x={x},y={y})";
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 96bf34fe183cd4ce986db61a6639cd5b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,57 @@
using UnityEngine.Scripting;
namespace UnityEngine.InputSystem.Processors
{
/// <summary>
/// Scale the components of a <see cref="Vector3"/> by constant factors.
/// </summary>
/// <remarks>
/// This processor is registered (see <see cref="InputSystem.RegisterProcessor{T}"/>) under the name "scaleVector3".
///
/// <example>
/// <code>
/// // Double the magnitude of gravity values read from a gravity sensor.
/// myAction.AddBinding("&lt;GravitySensor&gt;/gravity").WithProcessor("scaleVector3(x=2,y=2,z=2)");
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="ScaleProcessor"/>
/// <seealso cref="ScaleVector2Processor"/>
public class ScaleVector3Processor : InputProcessor<Vector3>
{
/// <summary>
/// Scale factor to apply to the vector's <c>x</c> axis. Defaults to 1.
/// </summary>
[Tooltip("Scale factor to multiply the incoming Vector3's X component by.")]
public float x = 1;
/// <summary>
/// Scale factor to apply to the vector's <c>y</c> axis. Defaults to 1.
/// </summary>
[Tooltip("Scale factor to multiply the incoming Vector3's Y component by.")]
public float y = 1;
/// <summary>
/// Scale factor to apply to the vector's <c>z</c> axis. Defaults to 1.
/// </summary>
[Tooltip("Scale factor to multiply the incoming Vector3's Z component by.")]
public float z = 1;
/// <summary>
/// Return <paramref name="value"/> scaled by <see cref="x"/>, <see cref="y"/>, and <see cref="z"/>.
/// </summary>
/// <param name="value">Input value.</param>
/// <param name="control">Ignored.</param>
/// <returns>Scaled vector.</returns>
public override Vector3 Process(Vector3 value, InputControl control)
{
return new Vector3(value.x * x, value.y * y, value.z * z);
}
/// <inheritdoc/>
public override string ToString()
{
return $"ScaleVector3(x={x},y={y},z={z})";
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 681942113a3d6460a90af134a86edb78
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,105 @@
using System;
using UnityEngine.Scripting;
#if UNITY_EDITOR
using UnityEngine.InputSystem.Editor;
using UnityEngine.UIElements;
#endif
////REVIEW: rename to RadialDeadzone
////TODO: add different deadzone shapes and/or option to min/max X and Y separately
namespace UnityEngine.InputSystem.Processors
{
/// <summary>
/// Processes a Vector2 to apply deadzoning according to the magnitude of the vector (rather
/// than just clamping individual axes). Normalizes to the min/max range.
/// </summary>
/// <seealso cref="AxisDeadzoneProcessor"/>
public class StickDeadzoneProcessor : InputProcessor<Vector2>
{
/// <summary>
/// Value at which the lower bound deadzone starts.
/// </summary>
/// <remarks>
/// Values in the input at or below min will get dropped and values
/// will be scaled to the range between min and max.
/// </remarks>
public float min;
public float max;
private float minOrDefault => min == default ? InputSystem.settings.defaultDeadzoneMin : min;
private float maxOrDefault => max == default ? InputSystem.settings.defaultDeadzoneMax : max;
public override Vector2 Process(Vector2 value, InputControl control = null)
{
var magnitude = value.magnitude;
var newMagnitude = GetDeadZoneAdjustedValue(magnitude);
if (newMagnitude == 0)
value = Vector2.zero;
else
value *= newMagnitude / magnitude;
return value;
}
private float GetDeadZoneAdjustedValue(float value)
{
var min = minOrDefault;
var max = maxOrDefault;
var absValue = Mathf.Abs(value);
if (absValue < min)
return 0;
if (absValue > max)
return Mathf.Sign(value);
return Mathf.Sign(value) * ((absValue - min) / (max - min));
}
public override string ToString()
{
return $"StickDeadzone(min={minOrDefault},max={maxOrDefault})";
}
}
#if UNITY_EDITOR
internal class StickDeadzoneProcessorEditor : InputParameterEditor<StickDeadzoneProcessor>
{
protected override void OnEnable()
{
m_MinSetting.Initialize("Min",
"Vector length below which input values will be clamped. After clamping, vector lengths will be renormalized to [0..1] between min and max.",
"Default Deadzone Min",
() => target.min, v => target.min = v,
() => InputSystem.settings.defaultDeadzoneMin);
m_MaxSetting.Initialize("Max",
"Vector length above which input values will be clamped. After clamping, vector lengths will be renormalized to [0..1] between min and max.",
"Default Deadzone Max",
() => target.max, v => target.max = v,
() => InputSystem.settings.defaultDeadzoneMax);
}
public override void OnGUI()
{
#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
if (!InputSystem.settings.useIMGUIEditorForAssets) return;
#endif
m_MinSetting.OnGUI();
m_MaxSetting.OnGUI();
}
#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
public override void OnDrawVisualElements(VisualElement root, Action onChangedCallback)
{
m_MinSetting.OnDrawVisualElements(root, onChangedCallback);
m_MaxSetting.OnDrawVisualElements(root, onChangedCallback);
}
#endif
private CustomOrDefaultSetting m_MinSetting;
private CustomOrDefaultSetting m_MaxSetting;
}
#endif
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a327bafe91572467f834d0435da166fb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,123 @@
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
////REVIEW: expose euler angle subcontrols?
namespace UnityEngine.InputSystem.Controls
{
/// <summary>
/// A generic input control reading quaternion (rotation) values.
/// </summary>
public class QuaternionControl : InputControl<Quaternion>
{
// Accessing these components as individual controls usually doesn't make too much sense,
// but having these controls allows changing the state format on the quaternion without
// requiring the control to explicitly support the various different storage formats.
// Also, it allows putting processors on the individual components which may be necessary
// to properly convert the source data.
/// <summary>
/// The X component of the quaternion.
/// </summary>
/// <value>Control representing the X component.</value>
[InputControl(displayName = "X")]
public AxisControl x { get; set; }
/// <summary>
/// The Y component of the quaternion.
/// </summary>
/// <value>Control representing the Y component.</value>
[InputControl(displayName = "Y")]
public AxisControl y { get; set; }
/// <summary>
/// The Z component of the quaternion.
/// </summary>
/// <value>Control representing the Z component.</value>
[InputControl(displayName = "Z")]
public AxisControl z { get; set; }
/// <summary>
/// The W component of the quaternion.
/// </summary>
/// <value>Control representing the W component.</value>
[InputControl(displayName = "W")]
public AxisControl w { get; set; }
/// <summary>
/// Default-initialize the control.
/// </summary>
public QuaternionControl()
{
m_StateBlock.sizeInBits = sizeof(float) * 4 * 8;
m_StateBlock.format = InputStateBlock.FormatQuaternion;
}
/// <inheritdoc/>
protected override void FinishSetup()
{
x = GetChildControl<AxisControl>("x");
y = GetChildControl<AxisControl>("y");
z = GetChildControl<AxisControl>("z");
w = GetChildControl<AxisControl>("w");
base.FinishSetup();
}
/// <inheritdoc/>
public override unsafe Quaternion ReadUnprocessedValueFromState(void* statePtr)
{
switch (m_OptimizedControlDataType)
{
case InputStateBlock.kFormatQuaternion:
return *(Quaternion*)((byte*)statePtr + (int)m_StateBlock.byteOffset);
default:
return new Quaternion(
x.ReadValueFromStateWithCaching(statePtr),
y.ReadValueFromStateWithCaching(statePtr),
z.ReadValueFromStateWithCaching(statePtr),
w.ReadUnprocessedValueFromStateWithCaching(statePtr));
}
}
/// <inheritdoc/>
public override unsafe void WriteValueIntoState(Quaternion value, void* statePtr)
{
switch (m_OptimizedControlDataType)
{
case InputStateBlock.kFormatQuaternion:
*(Quaternion*)((byte*)statePtr + (int)m_StateBlock.byteOffset) = value;
break;
default:
x.WriteValueIntoState(value.x, statePtr);
y.WriteValueIntoState(value.y, statePtr);
z.WriteValueIntoState(value.z, statePtr);
w.WriteValueIntoState(value.w, statePtr);
break;
}
}
protected override FourCC CalculateOptimizedControlDataType()
{
if (
m_StateBlock.sizeInBits == sizeof(float) * 4 * 8 &&
m_StateBlock.bitOffset == 0 &&
x.optimizedControlDataType == InputStateBlock.FormatFloat &&
y.optimizedControlDataType == InputStateBlock.FormatFloat &&
z.optimizedControlDataType == InputStateBlock.FormatFloat &&
w.optimizedControlDataType == InputStateBlock.FormatFloat &&
y.m_StateBlock.byteOffset == x.m_StateBlock.byteOffset + 4 &&
z.m_StateBlock.byteOffset == x.m_StateBlock.byteOffset + 8 &&
w.m_StateBlock.byteOffset == x.m_StateBlock.byteOffset + 12 &&
// For some unknown reason ReadUnprocessedValueFromState here uses ReadValueFromState on x/y/z (but not w)
// which means we can't optimize if there any processors on x/y/z
x.m_ProcessorStack.length == 0 &&
y.m_ProcessorStack.length == 0 &&
z.m_ProcessorStack.length == 0
)
return InputStateBlock.FormatQuaternion;
return InputStateBlock.FormatInvalid;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3e9857b4032c493aa79e61db86f9e09b
timeCreated: 1506742992

View File

@@ -0,0 +1,119 @@
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Processors;
namespace UnityEngine.InputSystem.Controls
{
/// <summary>
/// A two-axis thumbstick control that can act as both a vector and a four-way dpad.
/// </summary>
/// <remarks>
/// Stick controls are used to represent the thumbsticks on gamepads (see <see cref="Gamepad.leftStick"/>
/// and <see cref="Gamepad.rightStick"/>) as well as the main stick control of joysticks (see
/// <see cref="Joystick.stick"/>).
///
/// Essentially, a stick is an extended <c>Vector2</c> control that can function either
/// as a combined 2D vector, as independent vertical and horizontal axes, or as four
/// individual, directional buttons. The following example demonstrates this based on the
/// gamepad's left stick.
///
/// <example>
/// <code>
/// // Read stick as a combined 2D vector.
/// Gamepad.current.leftStick.ReadValue();
///
/// // Read X and Y axis of stick individually.
/// Gamepad.current.leftStick.x.ReadValue();
/// Gamepad.current.leftStick.y.ReadValue();
///
/// // Read the stick as four individual directional buttons.
/// Gamepad.current.leftStick.up.ReadValue();
/// Gamepad.current.leftStick.down.ReadValue();
/// Gamepad.current.leftStick.left.ReadValue();
/// Gamepad.current.leftStick.right.ReadValue();
/// </code>
/// </example>
///
/// In terms of memory, a stick controls is still just from one value for the X axis
/// and one value for the Y axis.
///
/// Unlike dpads (see <see cref="DpadControl"/>), sticks will usually have deadzone processors
/// (see <see cref="StickDeadzoneProcessor"/>) applied to them to get rid of noise around the
/// resting point of the stick. The X and Y axis also have deadzones applied to them by
/// default (<see cref="AxisDeadzoneProcessor"/>). Note, however, that the deadzoning of
/// individual axes is different from the deadzoning applied to the stick as a whole and
/// thus does not have to result in exactly the same values. Deadzoning of individual axes
/// is linear (i.e. the result is simply clamped and normalized back into [0..1] range) whereas
/// the deadzoning of sticks is radial (i.e. the length of the vector is taken into account
/// which means that <em>both</em> the X and Y axis contribute).
/// </remarks>
public class StickControl : Vector2Control
{
////REVIEW: should X and Y have "Horizontal" and "Vertical" as long display names and "X" and "Y" as short names?
// Buttons for each of the directions. Allows the stick to function as a dpad.
// Note that these controls are marked as synthetic as there isn't real buttons for the half-axes
// on the device. This aids in interactive picking by making sure that if we have to decide between,
// say, leftStick/x and leftStick/left, leftStick/x wins out.
////REVIEW: up/down/left/right should probably prohibit being written to
////REVIEW: Should up/down/left/control actually be their own control types that *read* the values
//// from X and Y instead of sharing their state? The current setup easily leads to various
//// problems because more than just the state block is needed to read the value of a control
//// from state correctly.
/// <summary>
/// A synthetic button representing the upper half of the stick's Y axis, i.e. the 0 to 1 range.
/// </summary>
/// <value>Control representing the stick's upper half Y axis.</value>
/// <remarks>
/// The control is marked as <see cref="InputControl.synthetic"/>.
/// </remarks>
[InputControl(useStateFrom = "y", processors = "axisDeadzone", parameters = "clamp=2,clampMin=0,clampMax=1", synthetic = true, displayName = "Up")]
// Set min&max on XY axes. We do this here as the documentation generator will not be happy
// if we place this above the doc comment.
// Also puts AxisDeadzones on the axes.
[InputControl(name = "x", minValue = -1f, maxValue = 1f, layout = "Axis", processors = "axisDeadzone")]
[InputControl(name = "y", minValue = -1f, maxValue = 1f, layout = "Axis", processors = "axisDeadzone")]
public ButtonControl up { get; set; }
/// <summary>
/// A synthetic button representing the lower half of the stick's Y axis, i.e. the -1 to 0 range (inverted).
/// </summary>
/// <value>Control representing the stick's lower half Y axis.</value>
/// <remarks>
/// The control is marked as <see cref="InputControl.synthetic"/>.
/// </remarks>
[InputControl(useStateFrom = "y", processors = "axisDeadzone", parameters = "clamp=2,clampMin=-1,clampMax=0,invert", synthetic = true, displayName = "Down")]
public ButtonControl down { get; set; }
/// <summary>
/// A synthetic button representing the left half of the stick's X axis, i.e. the -1 to 0 range (inverted).
/// </summary>
/// <value>Control representing the stick's left half X axis.</value>
/// <remarks>
/// The control is marked as <see cref="InputControl.synthetic"/>.
/// </remarks>
[InputControl(useStateFrom = "x", processors = "axisDeadzone", parameters = "clamp=2,clampMin=-1,clampMax=0,invert", synthetic = true, displayName = "Left")]
public ButtonControl left { get; set; }
/// <summary>
/// A synthetic button representing the right half of the stick's X axis, i.e. the 0 to 1 range.
/// </summary>
/// <value>Control representing the stick's right half X axis.</value>
/// <remarks>
/// The control is marked as <see cref="InputControl.synthetic"/>.
/// </remarks>
[InputControl(useStateFrom = "x", processors = "axisDeadzone", parameters = "clamp=2,clampMin=0,clampMax=1", synthetic = true, displayName = "Right")]
public ButtonControl right { get; set; }
protected override void FinishSetup()
{
base.FinishSetup();
up = GetChildControl<ButtonControl>("up");
down = GetChildControl<ButtonControl>("down");
left = GetChildControl<ButtonControl>("left");
right = GetChildControl<ButtonControl>("right");
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 854faad156b245d4b50a15e233e9ad55
timeCreated: 1506743413

View File

@@ -0,0 +1,243 @@
using UnityEngine.InputSystem.Utilities;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.Scripting;
////TODO: enforce memory layout of TouchControl to be that of TouchState (build that into the layout system? "freeze"/final layout?)
namespace UnityEngine.InputSystem.Controls
{
/// <summary>
/// A control representing a touch contact.
/// </summary>
/// <remarks>
/// Note that unlike most other control types, <c>TouchControls</c> do not have
/// a flexible memory layout. They are hardwired to <see cref="TouchState"/> and
/// will not work correctly with a different memory layouts. Additional fields may
/// be appended to the struct but what's there in the struct has to be located
/// at exactly those memory addresses.
/// </remarks>
[InputControlLayout(stateType = typeof(TouchState))]
public class TouchControl : InputControl<TouchState>
{
/// <summary>
/// Button that indicates whether there is currently an ongoing touch
/// contact on the control. When touch is ongoing, button will be 1,
/// otherwise button will be 0.
/// </summary>
/// <value>Control representing an ongoing touch contact.</value>
/// <remarks>
/// This control simply monitors <see cref="phase"/> and will read as 1 whenever
/// the phase is <see cref="TouchPhase.Began"/>, <see cref="TouchPhase.Moved"/>,
/// or <see cref="TouchPhase.Stationary"/>.
/// </remarks>
/// <seealso cref="phase"/>
public TouchPressControl press { get; set; }
/// <summary>
/// Gets the index of the display that was touched.
/// <see href="https://docs.unity3d.com/ScriptReference/Display.html"/>
/// </summary>
public IntegerControl displayIndex { get; set; }
/// <summary>
/// The ID of the touch contact as reported by the underlying system.
/// </summary>
/// <value>Control reading out the ID of the touch.</value>
/// <remarks>
/// Each touch contact that is made with the screen receives its own unique ID which is
/// normally assigned by the underlying platform.
///
/// Note a platform may reuse touch IDs after their respective touches have finished.
/// This means that the guarantee of uniqueness is only made with respect to currently
/// ongoing touches.
/// </remarks>
/// <seealso cref="TouchState.touchId"/>
public IntegerControl touchId { get; set; }
/// <summary>
/// Absolute screen-space position on the touch surface.
/// </summary>
/// <value>Control representing the screen-space of the touch.</value>
/// <seealso cref="TouchState.position"/>
public Vector2Control position { get; set; }
/// <summary>
/// Screen-space motion delta of the touch.
/// </summary>
/// <value>Control representing the screen-space motion delta of the touch.</value>
/// <remarks>
/// This is either supplied directly by the underlying platform or computed on the
/// fly by <see cref="Touchscreen"/> from the last known position of the touch.
///
/// Note that deltas have behaviors attached to them different from most other
/// controls. See <see cref="Pointer.delta"/> for details.
/// </remarks>
/// <seealso cref="TouchState.delta"/>
public DeltaControl delta { get; set; }
/// <summary>
/// Normalized pressure of the touch against the touch surface.
/// </summary>
/// <value>Control representing the pressure level of the touch.</value>
/// <remarks>
/// Not all touchscreens are pressure-sensitive. If unsupported, this control will remain
/// at default value.
///
/// In general, touch pressure is supported on mobile platforms only.
///
/// Note that it is possible for the value to go above 1 even though it is considered normalized. The reason is
/// that calibration on the system can put the maximum pressure point below the physically supported maximum value.
/// </remarks>
/// <seealso cref="TouchState.pressure"/>
/// <seealso cref="Pointer.pressure"/>
public AxisControl pressure { get; set; }
/// <summary>
/// Screen-space radius of the touch.
/// </summary>
/// <value>Control representing the horizontal and vertical extents of the touch contact.</value>
/// <remarks>
/// If supported by the device, this reports the size of the touch contact based on its
/// <see cref="position"/> center point. If not supported, this will be <c>default(Vector2)</c>.
/// </remarks>
/// <seealso cref="Pointer.radius"/>
public Vector2Control radius { get; set; }
/// <summary>
/// Current phase of the touch.
/// </summary>
/// <value>Control representing the current phase of the touch.</value>
/// <remarks>
/// This will be <see cref="TouchPhase.None"/> if no touch has been registered on the control
/// yet or if the control has been reset to its default state.
/// </remarks>
/// <seealso cref="isInProgress"/>
public TouchPhaseControl phase { get; set; }
/// <summary>
/// Whether the touch comes from a source other than direct contact with the touch surface.
/// </summary>
/// <value>Control indicating whether the touch was generated indirectly.</value>
/// <remarks>
/// Indirect touches can be generated with a stylus, for example.
/// </remarks>
public ButtonControl indirectTouch { get; set; }
/// <summary>
/// Whether the touch has performed a tap.
/// </summary>
/// <value>Control that indicates whether the touch has tapped the screen.</value>
/// <remarks>
/// A tap is defined as a touch that begins and ends within <see cref="InputSettings.defaultTapTime"/> and
/// stays within <see cref="InputSettings.tapRadius"/> of its <see cref="startPosition"/>. If this
/// is the case for a touch, this button is set to 1 at the time the touch goes to <see cref="phase"/>
/// <see cref="TouchPhase.Ended"/>.
///
/// The button resets to 0 only when another touch is started on the control or when the control
/// is reset.
/// </remarks>
/// <seealso cref="tapCount"/>
/// <seealso cref="InputSettings.defaultTapTime"/>
public ButtonControl tap { get; set; }
/// <summary>
/// Number of times that the touch has been tapped in succession.
/// </summary>
/// <value>Control that indicates how many taps have been performed one after the other.</value>
/// <remarks>
/// Successive taps have to come within <see cref="InputSettings.multiTapDelayTime"/> for them
/// to increase the tap count. I.e. if a new tap finishes within that time after <see cref="startTime"/>
/// of the previous touch, the tap count is increased by one. If more than <see cref="InputSettings.multiTapDelayTime"/>
/// passes after a tap with no successive tap, the tap count is reset to zero.
/// </remarks>
public IntegerControl tapCount { get; set; }
/// <summary>
/// Time in seconds on the same timeline as <c>Time.realTimeSinceStartup</c> when the touch began.
/// </summary>
/// <value>Control representing the start time of the touch.</value>
/// <remarks>
/// This is the value of <see cref="InputEvent.time"/> when the touch starts with
/// <see cref="phase"/> <see cref="TouchPhase.Began"/>.
/// </remarks>
/// <seealso cref="InputEvent.time"/>
public DoubleControl startTime { get; set; }
/// <summary>
/// Screen-space position where the touch started.
/// </summary>
/// <value>Control representing the start position of the touch.</value>
/// <seealso cref="position"/>
public Vector2Control startPosition { get; set; }
/// <summary>
/// Whether a touch on the control is currently is progress.
/// </summary>
/// <value>If true, a touch is in progress, i.e. has a <see cref="phase"/> of
/// <see cref="TouchPhase.Began"/>, <see cref="TouchPhase.Moved"/>, or <see
/// cref="TouchPhase.Canceled"/>.</value>
public bool isInProgress
{
get
{
switch (phase.value)
{
case TouchPhase.Began:
case TouchPhase.Moved:
case TouchPhase.Stationary:
return true;
}
return false;
}
}
/// <summary>
/// Default-initialize the touch control.
/// </summary>
/// <remarks>
/// Sets the <see cref="InputStateBlock.format"/> to <c>"TOUC"</c>.
/// </remarks>
public TouchControl()
{
m_StateBlock.format = new FourCC('T', 'O', 'U', 'C');
}
/// <inheritdoc />
protected override void FinishSetup()
{
press = GetChildControl<TouchPressControl>("press");
displayIndex = GetChildControl<IntegerControl>("displayIndex");
touchId = GetChildControl<IntegerControl>("touchId");
position = GetChildControl<Vector2Control>("position");
delta = GetChildControl<DeltaControl>("delta");
pressure = GetChildControl<AxisControl>("pressure");
radius = GetChildControl<Vector2Control>("radius");
phase = GetChildControl<TouchPhaseControl>("phase");
indirectTouch = GetChildControl<ButtonControl>("indirectTouch");
tap = GetChildControl<ButtonControl>("tap");
tapCount = GetChildControl<IntegerControl>("tapCount");
startTime = GetChildControl<DoubleControl>("startTime");
startPosition = GetChildControl<Vector2Control>("startPosition");
////TODO: throw if state layouts of the controls doesn't match TouchState
base.FinishSetup();
}
/// <inheritdoc />
public override unsafe TouchState ReadUnprocessedValueFromState(void* statePtr)
{
var valuePtr = (TouchState*)((byte*)statePtr + (int)m_StateBlock.byteOffset);
return *valuePtr;
}
/// <inheritdoc />
public override unsafe void WriteValueIntoState(TouchState value, void* statePtr)
{
var valuePtr = (TouchState*)((byte*)statePtr + (int)m_StateBlock.byteOffset);
UnsafeUtility.MemCpy(valuePtr, UnsafeUtility.AddressOf(ref value), UnsafeUtility.SizeOf<TouchState>());
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 793d3fce58f547d688fdc78bee75cd30
timeCreated: 1508737227

View File

@@ -0,0 +1,48 @@
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
// Unfortunately, C# (at least up to version 6) does not support enum type constraints. There's
// ways to work around it in some situations (https://stackoverflow.com/questions/79126/create-generic-method-constraining-t-to-an-enum)
// but not in a way that will allow us to convert an int to the enum type.
////TODO: allow this to be stored in less than 32bits
namespace UnityEngine.InputSystem.Controls
{
/// <summary>
/// A control reading a <see cref="TouchPhase"/> value.
/// </summary>
/// <remarks>
/// This is used mainly by <see cref="Touchscreen"/> to read <see cref="TouchState.phase"/>.
/// </remarks>
/// <seealso cref="Touchscreen"/>
[InputControlLayout(hideInUI = true)]
public class TouchPhaseControl : InputControl<TouchPhase>
{
/// <summary>
/// Default-initialize the control.
/// </summary>
/// <remarks>
/// Format of the control is <see cref="InputStateBlock.FormatInt"/>
/// by default.
/// </remarks>
public TouchPhaseControl()
{
m_StateBlock.format = InputStateBlock.FormatInt;
}
/// <inheritdoc />
public override unsafe TouchPhase ReadUnprocessedValueFromState(void* statePtr)
{
var intValue = stateBlock.ReadInt(statePtr);
return (TouchPhase)intValue;
}
/// <inheritdoc />
public override unsafe void WriteValueIntoState(TouchPhase value, void* statePtr)
{
var valuePtr = (byte*)statePtr + (int)m_StateBlock.byteOffset;
*(TouchPhase*)valuePtr = value;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2e3ecb5c3d351df489bd05ac1d820a6a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,52 @@
using System;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
namespace UnityEngine.InputSystem.Controls
{
/// <summary>
/// A button that reads its pressed state from <see cref="TouchControl.phase"/>.
/// </summary>
/// <remarks>
/// This control is used by <see cref="TouchControl"/> to link <see cref="TouchControl.press"/>
/// to <see cref="TouchControl.phase"/>. It will return 1 as long as the value of
/// phase is <see cref="TouchPhase.Began"/>, <see cref="TouchPhase.Stationary"/>, or
/// <see cref="TouchPhase.Moved"/>, i.e. as long as the touch is in progress. For
/// all other phases, it will return 0.
/// </remarks>
/// <seealso cref="TouchControl"/>
[InputControlLayout(hideInUI = true)]
public class TouchPressControl : ButtonControl
{
/// <inheritdoc />
protected override void FinishSetup()
{
base.FinishSetup();
if (!stateBlock.format.IsIntegerFormat())
throw new NotSupportedException(
$"Non-integer format '{stateBlock.format}' is not supported for TouchButtonControl '{this}'");
}
/// <inheritdoc />
public override unsafe float ReadUnprocessedValueFromState(void* statePtr)
{
var valuePtr = (byte*)statePtr + (int)m_StateBlock.byteOffset;
var uintValue = MemoryHelpers.ReadMultipleBitsAsUInt(valuePtr, m_StateBlock.bitOffset, m_StateBlock.sizeInBits);
var phaseValue = (TouchPhase)uintValue;
var value = 0.0f;
if (phaseValue == TouchPhase.Began || phaseValue == TouchPhase.Stationary ||
phaseValue == TouchPhase.Moved)
value = 1;
return Preprocess(value);
}
public override unsafe void WriteValueIntoState(float value, void* statePtr)
{
throw new NotSupportedException();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e3da24ab555e0c04793b0618d387a2b5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,106 @@
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
namespace UnityEngine.InputSystem.Controls
{
/// <summary>
/// A floating-point 2D vector control composed of two <see cref="AxisControl"/>s.
/// </summary>
/// <remarks>
/// An example is <see cref="Pointer.position"/>.
///
/// <example>
/// <code>
/// Debug.Log(string.Format("Mouse position x={0} y={1}",
/// Mouse.current.position.x.ReadValue(),
/// Mouse.current.position.y.ReadValue()));
/// </code>
/// </example>
///
/// Normalization is not implied. The X and Y coordinates can be in any range or units.
/// </remarks>
public class Vector2Control : InputControl<Vector2>
{
/// <summary>
/// Horizontal position of the control.
/// </summary>
/// <value>Control representing horizontal motion input.</value>
[InputControl(offset = 0, displayName = "X")]
public AxisControl x { get; set; }
/// <summary>
/// Vertical position of the control.
/// </summary>
/// <value>Control representing vertical motion input.</value>
[InputControl(offset = 4, displayName = "Y")]
public AxisControl y { get; set; }
/// <summary>
/// Default-initialize the control.
/// </summary>
public Vector2Control()
{
m_StateBlock.format = InputStateBlock.FormatVector2;
}
/// <inheritdoc />
protected override void FinishSetup()
{
x = GetChildControl<AxisControl>("x");
y = GetChildControl<AxisControl>("y");
base.FinishSetup();
}
/// <inheritdoc />
public override unsafe Vector2 ReadUnprocessedValueFromState(void* statePtr)
{
switch (m_OptimizedControlDataType)
{
case InputStateBlock.kFormatVector2:
return *(Vector2*)((byte*)statePtr + (int)m_StateBlock.byteOffset);
default:
return new Vector2(
x.ReadUnprocessedValueFromStateWithCaching(statePtr),
y.ReadUnprocessedValueFromStateWithCaching(statePtr));
}
}
/// <inheritdoc />
public override unsafe void WriteValueIntoState(Vector2 value, void* statePtr)
{
switch (m_OptimizedControlDataType)
{
case InputStateBlock.kFormatVector2:
*(Vector2*)((byte*)statePtr + (int)m_StateBlock.byteOffset) = value;
break;
default:
x.WriteValueIntoState(value.x, statePtr);
y.WriteValueIntoState(value.y, statePtr);
break;
}
}
/// <inheritdoc />
public override unsafe float EvaluateMagnitude(void* statePtr)
{
////REVIEW: this can go beyond 1; that okay?
return ReadValueFromStateWithCaching(statePtr).magnitude;
}
protected override FourCC CalculateOptimizedControlDataType()
{
if (
m_StateBlock.sizeInBits == sizeof(float) * 2 * 8 &&
m_StateBlock.bitOffset == 0 &&
x.optimizedControlDataType == InputStateBlock.FormatFloat &&
y.optimizedControlDataType == InputStateBlock.FormatFloat &&
y.m_StateBlock.byteOffset == x.m_StateBlock.byteOffset + 4
)
return InputStateBlock.FormatVector2;
return InputStateBlock.FormatInvalid;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 06c77ad4af684d2eab4294e0d51afccd
timeCreated: 1506742985

View File

@@ -0,0 +1,84 @@
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
namespace UnityEngine.InputSystem.Controls
{
/// <summary>
/// A floating-point 3D vector control composed of three <see cref="AxisControl">AxisControls</see>.
/// </summary>
public class Vector3Control : InputControl<Vector3>
{
[InputControl(offset = 0, displayName = "X")]
public AxisControl x { get; set; }
[InputControl(offset = 4, displayName = "Y")]
public AxisControl y { get; set; }
[InputControl(offset = 8, displayName = "Z")]
public AxisControl z { get; set; }
public Vector3Control()
{
m_StateBlock.format = InputStateBlock.FormatVector3;
}
protected override void FinishSetup()
{
x = GetChildControl<AxisControl>("x");
y = GetChildControl<AxisControl>("y");
z = GetChildControl<AxisControl>("z");
base.FinishSetup();
}
public override unsafe Vector3 ReadUnprocessedValueFromState(void* statePtr)
{
switch (m_OptimizedControlDataType)
{
case InputStateBlock.kFormatVector3:
return *(Vector3*)((byte*)statePtr + (int)m_StateBlock.byteOffset);
default:
return new Vector3(
x.ReadUnprocessedValueFromStateWithCaching(statePtr),
y.ReadUnprocessedValueFromStateWithCaching(statePtr),
z.ReadUnprocessedValueFromStateWithCaching(statePtr));
}
}
public override unsafe void WriteValueIntoState(Vector3 value, void* statePtr)
{
switch (m_OptimizedControlDataType)
{
case InputStateBlock.kFormatVector3:
*(Vector3*)((byte*)statePtr + (int)m_StateBlock.byteOffset) = value;
break;
default:
x.WriteValueIntoState(value.x, statePtr);
y.WriteValueIntoState(value.y, statePtr);
z.WriteValueIntoState(value.z, statePtr);
break;
}
}
public override unsafe float EvaluateMagnitude(void* statePtr)
{
////REVIEW: this can go beyond 1; that okay?
return ReadValueFromStateWithCaching(statePtr).magnitude;
}
protected override FourCC CalculateOptimizedControlDataType()
{
if (
m_StateBlock.sizeInBits == sizeof(float) * 3 * 8 &&
m_StateBlock.bitOffset == 0 &&
x.optimizedControlDataType == InputStateBlock.FormatFloat &&
y.optimizedControlDataType == InputStateBlock.FormatFloat &&
z.optimizedControlDataType == InputStateBlock.FormatFloat &&
y.m_StateBlock.byteOffset == x.m_StateBlock.byteOffset + 4 &&
z.m_StateBlock.byteOffset == x.m_StateBlock.byteOffset + 8
)
return InputStateBlock.FormatVector3;
return InputStateBlock.FormatInvalid;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 914fdb42cb964faf92803ee51a7279fd
timeCreated: 1506742977