UI 자동화를 위해 바인딩 기능 구현

- 유니티 에셋 인증 오류로 meta 재생성
This commit is contained in:
2026-01-25 01:31:34 +09:00
parent 2ceb28f55d
commit ce83f21c93
1861 changed files with 377882 additions and 211 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6adbf65a53ff9b04990c2116b3e4d5b7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,240 @@
using System.ComponentModel;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Processors;
using UnityEngine.InputSystem.Utilities;
#if UNITY_EDITOR
using System;
using UnityEngine.InputSystem.Editor;
using UnityEngine.UIElements;
#endif
namespace UnityEngine.InputSystem.Composites
{
/// <summary>
/// A single axis value computed from one axis that pulls in the <see cref="negative"/> direction (<see cref="minValue"/>) and one
/// axis that pulls in the <see cref="positive"/> direction (<see cref="maxValue"/>).
/// </summary>
/// <remarks>
/// The limits of the axis are determined by <see cref="minValue"/> and <see cref="maxValue"/>.
/// By default, they are set to <c>[-1..1]</c>. The values can be set as parameters.
///
/// <example>
/// <code>
/// var action = new InputAction();
/// action.AddCompositeBinding("Axis(minValue=0,maxValue=2)")
/// .With("Negative", "&lt;Keyboard&gt;/a")
/// .With("Positive", "&lt;Keyboard&gt;/d");
/// </code>
/// </example>
///
/// If both axes are actuated at the same time, the behavior depends on <see cref="whichSideWins"/>.
/// By default, neither side will win (<see cref="WhichSideWins.Neither"/>) and the result
/// will be 0 (or, more precisely, the midpoint between <see cref="minValue"/> and <see cref="maxValue"/>).
/// This can be customized to make the positive side win (<see cref="WhichSideWins.Positive"/>)
/// or the negative one (<see cref="WhichSideWins.Negative"/>).
///
/// This is useful, for example, in a driving game where break should cancel out accelerate.
/// By binding <see cref="negative"/> to the break control(s) and <see cref="positive"/> to the
/// acceleration control(s), and setting <see cref="whichSideWins"/> to <see cref="WhichSideWins.Negative"/>,
/// if the break button is pressed, it will always cause the acceleration button to be ignored.
///
/// The actual <em>absolute</em> values of <see cref="negative"/> and <see cref="positive"/> are used
/// to scale <see cref="minValue"/> and <see cref="maxValue"/> respectively. So if, for example, <see cref="positive"/>
/// is bound to <see cref="Gamepad.rightTrigger"/> and the trigger is at a value of 0.5, then the resulting
/// value is <c>maxValue * 0.5</c> (the actual formula is <c>midPoint + (maxValue - midPoint) * positive</c>).
/// </remarks>
[DisplayStringFormat("{negative}/{positive}")]
[DisplayName("Positive/Negative Binding")]
public class AxisComposite : InputBindingComposite<float>
{
/// <summary>
/// Binding for the axis input that controls the negative [<see cref="minValue"/>..0] direction of the
/// combined axis.
/// </summary>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
[InputControl(layout = "Axis")] public int negative = 0;
/// <summary>
/// Binding for the axis input that controls the positive [0..<see cref="maxValue"/>] direction of the
/// combined axis.
/// </summary>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
[InputControl(layout = "Axis")] public int positive = 0;
/// <summary>
/// The lower bound that the axis is limited to. -1 by default.
/// </summary>
/// <remarks>
/// This value corresponds to the full actuation of the control(s) bound to <see cref="negative"/>.
///
/// <example>
/// <code>
/// var action = new InputAction();
/// action.AddCompositeBinding("Axis(minValue=0,maxValue=2)")
/// .With("Negative", "&lt;Keyboard&gt;/a")
/// .With("Positive", "&lt;Keyboard&gt;/d");
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="maxValue"/>
/// <seealso cref="negative"/>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
[Tooltip("Value to return when the negative side is fully actuated.")]
public float minValue = -1;
/// <summary>
/// The upper bound that the axis is limited to. 1 by default.
/// </summary>
/// <remarks>
/// This value corresponds to the full actuation of the control(s) bound to <see cref="positive"/>.
///
/// <example>
/// <code>
/// var action = new InputAction();
/// action.AddCompositeBinding("Axis(minValue=0,maxValue=2)")
/// .With("Negative", "&lt;Keyboard&gt;/a")
/// .With("Positive", "&lt;Keyboard&gt;/d");
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="minValue"/>
/// <seealso cref="positive"/>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
[Tooltip("Value to return when the positive side is fully actuated.")]
public float maxValue = 1;
/// <summary>
/// If both the <see cref="positive"/> and <see cref="negative"/> button are actuated, this
/// determines which value is returned from the composite.
/// </summary>
[Tooltip("If both the positive and negative side are actuated, decides what value to return. 'Neither' (default) means that " +
"the resulting value is the midpoint between min and max. 'Positive' means that max will be returned. 'Negative' means that " +
"min will be returned.")]
public WhichSideWins whichSideWins = WhichSideWins.Neither;
/// <summary>
/// The value that is returned if the composite is in a neutral position, that is, if
/// neither <see cref="positive"/> nor <see cref="negative"/> are actuated or if
/// <see cref="whichSideWins"/> is set to <see cref="WhichSideWins.Neither"/> and
/// both <see cref="positive"/> and <see cref="negative"/> are actuated.
/// </summary>
public float midPoint => (maxValue + minValue) / 2;
////TODO: add parameters to control ramp up&down
/// <inheritdoc />
public override float ReadValue(ref InputBindingCompositeContext context)
{
var negativeValue = Mathf.Abs(context.ReadValue<float>(negative));
var positiveValue = Mathf.Abs(context.ReadValue<float>(positive));
var negativeIsActuated = negativeValue > Mathf.Epsilon;
var positiveIsActuated = positiveValue > Mathf.Epsilon;
if (negativeIsActuated == positiveIsActuated)
{
switch (whichSideWins)
{
case WhichSideWins.Negative:
positiveIsActuated = false;
break;
case WhichSideWins.Positive:
negativeIsActuated = false;
break;
case WhichSideWins.Neither:
return midPoint;
}
}
var mid = midPoint;
if (negativeIsActuated)
return mid - (mid - minValue) * negativeValue;
return mid + (maxValue - mid) * positiveValue;
}
/// <inheritdoc />
public override float EvaluateMagnitude(ref InputBindingCompositeContext context)
{
var value = ReadValue(ref context);
if (value < midPoint)
{
value = Mathf.Abs(value - midPoint);
return NormalizeProcessor.Normalize(value, 0, Mathf.Abs(minValue), 0);
}
value = Mathf.Abs(value - midPoint);
return NormalizeProcessor.Normalize(value, 0, Mathf.Abs(maxValue), 0);
}
/// <summary>
/// What happens to the value of an <see cref="AxisComposite"/> if both <see cref="positive"/>
/// and <see cref="negative"/> are actuated at the same time.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1717:OnlyFlagsEnumsShouldHavePluralNames", Justification = "False positive: `Wins` is not a plural form.")]
public enum WhichSideWins
{
/// <summary>
/// If both <see cref="positive"/> and <see cref="negative"/> are actuated, the sides cancel
/// each other out and the result is 0.
/// </summary>
Neither = 0,
/// <summary>
/// If both <see cref="positive"/> and <see cref="negative"/> are actuated, the value of
/// <see cref="positive"/> wins and <see cref="negative"/> is ignored.
/// </summary>
Positive = 1,
/// <summary>
/// If both <see cref="positive"/> and <see cref="negative"/> are actuated, the value of
/// <see cref="negative"/> wins and <see cref="positive"/> is ignored.
/// </summary>
Negative = 2,
}
}
#if UNITY_EDITOR
internal class AxisCompositeEditor : InputParameterEditor<AxisComposite>
{
private const string label = "Which Side Wins";
private const string tooltipText = "Determine which axis 'wins' if both are actuated at the same time. "
+ "If 'Neither' is selected, the result is 0 (or, more precisely, "
+ "the midpoint between minValue and maxValue).";
public override void OnGUI()
{
}
public override void OnDrawVisualElements(VisualElement root, Action onChangedCallback)
{
var modeField = new EnumField(label, target.whichSideWins)
{
tooltip = tooltipText
};
modeField.RegisterValueChangedCallback(evt =>
{
target.whichSideWins = (AxisComposite.WhichSideWins)evt.newValue;
onChangedCallback();
});
root.Add(modeField);
}
}
#endif
}

View File

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

View File

@@ -0,0 +1,203 @@
using System;
using System.ComponentModel;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.Scripting;
////TODO: remove this once we can break the API
namespace UnityEngine.InputSystem.Composites
{
/// <summary>
/// A button with an additional modifier. The button only triggers when
/// the modifier is pressed.
/// </summary>
/// <remarks>
/// This composite can be used to require another button to be held while
/// pressing the button that triggers the action. This is most commonly used
/// on keyboards to require one of the modifier keys (shift, ctrl, or alt)
/// to be held in combination with another key, e.g. "CTRL+1".
///
/// <example>
/// <code>
/// // Create a button action that triggers when CTRL+1
/// // is pressed on the keyboard.
/// var action = new InputAction(type: InputActionType.Button);
/// action.AddCompositeBinding("ButtonWithOneModifier")
/// .With("Modifier", "&lt;Keyboard&gt;/leftCtrl")
/// .With("Modifier", "&lt;Keyboard&gt;/rightControl")
/// .With("Button", "&lt;Keyboard&gt;/1")
/// </code>
/// </example>
///
/// Note that this is not restricted to the keyboard and will preserve
/// the full value of the button.
///
/// <example>
/// <code>
/// // Create a button action that requires the A button on the
/// // gamepad to be held and will then trigger from the gamepad's
/// // left trigger button.
/// var action = new InputAction(type: InputActionType.Button);
/// action.AddCompositeBinding("ButtonWithOneModifier")
/// .With("Modifier", "&lt;Gamepad&gt;/buttonSouth")
/// .With("Button", "&lt;Gamepad&gt;/leftTrigger");
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="ButtonWithTwoModifiers"/>
[DesignTimeVisible(false)] // Obsoleted by OneModifierComposite
[DisplayStringFormat("{modifier}+{button}")]
public class ButtonWithOneModifier : InputBindingComposite<float>
{
/// <summary>
/// Binding for the button that acts as a modifier, e.g. <c>&lt;Keyboard/leftCtrl</c>.
/// </summary>
/// <value>Part index to use with <see cref="InputBindingCompositeContext.ReadValue{T}(int)"/>.</value>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
// ReSharper disable once UnassignedField.Global
[InputControl(layout = "Button")] public int modifier;
/// <summary>
/// Binding for the button that is gated by the modifier. The composite will assume the value
/// of this button while the modifier is pressed.
/// </summary>
/// <value>Part index to use with <see cref="InputBindingCompositeContext.ReadValue{T}(int)"/>.</value>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
// ReSharper disable once UnassignedField.Global
[InputControl(layout = "Button")] public int button;
/// <summary>
/// If set to <c>true</c>, <see cref="modifier"/> can be pressed after <see cref="button"/> and the composite will
/// still trigger. Default is false.
/// </summary>
/// <remarks>
/// By default, if the setting <see cref="InputSettings.shortcutKeysConsumeInput"/> is enabled,
/// <see cref="modifier"/> is required to be in pressed state before or at the same time that <see cref="button"/>
/// goes into pressed state for the composite as a whole to trigger. This means that binding to, for example, <c>Shift+B</c>,
/// the <c>shift</c> key has to be pressed before pressing the <c>B</c> key. This is the behavior usually expected with
/// keyboard shortcuts.
///
/// This parameter can be used to bypass this behavior and allow any timing between <see cref="modifier"/> and <see cref="button"/>.
/// The only requirement is for them both to concurrently be in pressed state.
///
/// To don't depends on the setting please consider using <see cref="modifiersOrder"/> instead.
/// </remarks>
[Tooltip("Obsolete please use modifiers Order. If enabled, this will override the Input Consumption setting, allowing the modifier keys to be pressed after the button and the composite will still trigger.")]
[Obsolete("Use ModifiersOrder.Unordered with 'modifiersOrder' instead")]
public bool overrideModifiersNeedToBePressedFirst;
/// <summary>
/// Determines how a <c>modifiers</c> keys need to be pressed in order or not.
/// </summary>
public enum ModifiersOrder
{
/// <summary>
/// By default, if the setting <see cref="InputSettings.shortcutKeysConsumeInput"/> is enabled,
/// <see cref="modifier"/> is required to be in pressed state before or at the same time that <see cref="button"/>
/// goes into pressed state for the composite as a whole to trigger. This means that binding to, for example, <c>Shift+B</c>,
/// the <c>shift</c> key has to be pressed before pressing the <c>B</c> key. This is the behavior usually expected with
/// keyboard shortcuts.
///
/// If the setting <see cref="InputSettings.shortcutKeysConsumeInput"/> is disabled,
/// modifiers can be pressed after the button and the composite will still trigger.
/// </summary>
Default = 0,
/// <summary>
/// <see cref="modifier"/> is required to be in pressed state before or at the same
/// time that <see cref="button"/> goes into pressed state for the composite as a whole to trigger. This means that binding to,
/// for example, <c>Ctrl+B</c>, the <c>ctrl</c> key have to be pressed before pressing the <c>B</c> key.
/// This is the behavior usually expected with keyboard shortcuts.
/// </summary>
Ordered = 1,
/// <summary>
/// <see cref="modifier"/> can be pressed after <see cref="button"/>
/// and the composite will still trigger. The only requirement is for all of them to concurrently be in pressed state.
/// </summary>
Unordered = 2
}
/// <summary>
/// If set to <c>Ordered</c> or <c>Unordered</c>, the built-in logic to determine if modifiers need to be pressed first is overridden.
/// </summary>
/// <remarks>
/// By default, if the setting <see cref="InputSettings.shortcutKeysConsumeInput"/> is enabled,
/// <see cref="modifier"/> is required to be in pressed state before or at the same time that <see cref="button"/>
/// goes into pressed state for the composite as a whole to trigger. This means that binding to, for example, <c>Shift+B</c>,
/// the <c>shift</c> key has to be pressed before pressing the <c>B</c> key. This is the behavior usually expected with
/// keyboard shortcuts.
///
/// If the setting <see cref="InputSettings.shortcutKeysConsumeInput"/> is disabled,
/// modifiers can be pressed after the button and the composite will still trigger.
///
/// This parameter can be used to bypass this behavior and enforce the timing order or allow any timing between <see cref="modifier"/> and <see cref="button"/>.
/// The only requirement is for them both to concurrently be in pressed state.
/// </remarks>
[Tooltip("By default it follows the Input Consumption setting to determine if the modifers keys need to be pressed first.")]
public ModifiersOrder modifiersOrder = ModifiersOrder.Default;
/// <summary>
/// Return the value of the <see cref="button"/> part if <see cref="modifier"/> is pressed. Otherwise
/// return 0.
/// </summary>
/// <param name="context">Evaluation context passed in from the input system.</param>
/// <returns>The current value of the composite.</returns>
public override float ReadValue(ref InputBindingCompositeContext context)
{
if (ModifierIsPressed(ref context))
return context.ReadValue<float>(button);
return default;
}
private bool ModifierIsPressed(ref InputBindingCompositeContext context)
{
var modifierDown = context.ReadValueAsButton(modifier);
if (modifierDown && modifiersOrder == ModifiersOrder.Ordered)
{
var timestamp = context.GetPressTime(button);
var timestamp1 = context.GetPressTime(modifier);
return timestamp1 <= timestamp;
}
return modifierDown;
}
/// <summary>
/// Same as <see cref="ReadValue"/> in this case.
/// </summary>
/// <param name="context">Evaluation context passed in from the input system.</param>
/// <returns>A >0 value if the composite is currently actuated.</returns>
public override float EvaluateMagnitude(ref InputBindingCompositeContext context)
{
return ReadValue(ref context);
}
protected override void FinishSetup(ref InputBindingCompositeContext context)
{
if (modifiersOrder == ModifiersOrder.Default)
{
// Legacy. We need to reference the obsolete member here so temporarily
// turn off the warning.
#pragma warning disable CS0618
if (overrideModifiersNeedToBePressedFirst)
#pragma warning restore CS0618
modifiersOrder = ModifiersOrder.Unordered;
else
modifiersOrder = InputSystem.settings.shortcutKeysConsumeInput ? ModifiersOrder.Ordered : ModifiersOrder.Unordered;
}
}
}
}

View File

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

View File

@@ -0,0 +1,218 @@
using System;
using System.ComponentModel;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.Scripting;
////TODO: remove this once we can break the API
namespace UnityEngine.InputSystem.Composites
{
/// <summary>
/// A button with two additional modifiers. The button only triggers when
/// both modifiers are pressed.
/// </summary>
/// <remarks>
/// This composite can be used to require two other buttons to be held while
/// using the control that triggers the action. This is most commonly used
/// on keyboards to require two of the modifier keys (shift, ctrl, or alt)
/// to be held in combination with another key, e.g. "CTRL+SHIFT+1".
///
/// <example>
/// <code>
/// // Create a button action that triggers when CTRL+SHIFT+1
/// // is pressed on the keyboard.
/// var action = new InputAction(type: InputActionType.Button);
/// action.AddCompositeBinding("TwoModifiers")
/// .With("Modifier1", "&lt;Keyboard&gt;/leftCtrl")
/// .With("Modifier1", "&lt;Keyboard&gt;/rightCtrl")
/// .With("Modifier2", "&lt;Keyboard&gt;/leftShift")
/// .With("Modifier2", "&lt;Keyboard&gt;/rightShift")
/// .With("Button", "&lt;Keyboard&gt;/1")
/// </code>
/// </example>
///
/// Note that this is not restricted to the keyboard and will preserve
/// the full value of the button.
///
/// <example>
/// <code>
/// // Create a button action that requires the A and X button on the
/// // gamepad to be held and will then trigger from the gamepad's
/// // left trigger button.
/// var action = new InputAction(type: InputActionType.Button);
/// action.AddCompositeBinding("ButtonWithTwoModifiers")
/// .With("Modifier1", "&lt;Gamepad&gt;/buttonSouth")
/// .With("Modifier2", "&lt;Gamepad&gt;/buttonWest")
/// .With("Button", "&lt;Gamepad&gt;/leftTrigger");
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="ButtonWithOneModifier"/>
[DesignTimeVisible(false)] // Obsoleted by TwoModifiersComposite
[DisplayStringFormat("{modifier1}+{modifier2}+{button}")]
public class ButtonWithTwoModifiers : InputBindingComposite<float>
{
/// <summary>
/// Binding for the first button that acts as a modifier, e.g. <c>&lt;Keyboard/leftCtrl</c>.
/// </summary>
/// <value>Part index to use with <see cref="InputBindingCompositeContext.ReadValue{T}(int)"/>.</value>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
// ReSharper disable once UnassignedField.Global
[InputControl(layout = "Button")] public int modifier1;
/// <summary>
/// Binding for the second button that acts as a modifier, e.g. <c>&lt;Keyboard/leftCtrl</c>.
/// </summary>
/// <value>Part index to use with <see cref="InputBindingCompositeContext.ReadValue{T}(int)"/>.</value>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
// ReSharper disable once UnassignedField.Global
[InputControl(layout = "Button")] public int modifier2;
/// <summary>
/// Binding for the button that is gated by <see cref="modifier1"/> and <see cref="modifier2"/>.
/// The composite will assume the value of this button while both of the modifiers are pressed.
/// </summary>
/// <value>Part index to use with <see cref="InputBindingCompositeContext.ReadValue{T}(int)"/>.</value>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
// ReSharper disable once UnassignedField.Global
[InputControl(layout = "Button")] public int button;
/// <summary>
/// If set to <c>true</c>, <see cref="modifier1"/> and/or <see cref="modifier2"/> can be pressed after <see cref="button"/>
/// and the composite will still trigger. Default is false.
/// </summary>
/// <remarks>
/// By default, if the setting <see cref="InputSettings.shortcutKeysConsumeInput"/> is enabled,
/// <see cref="modifier1"/> and <see cref="modifier2"/> are required to be in pressed state before or at the same
/// time that <see cref="button"/> goes into pressed state for the composite as a whole to trigger. This means that binding to,
/// for example, <c>Ctrl+Shift+B</c>, the <c>ctrl</c> and <c>shift</c> keys have to be pressed, in any order, before pressing the <c>B</c> key.
/// This is the behavior usually expected with keyboard shortcuts.
///
/// This parameter can be used to bypass this behavior and allow any timing between <see cref="modifier1"/>, <see cref="modifier2"/>,
/// and <see cref="button"/>. The only requirement is for all of them to concurrently be in pressed state.
///
/// To don't depends on the setting please consider using <see cref="modifiersOrder"/> instead.
/// </remarks>
[Tooltip("Obsolete please use modifiers Order. If enabled, this will override the Input Consumption setting, allowing the modifier keys to be pressed after the button and the composite will still trigger.")]
[Obsolete("Use ModifiersOrder.Unordered with 'modifiersOrder' instead")]
public bool overrideModifiersNeedToBePressedFirst;
/// <summary>
/// Determines how a <c>modifiers</c> keys need to be pressed in order or not.
/// </summary>
public enum ModifiersOrder
{
/// <summary>
/// By default, if the setting <see cref="InputSettings.shortcutKeysConsumeInput"/> is enabled,
/// <see cref="modifier1"/> and <see cref="modifier2"/> are required to be in pressed state before or at the same
/// time that <see cref="button"/> goes into pressed state for the composite as a whole to trigger. This means that binding to,
/// for example, <c>Ctrl+Shift+B</c>, the <c>ctrl</c> and <c>shift</c> keys have to be pressed, in any order, before pressing the <c>B</c> key.
/// This is the behavior usually expected with keyboard shortcuts.
///
/// If the setting <see cref="InputSettings.shortcutKeysConsumeInput"/> is disabled,
/// modifiers can be pressed after the button and the composite will still trigger.
/// </summary>
Default = 0,
/// <summary>
/// <see cref="modifier1"/> and <see cref="modifier2"/> are required to be in pressed state before or at the same
/// time that <see cref="button"/> goes into pressed state for the composite as a whole to trigger. This means that binding to,
/// for example, <c>Ctrl+Shift+B</c>, the <c>ctrl</c> and <c>shift</c> keys have to be pressed, in any order, before pressing the <c>B</c> key.
/// This is the behavior usually expected with keyboard shortcuts.
/// </summary>
Ordered = 1,
/// <summary>
/// <see cref="modifier1"/> and/or <see cref="modifier2"/> can be pressed after <see cref="button"/>
/// and the composite will still trigger. The only requirement is for all of them to concurrently be in pressed state.
/// </summary>
Unordered = 2
}
/// <summary>
/// If set to <c>Ordered</c> or <c>Unordered</c>, the built-in logic to determine if modifiers need to be pressed first is overridden.
/// </summary>
/// <remarks>
/// By default, if the setting <see cref="InputSettings.shortcutKeysConsumeInput"/> is enabled,
/// <see cref="modifier1"/> and <see cref="modifier2"/> are required to be in pressed state before or at the same
/// time that <see cref="button"/> goes into pressed state for the composite as a whole to trigger. This means that binding to,
/// for example, <c>Ctrl+Shift+B</c>, the <c>ctrl</c> and <c>shift</c> keys have to be pressed, in any order, before pressing the <c>B</c> key.
/// This is the behavior usually expected with keyboard shortcuts.
///
/// If the setting <see cref="InputSettings.shortcutKeysConsumeInput"/> is disabled,
/// modifiers can be pressed after the button and the composite will still trigger.
///
/// This field allows you to explicitly override this default inference.
/// </remarks>
[Tooltip("By default it follows the Input Consumption setting to determine if the modifers keys need to be pressed first.")]
public ModifiersOrder modifiersOrder = ModifiersOrder.Default;
/// <summary>
/// Return the value of the <see cref="button"/> part while both <see cref="modifier1"/> and <see cref="modifier2"/>
/// are pressed. Otherwise return 0.
/// </summary>
/// <param name="context">Evaluation context passed in from the input system.</param>
/// <returns>The current value of the composite.</returns>
public override float ReadValue(ref InputBindingCompositeContext context)
{
if (ModifiersArePressed(ref context))
return context.ReadValue<float>(button);
return default;
}
private bool ModifiersArePressed(ref InputBindingCompositeContext context)
{
var modifiersDown = context.ReadValueAsButton(modifier1) && context.ReadValueAsButton(modifier2);
if (modifiersDown && modifiersOrder == ModifiersOrder.Ordered)
{
var timestamp = context.GetPressTime(button);
var timestamp1 = context.GetPressTime(modifier1);
var timestamp2 = context.GetPressTime(modifier2);
return timestamp1 <= timestamp && timestamp2 <= timestamp;
}
return modifiersDown;
}
/// <summary>
/// Same as <see cref="ReadValue"/> in this case.
/// </summary>
/// <param name="context">Evaluation context passed in from the input system.</param>
/// <returns>A >0 value if the composite is currently actuated.</returns>
public override float EvaluateMagnitude(ref InputBindingCompositeContext context)
{
return ReadValue(ref context);
}
protected override void FinishSetup(ref InputBindingCompositeContext context)
{
if (modifiersOrder == ModifiersOrder.Default)
{
// Legacy. We need to reference the obsolete member here so temporarily
// turn off the warning.
#pragma warning disable CS0618
if (overrideModifiersNeedToBePressedFirst)
#pragma warning restore CS0618
modifiersOrder = ModifiersOrder.Unordered;
else
modifiersOrder = InputSystem.settings.shortcutKeysConsumeInput ? ModifiersOrder.Ordered : ModifiersOrder.Unordered;
}
}
}
}

View File

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

View File

@@ -0,0 +1,254 @@
using System;
using System.ComponentModel;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.Scripting;
////TODO: allow making modifier optional; maybe alter the value (e.g. 0=unpressed, 0.5=pressed without modifier, 1=pressed with modifier)
namespace UnityEngine.InputSystem.Composites
{
/// <summary>
/// A binding with an additional modifier. The bound controls only trigger when
/// the modifier is pressed.
/// </summary>
/// <remarks>
/// This composite can be used to require a button to be held in order to "activate"
/// another binding. This is most commonly used on keyboards to require one of the
/// modifier keys (shift, ctrl, or alt) to be held in combination with another control,
/// e.g. "CTRL+1".
///
/// <example>
/// <code>
/// // Create a button action that triggers when CTRL+1
/// // is pressed on the keyboard.
/// var action = new InputAction(type: InputActionType.Button);
/// action.AddCompositeBinding("OneModifier")
/// .With("Modifier", "&lt;Keyboard&gt;/ctrl")
/// .With("Binding", "&lt;Keyboard&gt;/1")
/// </code>
/// </example>
///
/// However, this can also be used to "gate" other types of controls. For example, a "look"
/// action could be bound to mouse <see cref="Pointer.delta"/> such that the <see cref="Keyboard.altKey"/> on the
/// keyboard has to be pressed in order for the player to be able to look around.
///
/// <example>
/// <code>
/// lookAction.AddCompositeBinding("OneModifier")
/// .With("Modifier", "&lt;Keyboard&gt;/alt")
/// .With("Binding", "&lt;Mouse&gt;/delta")
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="TwoModifiersComposite"/>
[DisplayStringFormat("{modifier}+{binding}")]
[DisplayName("Binding With One Modifier")]
public class OneModifierComposite : InputBindingComposite
{
/// <summary>
/// Binding for the button that acts as a modifier, e.g. <c>&lt;Keyboard/ctrl</c>.
/// </summary>
/// <value>Part index to use with <see cref="InputBindingCompositeContext.ReadValue{T}(int)"/>.</value>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
// ReSharper disable once UnassignedField.Global
[InputControl(layout = "Button")] public int modifier;
/// <summary>
/// Binding for the control that is gated by the modifier. The composite will assume the value
/// of this control while the modifier is considered pressed (that is, has a magnitude equal to or
/// greater than the button press point).
/// </summary>
/// <value>Part index to use with <see cref="InputBindingCompositeContext.ReadValue{T}(int)"/>.</value>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
// ReSharper disable once UnassignedField.Global
[InputControl] public int binding;
/// <summary>
/// Type of values read from controls bound to <see cref="binding"/>.
/// </summary>
public override Type valueType => m_ValueType;
/// <summary>
/// Size of the largest value that may be read from the controls bound to <see cref="binding"/>.
/// </summary>
public override int valueSizeInBytes => m_ValueSizeInBytes;
/// <summary>
/// If set to <c>true</c>, the built-in logic to determine if modifiers need to be pressed first is overridden.
/// Default value is <c>false</c>.
/// </summary>
/// <remarks>
/// By default, if the setting <see cref="InputSettings.shortcutKeysConsumeInput"/> is enabled,
/// if <see cref="binding"/> is bound to only <see cref="Controls.ButtonControl"/>s, then the composite requires
/// <see cref="modifier"/> to be pressed <em>before</em> pressing <see cref="binding"/>. This means that binding to, for example,
/// <c>Ctrl+B</c>, the <c>ctrl</c> keys have to be pressed before pressing the <c>B</c> key. This is the behavior usually expected
/// with keyboard shortcuts.
///
/// However, when binding, for example, <c>Ctrl+MouseDelta</c>, it should be possible to press <c>ctrl</c> at any time. The default
/// logic will automatically detect the difference between this binding and the button binding in the example above and behave
/// accordingly.
///
/// This field allows you to explicitly override this default inference and make it so that regardless of what <see cref="binding"/>
/// is bound to, any press sequence is acceptable. For the example binding to <c>Ctrl+B</c>, it would mean that pressing <c>B</c> and
/// only then pressing <c>Ctrl</c> will still trigger the binding.
///
/// To don't depends on the setting please consider using <see cref="modifiersOrder"/> instead.
/// </remarks>
[Tooltip("Obsolete please use modifiers Order. If enabled, this will override the Input Consumption setting, allowing the modifier keys to be pressed after the button and the composite will still trigger.")]
[Obsolete("Use ModifiersOrder.Unordered with 'modifiersOrder' instead")]
public bool overrideModifiersNeedToBePressedFirst;
/// <summary>
/// Determines how a <c>modifiers</c> keys need to be pressed in order or not.
/// </summary>
public enum ModifiersOrder
{
/// <summary>
/// By default, if the setting <see cref="InputSettings.shortcutKeysConsumeInput"/> is enabled,
/// if <see cref="binding"/> is bound to only <see cref="Controls.ButtonControl"/>s, then the composite requires
/// <see cref="modifier"/> to be pressed <em>before</em> pressing <see cref="binding"/>. This means that binding to, for example,
/// <c>Ctrl+B</c>, the <c>ctrl</c> keys have to be pressed before pressing the <c>B</c> key. This is the behavior usually expected
/// with keyboard shortcuts.
///
/// If the setting <see cref="InputSettings.shortcutKeysConsumeInput"/> is disabled,
/// modifiers can be pressed after the button and the composite will still trigger.
/// </summary>
Default = 0,
/// <summary>
/// if <see cref="binding"/> is bound to only <see cref="Controls.ButtonControl"/>s, then the composite requires
/// <see cref="modifier"/> to be pressed <em>before</em> pressing <see cref="binding"/>. This means that binding to, for example,
/// <c>Ctrl+B</c>, the <c>ctrl</c> key have to be pressed before pressing the <c>B</c> key. This is the behavior usually expected
/// with keyboard shortcuts.
/// </summary>
Ordered = 1,
/// <summary>
/// <see cref="modifier"/> can be pressed after <see cref="binding"/>
/// and the composite will still trigger. The only requirement is for all of them to concurrently be in pressed state.
/// </summary>
Unordered = 2
}
/// <summary>
/// If set to <c>Ordered</c> or <c>Unordered</c>, the built-in logic to determine if modifiers need to be pressed first is overridden.
/// </summary>
/// <remarks>
/// By default, if the setting <see cref="InputSettings.shortcutKeysConsumeInput"/> is enabled,
/// if <see cref="binding"/> is bound to only <see cref="Controls.ButtonControl"/>s, then the composite requires
/// <see cref="modifier"/> to be pressed <em>before</em> pressing <see cref="binding"/>. This means that binding to, for example,
/// <c>Ctrl+B</c>, the <c>ctrl</c> keys have to be pressed before pressing the <c>B</c> key. This is the behavior usually expected
/// with keyboard shortcuts.
///
/// If the setting <see cref="InputSettings.shortcutKeysConsumeInput"/> is disabled,
/// modifiers can be pressed after the button and the composite will still trigger.
///
/// However, when binding, for example, <c>Ctrl+MouseDelta</c>, it should be possible to press <c>ctrl</c> at any time. The default
/// logic will automatically detect the difference between this binding and the button binding in the example above and behave
/// accordingly.
///
/// This field allows you to explicitly override this default inference and make the order mandatory or make it so that regardless of what <see cref="binding"/>
/// is bound to, any press sequence is acceptable. For the example binding to <c>Ctrl+B</c>, it would mean that pressing <c>B</c> and
/// only then pressing <c>Ctrl</c> will still trigger the binding.
/// </remarks>
[Tooltip("By default it follows the Input Consumption setting to determine if the modifers keys need to be pressed first.")]
public ModifiersOrder modifiersOrder = ModifiersOrder.Default;
private int m_ValueSizeInBytes;
private Type m_ValueType;
private bool m_BindingIsButton;
public override float EvaluateMagnitude(ref InputBindingCompositeContext context)
{
if (ModifierIsPressed(ref context))
return context.EvaluateMagnitude(binding);
return default;
}
/// <inheritdoc/>
public override unsafe void ReadValue(ref InputBindingCompositeContext context, void* buffer, int bufferSize)
{
if (ModifierIsPressed(ref context))
context.ReadValue(binding, buffer, bufferSize);
else
UnsafeUtility.MemClear(buffer, m_ValueSizeInBytes);
}
private bool ModifierIsPressed(ref InputBindingCompositeContext context)
{
var modifierDown = context.ReadValueAsButton(modifier);
// When the modifiers are gating a button, we require the modifiers to be pressed *first*.
if (modifierDown && m_BindingIsButton && modifiersOrder == ModifiersOrder.Ordered)
{
var timestamp = context.GetPressTime(binding);
var timestamp1 = context.GetPressTime(modifier);
return timestamp1 <= timestamp;
}
return modifierDown;
}
/// <inheritdoc/>
protected override void FinishSetup(ref InputBindingCompositeContext context)
{
DetermineValueTypeAndSize(ref context, binding, out m_ValueType, out m_ValueSizeInBytes, out m_BindingIsButton);
if (modifiersOrder == ModifiersOrder.Default)
{
// Legacy. We need to reference the obsolete member here so temporarily
// turn off the warning.
#pragma warning disable CS0618
if (overrideModifiersNeedToBePressedFirst)
#pragma warning restore CS0618
modifiersOrder = ModifiersOrder.Unordered;
else
modifiersOrder = InputSystem.settings.shortcutKeysConsumeInput ? ModifiersOrder.Ordered : ModifiersOrder.Unordered;
}
}
public override object ReadValueAsObject(ref InputBindingCompositeContext context)
{
if (context.ReadValueAsButton(modifier))
return context.ReadValueAsObject(binding);
return null;
}
internal static void DetermineValueTypeAndSize(ref InputBindingCompositeContext context, int part, out Type valueType, out int valueSizeInBytes, out bool isButton)
{
valueSizeInBytes = 0;
isButton = true;
Type type = null;
foreach (var control in context.controls)
{
if (control.part != part)
continue;
var controlType = control.control.valueType;
if (type == null || controlType.IsAssignableFrom(type))
type = controlType;
else if (!type.IsAssignableFrom(controlType))
type = typeof(Object);
valueSizeInBytes = Math.Max(control.control.valueSizeInBytes, valueSizeInBytes);
// *All* bound controls need to be buttons for us to classify this part as a "Button" part.
isButton &= control.control.isButton;
}
valueType = type;
}
}
}

View File

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

View File

@@ -0,0 +1,238 @@
using System;
using System.ComponentModel;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
namespace UnityEngine.InputSystem.Composites
{
/// <summary>
/// A binding with two additional modifiers modifier. The bound controls only trigger when
/// both modifiers are pressed.
/// </summary>
/// <remarks>
/// This composite can be used to require two buttons to be held in order to "activate"
/// another binding. This is most commonly used on keyboards to require two of the
/// modifier keys (shift, ctrl, or alt) to be held in combination with another control,
/// e.g. "SHIFT+CTRL+1".
///
/// <example>
/// <code>
/// // Create a button action that triggers when SHIFT+CTRL+1
/// // is pressed on the keyboard.
/// var action = new InputAction(type: InputActionType.Button);
/// action.AddCompositeBinding("TwoModifiers")
/// .With("Modifier", "&lt;Keyboard&gt;/ctrl")
/// .With("Modifier", "&lt;Keyboard&gt;/shift")
/// .With("Binding", "&lt;Keyboard&gt;/1")
/// </code>
/// </example>
///
/// However, this can also be used to "gate" other types of controls. For example, a "look"
/// action could be bound to mouse <see cref="Pointer.delta"/> such that the <see cref="Keyboard.ctrlKey"/> and
/// <see cref="Keyboard.shiftKey"/> on the keyboard have to be pressed in order for the player to be able to
/// look around.
///
/// <example>
/// <code>
/// var action = new InputAction();
/// action.AddCompositeBinding("TwoModifiers")
/// .With("Modifier1", "&lt;Keyboard&gt;/ctrl")
/// .With("Modifier2", "&lt;Keyboard&gt;/shift")
/// .With("Binding", "&lt;Mouse&gt;/delta");
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="OneModifierComposite"/>
[DisplayStringFormat("{modifier1}+{modifier2}+{binding}")]
[DisplayName("Binding With Two Modifiers")]
public class TwoModifiersComposite : InputBindingComposite
{
/// <summary>
/// Binding for the first button that acts as a modifier, e.g. <c>&lt;Keyboard/leftCtrl</c>.
/// </summary>
/// <value>Part index to use with <see cref="InputBindingCompositeContext.ReadValue{T}(int)"/>.</value>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
// ReSharper disable once UnassignedField.Global
[InputControl(layout = "Button")] public int modifier1;
/// <summary>
/// Binding for the second button that acts as a modifier, e.g. <c>&lt;Keyboard/leftCtrl</c>.
/// </summary>
/// <value>Part index to use with <see cref="InputBindingCompositeContext.ReadValue{T}(int)"/>.</value>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
// ReSharper disable once UnassignedField.Global
[InputControl(layout = "Button")] public int modifier2;
/// <summary>
/// Binding for the control that is gated by <see cref="modifier1"/> and <see cref="modifier2"/>.
/// The composite will assume the value of this button while both of the modifiers are pressed.
/// </summary>
/// <value>Part index to use with <see cref="InputBindingCompositeContext.ReadValue{T}(int)"/>.</value>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
// ReSharper disable once UnassignedField.Global
[InputControl] public int binding;
/// <summary>
/// If set to <c>true</c>, the built-in logic to determine if modifiers need to be pressed first is overridden.
/// Default value is <c>false</c>.
/// </summary>
/// <remarks>
/// By default, if the setting <see cref="InputSettings.shortcutKeysConsumeInput"/> is enabled,
/// if <see cref="binding"/> is bound to only <see cref="Controls.ButtonControl"/>s, then the composite requires
/// both <see cref="modifier1"/> and <see cref="modifier2"/> to be pressed <em>before</em> pressing <see cref="binding"/>.
/// This means that binding to, for example, <c>Ctrl+Shift+B</c>, the <c>ctrl</c> and <c>shift</c> keys have to be pressed, in any order,
/// before pressing the <c>B</c> key. This is the behavior usually expected with keyboard shortcuts.
///
/// However, when binding, for example, <c>Ctrl+Shift+MouseDelta</c>, it should be possible to press <c>ctrl</c> and <c>shift</c>
/// at any time and in any order. The default logic will automatically detect the difference between this binding and the button
/// binding in the example above and behave accordingly.
///
/// This field allows you to explicitly override this default inference and make it so that regardless of what <see cref="binding"/>
/// is bound to, any press sequence is acceptable. For the example binding to <c>Ctrl+Shift+B</c>, it would mean that pressing
/// <c>B</c> and only then pressing <c>Ctrl</c> and <c>Shift</c> will still trigger the binding.
///
/// To don't depends on the setting please consider using <see cref="modifiersOrder"/> instead.
/// </remarks>
[Tooltip("Obsolete please use modifiers Order. If enabled, this will override the Input Consumption setting, allowing the modifier keys to be pressed after the button and the composite will still trigger.")]
[Obsolete("Use ModifiersOrder.Unordered with 'modifiersOrder' instead")]
public bool overrideModifiersNeedToBePressedFirst;
/// <summary>
/// Determines how a <c>modifiers</c> keys need to be pressed in order or not.
/// </summary>
public enum ModifiersOrder
{
/// <summary>
/// By default, if the setting <see cref="InputSettings.shortcutKeysConsumeInput"/> is enabled,
/// if <see cref="binding"/> is bound to only <see cref="Controls.ButtonControl"/>s, then the composite requires
/// both <see cref="modifier1"/> and <see cref="modifier2"/> to be pressed <em>before</em> pressing <see cref="binding"/>.
/// This means that binding to, for example, <c>Ctrl+Shift+B</c>, the <c>ctrl</c> and <c>shift</c> keys have to be pressed, in any order,
/// before pressing the <c>B</c> key. This is the behavior usually expected with keyboard shortcuts.
///
/// If the setting <see cref="InputSettings.shortcutKeysConsumeInput"/> is disabled,
/// modifiers can be pressed after the button and the composite will still trigger.
/// </summary>
Default = 0,
/// <summary>
/// if <see cref = "binding" /> is bound to only <see cref = "Controls.ButtonControl" /> s, then the composite requires
/// both <see cref="modifier1"/> and <see cref="modifier2"/> to be pressed <em>before</em> pressing <see cref="binding"/>.
/// This means that binding to, for example, <c>Ctrl+Shift+B</c>, the <c>ctrl</c> and <c>shift</c> keys have to be pressed, in any order,
/// before pressing the <c>B</c> key. This is the behavior usually expected with keyboard shortcuts.
/// </summary>
Ordered = 1,
/// <summary>
/// <see cref="modifier1"/> and/or <see cref="modifier2"/> can be pressed after <see cref="binding"/>
/// and the composite will still trigger. The only requirement is for all of them to concurrently be in pressed state.
/// </summary>
Unordered = 2
}
/// <summary>
/// If set to <c>Ordered</c> or <c>Unordered</c>, the built-in logic to determine if modifiers need to be pressed first is overridden.
/// </summary>
/// <remarks>
/// By default, if the setting <see cref="InputSettings.shortcutKeysConsumeInput"/> is enabled,
/// if <see cref="binding"/> is bound to only <see cref="Controls.ButtonControl"/>s, then the composite requires
/// both <see cref="modifier1"/> and <see cref="modifier2"/> to be pressed <em>before</em> pressing <see cref="binding"/>.
/// This means that binding to, for example, <c>Ctrl+Shift+B</c>, the <c>ctrl</c> and <c>shift</c> keys have to be pressed, in any order,
/// before pressing the <c>B</c> key. This is the behavior usually expected with keyboard shortcuts.
///
/// If the setting <see cref="InputSettings.shortcutKeysConsumeInput"/> is disabled,
/// modifiers can be pressed after the button and the composite will still trigger.
///
/// This field allows you to explicitly override this default inference and make the order mandatory or make it so that regardless of what <see cref="binding"/>
/// is bound to, any press sequence is acceptable. For the example binding to <c>Ctrl+Shift+B</c>, it would mean that pressing
/// <c>B</c> and only then pressing <c>Ctrl</c> and <c>Shift</c> will still trigger the binding.
///
/// </remarks>
[Tooltip("By default it follows the Input Consumption setting to determine if the modifers keys need to be pressed first.")]
public ModifiersOrder modifiersOrder = ModifiersOrder.Default;
/// <summary>
/// Type of values read from controls bound to <see cref="binding"/>.
/// </summary>
public override Type valueType => m_ValueType;
/// <summary>
/// Size of the largest value that may be read from the controls bound to <see cref="binding"/>.
/// </summary>
public override int valueSizeInBytes => m_ValueSizeInBytes;
private int m_ValueSizeInBytes;
private Type m_ValueType;
private bool m_BindingIsButton;
public override float EvaluateMagnitude(ref InputBindingCompositeContext context)
{
if (ModifiersArePressed(ref context))
return context.EvaluateMagnitude(binding);
return default;
}
/// <inheritdoc/>
public override unsafe void ReadValue(ref InputBindingCompositeContext context, void* buffer, int bufferSize)
{
if (ModifiersArePressed(ref context))
context.ReadValue(binding, buffer, bufferSize);
else
UnsafeUtility.MemClear(buffer, m_ValueSizeInBytes);
}
private bool ModifiersArePressed(ref InputBindingCompositeContext context)
{
var modifiersDown = context.ReadValueAsButton(modifier1) && context.ReadValueAsButton(modifier2);
// When the modifiers are gating a button, we require the modifiers to be pressed *first*.
if (modifiersDown && m_BindingIsButton && modifiersOrder == ModifiersOrder.Ordered)
{
var timestamp = context.GetPressTime(binding);
var timestamp1 = context.GetPressTime(modifier1);
var timestamp2 = context.GetPressTime(modifier2);
return timestamp1 <= timestamp && timestamp2 <= timestamp;
}
return modifiersDown;
}
/// <inheritdoc/>
protected override void FinishSetup(ref InputBindingCompositeContext context)
{
OneModifierComposite.DetermineValueTypeAndSize(ref context, binding, out m_ValueType, out m_ValueSizeInBytes, out m_BindingIsButton);
if (modifiersOrder == ModifiersOrder.Default)
{
// Legacy. We need to reference the obsolete member here so temporarily
// turn off the warning.
#pragma warning disable CS0618
if (overrideModifiersNeedToBePressedFirst)
#pragma warning restore CS0618
modifiersOrder = ModifiersOrder.Unordered;
else
modifiersOrder = InputSystem.settings.shortcutKeysConsumeInput ? ModifiersOrder.Ordered : ModifiersOrder.Unordered;
}
}
public override object ReadValueAsObject(ref InputBindingCompositeContext context)
{
if (context.ReadValueAsButton(modifier1) && context.ReadValueAsButton(modifier2))
return context.ReadValueAsObject(binding);
return null;
}
}
}

View File

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

View File

@@ -0,0 +1,220 @@
using System;
using System.ComponentModel;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
#if UNITY_EDITOR
using UnityEngine.InputSystem.Editor;
using UnityEngine.UIElements;
#endif
////TODO: add support for ramp up/down
namespace UnityEngine.InputSystem.Composites
{
/// <summary>
/// A 2D planar motion vector computed from an up+down button pair and a left+right
/// button pair.
/// </summary>
/// <remarks>
/// This composite allows to grab arbitrary buttons from a device and arrange them in
/// a D-Pad like configuration. Based on button presses, the composite will return a
/// normalized direction vector (normalization can be turned off via <see cref="mode"/>).
///
/// Opposing motions cancel each other out. This means that if, for example, both the left
/// and right horizontal button are pressed, the resulting horizontal movement value will
/// be zero.
///
/// <example>
/// <code>
/// // Set up WASD style keyboard controls.
/// action.AddCompositeBinding("2DVector")
/// .With("Up", "&lt;Keyboard&gt;/w")
/// .With("Left", "&lt;Keyboard&gt;/a")
/// .With("Down", "&lt;Keyboard&gt;/s")
/// .With("Right", "&lt;Keyboard&gt;/d");
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="Vector3Composite"/>
[DisplayStringFormat("{up}/{left}/{down}/{right}")] // This results in WASD.
[DisplayName("Up/Down/Left/Right Composite")]
public class Vector2Composite : InputBindingComposite<Vector2>
{
/// <summary>
/// Binding for the button that represents the up (that is, <c>(0,1)</c>) direction of the vector.
/// </summary>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
[InputControl(layout = "Axis")] public int up;
/// <summary>
/// Binding for the button represents the down (that is, <c>(0,-1)</c>) direction of the vector.
/// </summary>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
[InputControl(layout = "Axis")] public int down;
/// <summary>
/// Binding for the button represents the left (that is, <c>(-1,0)</c>) direction of the vector.
/// </summary>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
[InputControl(layout = "Axis")] public int left;
/// <summary>
/// Binding for the button that represents the right (that is, <c>(1,0)</c>) direction of the vector.
/// </summary>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
[InputControl(layout = "Axis")] public int right;
[Obsolete("Use Mode.DigitalNormalized with 'mode' instead")]
public bool normalize = true;
/// <summary>
/// How to synthesize a <c>Vector2</c> from the values read from <see cref="up"/>, <see cref="down"/>,
/// <see cref="left"/>, and <see cref="right"/>.
/// </summary>
/// <value>Determines how X and Y of the resulting <c>Vector2</c> are formed from input values.</value>
/// <remarks>
/// <example>
/// <code>
/// var action = new InputAction();
///
/// // DigitalNormalized composite (the default). Turns gamepad left stick into
/// // control equivalent to the D-Pad.
/// action.AddCompositeBinding("2DVector(mode=0)")
/// .With("up", "&lt;Gamepad&gt;/leftStick/up")
/// .With("down", "&lt;Gamepad&gt;/leftStick/down")
/// .With("left", "&lt;Gamepad&gt;/leftStick/left")
/// .With("right", "&lt;Gamepad&gt;/leftStick/right");
///
/// // Digital composite. Turns gamepad left stick into control equivalent
/// // to the D-Pad except that diagonals will not be normalized.
/// action.AddCompositeBinding("2DVector(mode=1)")
/// .With("up", "&lt;Gamepad&gt;/leftStick/up")
/// .With("down", "&lt;Gamepad&gt;/leftStick/down")
/// .With("left", "&lt;Gamepad&gt;/leftStick/left")
/// .With("right", "&lt;Gamepad&gt;/leftStick/right");
///
/// // Analog composite. In this case results in setup that behaves exactly
/// // the same as leftStick already does. But you could use it, for example,
/// // to swap directions by binding "up" to leftStick/down and "down" to
/// // leftStick/up.
/// action.AddCompositeBinding("2DVector(mode=2)")
/// .With("up", "&lt;Gamepad&gt;/leftStick/up")
/// .With("down", "&lt;Gamepad&gt;/leftStick/down")
/// .With("left", "&lt;Gamepad&gt;/leftStick/left")
/// .With("right", "&lt;Gamepad&gt;/leftStick/right");
/// </code>
/// </example>
/// </remarks>
public Mode mode;
/// <inheritdoc />
public override Vector2 ReadValue(ref InputBindingCompositeContext context)
{
var mode = this.mode;
if (mode == Mode.Analog)
{
var upValue = context.ReadValue<float>(up);
var downValue = context.ReadValue<float>(down);
var leftValue = context.ReadValue<float>(left);
var rightValue = context.ReadValue<float>(right);
return DpadControl.MakeDpadVector(upValue, downValue, leftValue, rightValue);
}
var upIsPressed = context.ReadValueAsButton(up);
var downIsPressed = context.ReadValueAsButton(down);
var leftIsPressed = context.ReadValueAsButton(left);
var rightIsPressed = context.ReadValueAsButton(right);
// Legacy. We need to reference the obsolete member here so temporarily
// turn off the warning.
#pragma warning disable CS0618
if (!normalize) // Was on by default.
mode = Mode.Digital;
#pragma warning restore CS0618
return DpadControl.MakeDpadVector(upIsPressed, downIsPressed, leftIsPressed, rightIsPressed, mode == Mode.DigitalNormalized);
}
/// <inheritdoc />
public override float EvaluateMagnitude(ref InputBindingCompositeContext context)
{
var value = ReadValue(ref context);
return value.magnitude;
}
/// <summary>
/// Determines how a <c>Vector2</c> is synthesized from part controls.
/// </summary>
public enum Mode
{
/// <summary>
/// Part controls are treated as analog meaning that the floating-point values read from controls
/// will come through as is (minus the fact that the down and left direction values are negated).
/// </summary>
Analog = 2,
/// <summary>
/// Part controls are treated as buttons (on/off) and the resulting vector is normalized. This means
/// that if, for example, both left and up are pressed, instead of returning a vector (-1,1), a vector
/// of roughly (-0.7,0.7) (that is, corresponding to <c>new Vector2(-1,1).normalized</c>) is returned instead.
/// The resulting 2D area is diamond-shaped.
/// </summary>
DigitalNormalized = 0,
/// <summary>
/// Part controls are treated as buttons (on/off) and the resulting vector is not normalized. This means
/// that if, for example, both left and up are pressed, the resulting vector is (-1,1) and has a length
/// greater than 1. The resulting 2D area is box-shaped.
/// </summary>
Digital = 1
}
}
#if UNITY_EDITOR
internal class Vector2CompositeEditor : InputParameterEditor<Vector2Composite>
{
private const string label = "Mode";
private const string tooltipText = "How to synthesize a Vector2 from the inputs. Digital "
+ "treats part bindings as buttons (on/off) whereas Analog preserves "
+ "floating-point magnitudes as read from controls.";
public override void OnGUI()
{
}
public override void OnDrawVisualElements(VisualElement root, Action onChangedCallback)
{
var modeField = new EnumField(label, target.mode)
{
tooltip = tooltipText
};
modeField.RegisterValueChangedCallback(evt =>
{
target.mode = (Vector2Composite.Mode)evt.newValue;
onChangedCallback();
});
root.Add(modeField);
}
}
#endif
}

View File

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

View File

@@ -0,0 +1,200 @@
using System;
using System.ComponentModel;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
#if UNITY_EDITOR
using UnityEngine.InputSystem.Editor;
using UnityEngine.UIElements;
#endif
namespace UnityEngine.InputSystem.Composites
{
/// <summary>
/// A 3D vector formed from six floating-point inputs.
/// </summary>
/// <remarks>
/// Depending on the setting of <see cref="mode"/>, the vector is either in the [-1..1]
/// range on each axis (normalized or not depending on <see cref="mode"/>) or is in the
/// full value range of the input controls.
///
/// <example>
/// <code>
/// action.AddCompositeBinding("3DVector")
/// .With("Forward", "&lt;Keyboard&gt;/w")
/// .With("Backward", "&lt;Keyboard&gt;/s")
/// .With("Left", "&lt;Keyboard&gt;/a")
/// .With("Right", "&lt;Keyboard&gt;/d")
/// .With("Up", "&lt;Keyboard&gt;/q")
/// .With("Down", "&lt;Keyboard&gt;/e");
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="Vector2Composite"/>
[DisplayStringFormat("{up}+{down}/{left}+{right}/{forward}+{backward}")]
[DisplayName("Up/Down/Left/Right/Forward/Backward Composite")]
public class Vector3Composite : InputBindingComposite<Vector3>
{
/// <summary>
/// Binding for the button that represents the up (that is, <c>(0,1,0)</c>) direction of the vector.
/// </summary>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
[InputControl(layout = "Axis")] public int up;
/// <summary>
/// Binding for the button that represents the down (that is, <c>(0,-1,0)</c>) direction of the vector.
/// </summary>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
[InputControl(layout = "Axis")] public int down;
/// <summary>
/// Binding for the button that represents the left (that is, <c>(-1,0,0)</c>) direction of the vector.
/// </summary>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
[InputControl(layout = "Axis")] public int left;
/// <summary>
/// Binding for the button that represents the right (that is, <c>(1,0,0)</c>) direction of the vector.
/// </summary>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
[InputControl(layout = "Axis")] public int right;
/// <summary>
/// Binding for the button that represents the right (that is, <c>(0,0,1)</c>) direction of the vector.
/// </summary>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
[InputControl(layout = "Axis")] public int forward;
/// <summary>
/// Binding for the button that represents the right (that is, <c>(0,0,-1)</c>) direction of the vector.
/// </summary>
/// <remarks>
/// This property is automatically assigned by the input system.
/// </remarks>
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once FieldCanBeMadeReadOnly.Global
[InputControl(layout = "Axis")] public int backward;
/// <summary>
/// How to synthesize a <c>Vector3</c> from the values read from <see cref="up"/>, <see cref="down"/>,
/// <see cref="left"/>, <see cref="right"/>, <see cref="forward"/>, and <see cref="backward"/>.
/// </summary>
/// <value>Determines how X, Y, and Z of the resulting <c>Vector3</c> are formed from input values.</value>
public Mode mode = Mode.Analog;
/// <inheritdoc/>
public override Vector3 ReadValue(ref InputBindingCompositeContext context)
{
if (mode == Mode.Analog)
{
var upValue = context.ReadValue<float>(up);
var downValue = context.ReadValue<float>(down);
var leftValue = context.ReadValue<float>(left);
var rightValue = context.ReadValue<float>(right);
var forwardValue = context.ReadValue<float>(forward);
var backwardValue = context.ReadValue<float>(backward);
return new Vector3(rightValue - leftValue, upValue - downValue, forwardValue - backwardValue);
}
else
{
var upValue = context.ReadValueAsButton(up) ? 1f : 0f;
var downValue = context.ReadValueAsButton(down) ? -1f : 0f;
var leftValue = context.ReadValueAsButton(left) ? -1f : 0f;
var rightValue = context.ReadValueAsButton(right) ? 1f : 0f;
var forwardValue = context.ReadValueAsButton(forward) ? 1f : 0f;
var backwardValue = context.ReadValueAsButton(backward) ? -1f : 0f;
var vector = new Vector3(leftValue + rightValue, upValue + downValue, forwardValue + backwardValue);
if (mode == Mode.DigitalNormalized)
vector = vector.normalized;
return vector;
}
}
/// <inheritdoc />
public override float EvaluateMagnitude(ref InputBindingCompositeContext context)
{
var value = ReadValue(ref context);
return value.magnitude;
}
/// <summary>
/// Determines how a <c>Vector3</c> is synthesized from part controls.
/// </summary>
public enum Mode
{
/// <summary>
/// Part controls are treated as analog meaning that the floating-point values read from controls
/// will come through as is (minus the fact that the down and left direction values are negated).
/// </summary>
Analog,
/// <summary>
/// Part controls are treated as buttons (on/off) and the resulting vector is normalized. This means
/// that if, for example, both left and up are pressed, instead of returning a vector (-1,1,0), a vector
/// of roughly (-0.7,0.7,0) (that is, corresponding to <c>new Vector3(-1,1,0).normalized</c>) is returned instead.
/// </summary>
DigitalNormalized,
/// <summary>
/// Part controls are treated as buttons (on/off) and the resulting vector is not normalized. This means
/// that if both left and up are pressed, for example, the resulting vector is (-1,1,0) and has a length
/// greater than 1.
/// </summary>
Digital,
}
}
#if UNITY_EDITOR
internal class Vector3CompositeEditor : InputParameterEditor<Vector3Composite>
{
private const string label = "Mode";
private const string tooltip = "How to synthesize a Vector3 from the inputs. Digital "
+ "treats part bindings as buttons (on/off) whereas Analog preserves "
+ "floating-point magnitudes as read from controls.";
public override void OnGUI()
{
}
public override void OnDrawVisualElements(VisualElement root, Action onChangedCallback)
{
var modeField = new EnumField(label, target.mode)
{
tooltip = tooltip
};
modeField.RegisterValueChangedCallback(evt =>
{
target.mode = (Vector3Composite.Mode)evt.newValue;
onChangedCallback();
});
root.Add(modeField);
}
}
#endif
}

View File

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

View File

@@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using UnityEngine.InputSystem.Utilities;
////TODO: move indexer up here
namespace UnityEngine.InputSystem
{
/// <summary>
/// A collection of input actions (see <see cref="InputAction"/>).
/// </summary>
/// <seealso cref="InputActionMap"/>
/// <seealso cref="InputActionAsset"/>
public interface IInputActionCollection : IEnumerable<InputAction>
{
/// <summary>
/// Optional mask applied to all bindings in the collection.
/// </summary>
/// <remarks>
/// If this is not null, only bindings that match the mask will be used.
///
/// Modifying this property while any of the actions in the collection are enabled will
/// lead to the actions getting disabled temporarily and then re-enabled.
/// </remarks>
InputBinding? bindingMask { get; set; }
////REVIEW: should this allow restricting to a set of controls instead of confining it to just devices?
/// <summary>
/// Devices to use with the actions in this collection.
/// </summary>
/// <remarks>
/// If this is set, actions in the collection will exclusively bind to devices
/// in the given list. For example, if two gamepads are present in the system yet
/// only one gamepad is listed here, then a "&lt;Gamepad&gt;/leftStick" binding will
/// only bind to the gamepad in the list and not to the one that is only available
/// globally.
///
/// Modifying this property after bindings in the collection have already been resolved,
/// will lead to <see cref="InputAction.controls"/> getting refreshed. If any of the actions
/// in the collection are currently in progress (see <see cref="InputAction.phase"/>),
/// the actions will remain unaffected and in progress except if the controls currently
/// driving them (see <see cref="InputAction.activeControl"/>) are no longer part of any
/// of the selected devices. In that case, the action is <see cref="InputAction.canceled"/>.
/// </remarks>
ReadOnlyArray<InputDevice>? devices { get; set; }
/// <summary>
/// List of control schemes defined for the set of actions.
/// </summary>
/// <remarks>
/// Control schemes are optional and the list may be empty.
/// </remarks>
ReadOnlyArray<InputControlScheme> controlSchemes { get; }
/// <summary>
/// Check whether the given action is contained in this collection.
/// </summary>
/// <param name="action">An arbitrary input action.</param>
/// <returns>True if the given action is contained in the collection, false if not.</returns>
/// <remarks>
/// Calling this method will not allocate GC memory (unlike when iterating generically
/// over the collection). Also, a collection may have a faster containment check rather than
/// having to search through all its actions.
/// </remarks>
bool Contains(InputAction action);
/// <summary>
/// Enable all actions in the collection.
/// </summary>
/// <seealso cref="InputAction.Enable"/>
/// <seealso cref="InputAction.enabled"/>
void Enable();
/// <summary>
/// Disable all actions in the collection.
/// </summary>
/// <seealso cref="InputAction.Disable"/>
/// <seealso cref="InputAction.enabled"/>
void Disable();
}
/// <summary>
/// An extended version of <see cref="IInputActionCollection"/>.
/// </summary>
/// <remarks>
/// This interface will be merged into <see cref="IInputActionCollection"/> in a future (major) version.
/// </remarks>
public interface IInputActionCollection2 : IInputActionCollection
{
/// <summary>
/// Iterate over all bindings in the collection of actions.
/// </summary>
/// <seealso cref="InputActionMap.bindings"/>
/// <seealso cref="InputAction.bindings"/>
/// <seealso cref="InputActionAsset.bindings"/>
IEnumerable<InputBinding> bindings { get; }
/// <summary>
/// Find an <see cref="InputAction"/> in the collection by its <see cref="InputAction.name"/> or
/// by its <see cref="InputAction.id"/> (in string form).
/// </summary>
/// <param name="actionNameOrId">Name of the action as either a "map/action" combination (e.g. "gameplay/fire") or
/// a simple name. In the former case, the name is split at the '/' slash and the first part is used to find
/// a map with that name and the second part is used to find an action with that name inside the map. In the
/// latter case, all maps are searched in order and the first action that has the given name in any of the maps
/// is returned. Note that name comparisons are case-insensitive.
///
/// Alternatively, the given string can be a GUID as given by <see cref="InputAction.id"/>.</param>
/// <param name="throwIfNotFound">If <c>true</c>, instead of returning <c>null</c> when the action
/// cannot be found, throw <c>ArgumentException</c>.</param>
/// <returns>The action with the corresponding name or <c>null</c> if no matching action could be found.</returns>
/// <exception cref="ArgumentNullException"><paramref name="actionNameOrId"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Thrown if <paramref name="throwIfNotFound"/> is true and the
/// action could not be found. -Or- If <paramref name="actionNameOrId"/> contains a slash but is missing
/// either the action or the map name.</exception>
InputAction FindAction(string actionNameOrId, bool throwIfNotFound = false);
/// <summary>
/// Find the index of the first binding that matches the given mask.
/// </summary>
/// <param name="mask">A binding. See <see cref="InputBinding.Matches"/> for details.</param>
/// <param name="action">Receives the action on which the binding was found. If none was found,
/// will be set to <c>null</c>.</param>
/// <returns>Index into <see cref="InputAction.bindings"/> of <paramref name="action"/> of the binding
/// that matches <paramref name="mask"/>. If no binding matches, will return -1.</returns>
/// <remarks>
/// For details about matching bindings by a mask, see <see cref="InputBinding.Matches"/>.
///
/// <example>
/// <code>
/// var index = playerInput.actions.FindBinding(
/// new InputBinding { path = "&lt;Gamepad&gt;/buttonSouth" },
/// out var action);
///
/// if (index != -1)
/// Debug.Log($"The A button is bound to {action}");
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputBinding.Matches"/>
/// <seealso cref="bindings"/>
int FindBinding(InputBinding mask, out InputAction action);
}
}

View File

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

View File

@@ -0,0 +1,337 @@
using System;
using System.ComponentModel;
using System.Reflection;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.Scripting;
// [GESTURES]
// Idea for v2 of the input system:
// Separate interaction *recognition* from interaction *representation*
// This will likely also "solve" gestures
//
// ATM, an interaction is a prebuilt thing that rolls recognition and representation of an interaction into
// one single thing. That limits how powerful this can be. There's only ever one interaction coming from each interaction
// added to a setup.
//
// A much more powerful way would be to have the interactions configured on actions and bindings add *recognizers*
// which then *generate* interactions. This way, a single recognizer could spawn arbitrary many interactions. What the
// recognizer is attached to (the bindings) would simply act as triggers. Beyond that, the recognizer would have
// plenty freedom to start, perform, and stop interactions happening in response to input.
//
// It'll likely be a breaking change as far as user-implemented interactions go but at least the data as it looks today
// should work with this just fine.
////TODO: allow interactions to be constrained to a specific InputActionType
////TODO: add way for parameters on interactions and processors to be driven from global value source that is NOT InputSettings
//// (ATM it's very hard to e.g. have a scale value on gamepad stick bindings which is determined dynamically from player
//// settings in the game)
////REVIEW: what about putting an instance of one of these on every resolved control instead of sharing it between all controls resolved from a binding?
////REVIEW: can we have multiple interactions work together on the same binding? E.g. a 'Press' causing a start and a 'Release' interaction causing a performed
////REVIEW: have a default interaction so that there *always* is an interaction object when processing triggers?
namespace UnityEngine.InputSystem
{
/// <summary>
/// Interface for interaction patterns that drive actions.
/// </summary>
/// <remarks>
/// Actions have a built-in interaction pattern that to some extent depends on their type (<see
/// cref="InputActionType"/>, <see cref="InputAction.type"/>). What this means is that when controls
/// bound to an action are actuated, the action will initiate an interaction that in turn determines
/// when <see cref="InputAction.started"/>, <see cref="InputAction.performed"/>, and <see cref="InputAction.canceled"/>
/// are called.
///
/// The default interaction (that is, when no interaction has been added to a binding or the
/// action that the binding targets) will generally start and perform an action as soon as a control
/// is actuated, then perform the action whenever the value of the control changes except if the value
/// changes back to the default in which case the action is cancelled.
///
/// By writing custom interactions, it is possible to implement different interactions. For example,
/// <see cref="Interactions.HoldInteraction"/> will only start when a control is being actuated but
/// will only perform the action if the control is held for a minimum amount of time.
///
/// Interactions can be stateful and mutate state over time. In fact, interactions will usually
/// represent miniature state machines driven directly by input.
///
/// Multiple interactions can be applied to the same binding. The interactions will be processed in
/// sequence. However, the first interaction that starts the action will get to drive the state of
/// the action. If it performs the action, all interactions are reset. If it cancels, the first
/// interaction in the list that is in started state will get to take over and drive the action.
///
/// This makes it possible to have several interaction patterns on the same action. For example,
/// to have a "fire" action that allows for charging, one can have a "Hold" and a "Press" interaction
/// in sequence on the action.
///
/// <example>
/// <code>
/// // Create a fire action with two interactions:
/// // 1. Hold. Triggers charged firing. Has to come first as otherwise "Press" will immediately perform the action.
/// // 2. Press. Triggers instant firing.
/// // NOTE: An alternative is to use "Tap;Hold", that is, a "Tap" first and then a "Hold". The difference
/// // is relatively minor. In this setup, the "Tap" turns into a "Hold" if the button is held for
/// // longer than the tap time whereas in the setup below, the "Hold" turns into a "Press" if the
/// // button is released before the hold time has been reached.
/// var fireAction = new InputAction(type: InputActionType.Button, interactions: "Hold;Press");
/// fireAction.AddBinding("&lt;Gamepad&gt;/buttonSouth");
/// </code>
/// </example>
///
/// Custom interactions are automatically registered by reflection but it can also be manually registered using <see cref="InputSystem.RegisterInteraction"/>. This can be
/// done at any point during or after startup but has to be done before actions that reference the interaction
/// are enabled or have their controls queried. A good point is usually to do it during loading like so:
///
/// <example>
/// <code>
/// #if UNITY_EDITOR
/// [InitializeOnLoad]
/// #endif
/// public class MyInteraction : IInputInteraction
/// {
/// public void Process(ref InputInteractionContext context)
/// {
/// // ...
/// }
///
/// public void Reset()
/// {
/// }
///
/// static MyInteraction()
/// {
/// InputSystem.RegisterInteraction&lt;MyInteraction&gt;();
/// }
///
/// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
/// private static void Initialize()
/// {
/// // Will execute the static constructor as a side effect.
/// }
/// }
/// </code>
/// </example>
///
/// If your interaction will only work with a specific type of value (e.g. <c>float</c>), it is better
/// to base the implementation on <see cref="IInputInteraction{TValue}"/> instead. While the interface is the
/// same, the type parameter communicates to the input system that only controls that have compatible value
/// types should be used with your interaction.
///
/// Interactions, like processors (<see cref="InputProcessor"/>) and binding composites (<see cref="InputBindingComposite"/>)
/// may define their own parameters which can then be configured through the editor UI or set programmatically in
/// code. To define a parameter, add a public field to your class that has either a <c>bool</c>, an <c>int</c>,
/// a <c>float</c>, or an <c>enum</c> type. To set defaults for the parameters, assign default values
/// to the fields.
///
/// <example>
/// <code>
/// public class MyInteraction : IInputInteraction
/// {
/// public bool boolParameter;
/// public int intParameter;
/// public float floatParameter;
/// public MyEnum enumParameter = MyEnum.C; // Custom default.
///
/// public enum MyEnum
/// {
/// A,
/// B,
/// C
/// }
///
/// public void Process(ref InputInteractionContext context)
/// {
/// // ...
/// }
///
/// public void Reset()
/// {
/// }
/// }
///
/// // The parameters can be configured graphically in the editor or set programmatically in code.
/// // NOTE: Enum parameters are represented by their integer values. However, when setting enum parameters
/// // graphically in the UI, they will be presented as a dropdown using the available enum values.
/// var action = new InputAction(interactions: "MyInteraction(boolParameter=true,intParameter=1,floatParameter=1.2,enumParameter=1);
/// </code>
/// </example>
///
/// A default UI will be presented in the editor UI to configure the parameters of your interaction.
/// You can customize this by replacing the default UI with a custom implementation using <see cref="Editor.InputParameterEditor"/>.
/// This mechanism is the same as for processors and binding composites.
///
/// <example>
/// <code>
/// #if UNITY_EDITOR
/// public class MyCustomInteractionEditor : InputParameterEditor&lt;MyCustomInteraction&gt;
/// {
/// protected override void OnEnable()
/// {
/// // Do any setup work you need.
/// }
///
/// protected override void OnGUI()
/// {
/// // Use standard Unity UI calls do create your own parameter editor UI.
/// }
/// }
/// #endif
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputSystem.RegisterInteraction"/>
/// <seealso cref="InputBinding.interactions"/>
/// <seealso cref="InputAction.interactions"/>
/// <seealso cref="Editor.InputParameterEditor"/>
/// <seealso cref="InputActionRebindingExtensions.GetParameterValue(InputAction,string,InputBinding)"/>
/// <seealso cref="InputActionRebindingExtensions.ApplyParameterOverride(InputActionMap,string,PrimitiveValue,InputBinding)"/>
public interface IInputInteraction
{
/// <summary>
/// Perform processing of the interaction in response to input.
/// </summary>
/// <param name="context"></param>
/// <remarks>
/// This method is called whenever a control referenced in the binding that the interaction sits on
/// changes value. The interaction is expected to process the value change and, if applicable, call
/// <see cref="InputInteractionContext.Started"/> and/or its related methods to initiate a state change.
///
/// Note that if "control disambiguation" (i.e. the process where if multiple controls are bound to
/// the same action, the system decides which control gets to drive the action at any one point) is
/// in effect -- i.e. when either <see cref="InputActionType.Button"/> or <see cref="InputActionType.Value"/>
/// are used but not if <see cref="InputActionType.PassThrough"/> is used -- inputs that the disambiguation
/// chooses to ignore will cause this method to not be called.
///
/// Note that this method is called on the interaction even when there are multiple interactions
/// and the interaction is not the one currently in control of the action (because another interaction
/// that comes before it in the list had already started the action). Each interaction will get
/// processed independently and the action will decide when to use which interaction to drive the
/// action as a whole.
///
/// <example>
/// <code>
/// // Processing for an interaction that will perform the action only if a control
/// // is held at least at 3/4 actuation for at least 1 second.
/// public void Process(ref InputInteractionContext context)
/// {
/// var control = context.control;
///
/// // See if we're currently tracking a control.
/// if (m_Control != null)
/// {
/// // Ignore any input on a control we're not currently tracking.
/// if (m_Control != control)
/// return;
///
/// // Check if the control is currently actuated past our 3/4 threshold.
/// var isStillActuated = context.ControlIsActuated(0.75f);
///
/// // See for how long the control has been held.
/// var actuationTime = context.time - context.startTime;
///
/// if (!isStillActuated)
/// {
/// // Control is no longer actuated above 3/4 threshold. If it was held
/// // for at least a second, perform the action. Otherwise cancel it.
///
/// if (actuationTime >= 1)
/// context.Performed();
/// else
/// context.Canceled();
/// }
///
/// // Control changed value somewhere above 3/4 of its actuation. Doesn't
/// // matter to us so no change.
/// }
/// else
/// {
/// // We're not already tracking a control. See if the control that just triggered
/// // is actuated at least 3/4th of its way. If so, start tracking it.
///
/// var isActuated = context.ControlIsActuated(0.75f);
/// if (isActuated)
/// {
/// m_Control = context.control;
/// context.Started();
/// }
/// }
/// }
///
/// InputControl m_Control;
///
/// public void Reset()
/// {
/// m_Control = null;
/// }
/// </code>
/// </example>
/// </remarks>
void Process(ref InputInteractionContext context);
/// <summary>
/// Reset state that the interaction may hold. This should put the interaction back in its original
/// state equivalent to no input yet having been received.
/// </summary>
void Reset();
}
/// <summary>
/// Identical to <see cref="IInputInteraction"/> except that it allows an interaction to explicitly
/// advertise the value it expects.
/// </summary>
/// <typeparam name="TValue">Type of values expected by the interaction</typeparam>
/// <remarks>
/// Advertising the value type will an interaction type to be filtered out in the UI if the value type
/// it has is not compatible with the value type expected by the action.
///
/// In all other ways, this interface is identical to <see cref="IInputInteraction"/>.
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1040:AvoidEmptyInterfaces", Justification = "This interface is used to mark implementing classes to advertise the value it expects. This seems more elegant then the suggestion to use an attribute.")]
public interface IInputInteraction<TValue> : IInputInteraction
where TValue : struct
{
}
internal static class InputInteraction
{
public static TypeTable s_Interactions;
public static Type GetValueType(Type interactionType)
{
if (interactionType == null)
throw new ArgumentNullException(nameof(interactionType));
return TypeHelpers.GetGenericTypeArgumentFromHierarchy(interactionType, typeof(IInputInteraction<>), 0);
}
public static string GetDisplayName(string interaction)
{
if (string.IsNullOrEmpty(interaction))
throw new ArgumentNullException(nameof(interaction));
var interactionType = s_Interactions.LookupTypeRegistration(interaction);
if (interactionType == null)
return interaction;
return GetDisplayName(interactionType);
}
public static string GetDisplayName(Type interactionType)
{
if (interactionType == null)
throw new ArgumentNullException(nameof(interactionType));
var displayNameAttribute = interactionType.GetCustomAttribute<DisplayNameAttribute>();
if (displayNameAttribute == null)
{
if (interactionType.Name.EndsWith("Interaction"))
return interactionType.Name.Substring(0, interactionType.Name.Length - "Interaction".Length);
return interactionType.Name;
}
return displayNameAttribute.DisplayName;
}
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,68 @@
namespace UnityEngine.InputSystem
{
/// <summary>
/// Indicates what type of change related to an <see cref="InputAction">input action</see> occurred.
/// </summary>
/// <seealso cref="InputSystem.onActionChange"/>
public enum InputActionChange
{
/// <summary>
/// An individual action was enabled.
/// </summary>
/// <seealso cref="InputAction.Enable"/>
ActionEnabled,
/// <summary>
/// An individual action was disabled.
/// </summary>
/// <seealso cref="InputAction.Disable"/>
ActionDisabled,
/// <summary>
/// An <see cref="InputActionMap">action map</see> was enabled.
/// </summary>
/// <seealso cref="InputActionMap.Enable"/>
ActionMapEnabled,
/// <summary>
/// An <see cref="InputActionMap">action map</see> was disabled.
/// </summary>
/// <seealso cref="InputActionMap.Disable"/>
ActionMapDisabled,
/// <summary>
/// An <see cref="InputAction"/> was started.
/// </summary>
/// <seealso cref="InputAction.started"/>
/// <seealso cref="InputActionPhase.Started"/>
ActionStarted,
/// <summary>
/// An <see cref="InputAction"/> was performed.
/// </summary>
/// <seealso cref="InputAction.performed"/>
/// <seealso cref="InputActionPhase.Performed"/>
ActionPerformed,
/// <summary>
/// An <see cref="InputAction"/> was canceled.
/// </summary>
/// <seealso cref="InputAction.canceled"/>
/// <seealso cref="InputActionPhase.Canceled"/>
ActionCanceled,
/// <summary>
/// Bindings on an action or set of actions are about to be re-resolved. This is called while <see cref="InputAction.controls"/>
/// for actions are still untouched and thus still reflect the old binding state of each action.
/// </summary>
/// <seealso cref="InputAction.controls"/>
BoundControlsAboutToChange,
/// <summary>
/// Bindings on an action or set of actions have been resolved. This is called after <see cref="InputAction.controls"/>
/// have been updated.
/// </summary>
/// <seealso cref="InputAction.controls"/>
BoundControlsChanged,
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,134 @@
using UnityEngine.InputSystem.Interactions;
////REVIEW: this goes beyond just actions; is there a better name? just InputPhase?
////REVIEW: what about opening up phases completely to interactions and allow them to come up with whatever custom phases?
namespace UnityEngine.InputSystem
{
/// <summary>
/// Trigger phase of an <see cref="InputAction"/>.
/// </summary>
/// <remarks>
/// Actions can be triggered in steps. For example, a <see cref="SlowTapInteraction">
/// 'slow tap'</see> will put an action into <see cref="Started"/> phase when a button
/// the action is bound to is pressed. At that point, however, the action still
/// has to wait for the expiration of a timer in order to make it a 'slow tap'. If
/// the button is release before the timer expires, the action will be <see cref="Canceled"/>
/// whereas if the button is held long enough, the action will be <see cref="Performed"/>.
/// </remarks>
/// <seealso cref="InputAction.phase"/>
/// <seealso cref="InputAction.CallbackContext.phase"/>
/// <seealso cref="InputAction.started"/>
/// <seealso cref="InputAction.performed"/>
/// <seealso cref="InputAction.canceled"/>
public enum InputActionPhase
{
/// <summary>
/// The action is not enabled.
/// </summary>
Disabled,
/// <summary>
/// The action is enabled and waiting for input on its associated controls.
///
/// This is the phase that an action goes back to once it has been <see cref="Performed"/>
/// or <see cref="Canceled"/>.
/// </summary>
Waiting,
/// <summary>
/// An associated control has been actuated such that it may lead to the action
/// being triggered. Will lead to <see cref="InputAction.started"/> getting called.
///
/// This phase will only be invoked if there are interactions on the respective control
/// binding. Without any interactions, an action will go straight from <see cref="Waiting"/>
/// into <see cref="Performed"/> and back into <see cref="Waiting"/> whenever an associated
/// control changes value.
///
/// An example of an interaction that uses the <see cref="Started"/> phase is <see cref="SlowTapInteraction"/>.
/// When the button it is bound to is pressed, the associated action goes into the <see cref="Started"/>
/// phase. At this point, the interaction does not yet know whether the button press will result in just
/// a tap or will indeed result in slow tap. If the button is released before the time it takes to
/// recognize a slow tap, then the action will go to <see cref="Canceled"/> and then back to <see cref="Waiting"/>.
/// If, however, the button is held long enough for it to qualify as a slow tap, the action will progress
/// to <see cref="Performed"/> and then go back to <see cref="Waiting"/>.
///
/// <see cref="Started"/> can be useful for UI feedback. For example, in a game where the weapon can be charged,
/// UI feedback can be initiated when the action is <see cref="Started"/>.
///
/// <example>
/// <code>
/// fireAction.started +=
/// ctx =>
/// {
/// if (ctx.interaction is SlowTapInteraction)
/// {
/// weaponCharging = true;
/// weaponChargeStartTime = ctx.time;
/// }
/// }
/// fireAction.canceled +=
/// ctx =>
/// {
/// weaponCharging = false;
/// }
/// fireAction.performed +=
/// ctx =>
/// {
/// Fire();
/// weaponCharging = false;
/// }
/// </code>
/// </example>
///
/// By default, an action is started as soon as a control moves away from its default value. This is
/// the case for both <see cref="InputActionType.Button"/> actions (which, however, does not yet have to mean
/// that the button press threshold has been reached; refer to <see cref="InputSettings.defaultButtonPressPoint"/>)
/// and <see cref="InputActionType.Value"/> actions. <see cref="InputActionType.PassThrough"/> does not use
/// the <c>Started</c> phase and instead goes straight to <see cref="Performed"/>.
///
/// For <see cref="InputActionType.Value"/> actions, <c>Started</c> will immediately be followed by <see cref="Performed"/>.
///
/// Note that interactions (refer to <see cref="IInputInteraction"/>) can alter how an action does or does not progress through
/// the phases.
/// </summary>
Started,
/// <summary>
/// The action has been performed. Leads to <see cref="InputAction.performed"/> getting called.
///
/// By default, a <see cref="InputActionType.Button"/> action performs when a control crosses the button
/// press threshold (refer to <see cref="InputSettings.defaultButtonPressPoint"/>), a <see cref="InputActionType.Value"/>
/// action performs on any value change that isn't the default value, and a <see cref="InputActionType.PassThrough"/>
/// action performs on any value change including going back to the default value.
///
/// Note that interactions (refer to <see cref="IInputInteraction"/>) can alter how an action does or does not progress through
/// the phases.
///
/// For a given action, finding out whether it was performed in the current frame can be done with <see cref="InputAction.WasPerformedThisFrame"/>.
///
/// <example>
/// <code>
/// action.WasPerformedThisFrame();
/// </code>
/// </example>
/// </summary>
Performed,
/// <summary>
/// The action has stopped. Leads to <see cref="InputAction.canceled"/> getting called.
///
/// By default, a <see cref="InputActionType.Button"/> action cancels when a control falls back below the button
/// press threshold (refer to <see cref="InputSettings.defaultButtonPressPoint"/>) and a <see cref="InputActionType.Value"/>
/// action cancels when a control moves back to its default value. A <see cref="InputActionType.PassThrough"/> action
/// does not generally cancel based on input on its controls.
///
/// An action will also get canceled when it is disabled while in progress (refer to <see cref="InputAction.Disable"/>).
///
/// Note that interactions (refer to <see cref="IInputInteraction"/>) can alter how an action does or does not progress through
/// the phases.
/// </summary>
Canceled
}
}

View File

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

View File

@@ -0,0 +1,176 @@
using System;
namespace UnityEngine.InputSystem
{
/// <summary>
/// A serializable property type that can either reference an action externally defined
/// in an <see cref="InputActionAsset"/> or define a new action directly on the property.
/// </summary>
/// <remarks>
/// This struct is meant to be used for serialized fields in <c>MonoBehaviour</c> and
/// <c>ScriptableObject</c> classes. It has a custom property drawer attached to it
/// that allows to switch between using the property as a reference and using it
/// to define an action in place.
///
/// <example>
/// <code>
/// public class MyBehavior : MonoBehaviour
/// {
/// // This can be edited in the inspector to either reference an existing
/// // action or to define an action directly on the component.
/// public InputActionProperty myAction;
/// }
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputAction"/>
/// <seealso cref="InputActionReference"/>
[Serializable]
public struct InputActionProperty : IEquatable<InputActionProperty>, IEquatable<InputAction>, IEquatable<InputActionReference>
{
/// <summary>
/// The effective action held on to by the property.
/// </summary>
/// <value>The effective action object contained in the property.</value>
/// <remarks>
/// This property will return <c>null</c> if the property is using a <see cref="reference"/> and
/// the referenced action cannot be found. Also, it will be <c>null</c> if the property
/// has been manually initialized with a <c>null</c> <see cref="InputAction"/> using
/// <see cref="InputActionProperty(InputAction)"/>.
/// </remarks>
public InputAction action => m_UseReference ? m_Reference != null ? m_Reference.action : null : m_Action;
/// <summary>
/// If the property is set to use a reference to the action, this property returns
/// the reference. Otherwise it returns <c>null</c>.
/// </summary>
/// <value>Reference to external input action, if defined.</value>
public InputActionReference reference => m_UseReference ? m_Reference : null;
/// <summary>
/// The serialized loose action created in code serialized with this property.
/// </summary>
/// <value>The serialized action field.</value>
internal InputAction serializedAction => m_Action;
/// <summary>
/// The serialized reference to an external action.
/// </summary>
/// <value>The serialized reference field.</value>
internal InputActionReference serializedReference => m_Reference;
/// <summary>
/// Initialize the property to contain the given action.
/// </summary>
/// <param name="action">An action.</param>
/// <remarks>
/// When the struct is serialized, it will serialize the given action as part of it.
/// The <see cref="reference"/> property will return <c>null</c>.
/// </remarks>
public InputActionProperty(InputAction action)
{
m_UseReference = false;
m_Action = action;
m_Reference = null;
}
/// <summary>
/// Initialize the property to use the given action reference.
/// </summary>
/// <param name="reference">Reference to an <see cref="InputAction"/>.</param>
/// <remarks>
/// When the struct is serialized, it will only serialize a reference to
/// the given <paramref name="reference"/> object.
/// </remarks>
public InputActionProperty(InputActionReference reference)
{
m_UseReference = true;
m_Action = null;
m_Reference = reference;
}
/// <summary>
/// Compare two action properties to see whether they refer to the same action.
/// </summary>
/// <param name="other">Another action property.</param>
/// <returns>True if both properties refer to the same action.</returns>
public bool Equals(InputActionProperty other)
{
return m_Reference == other.m_Reference &&
m_UseReference == other.m_UseReference &&
m_Action == other.m_Action;
}
/// <summary>
/// Check whether the property refers to the same action.
/// </summary>
/// <param name="other">An action.</param>
/// <returns>True if <see cref="action"/> is the same as <paramref name="other"/>.</returns>
public bool Equals(InputAction other)
{
return ReferenceEquals(action, other);
}
/// <summary>
/// Check whether the property references the same action.
/// </summary>
/// <param name="other">An action reference.</param>
/// <returns>True if the property and <paramref name="other"/> reference the same action.</returns>
public bool Equals(InputActionReference other)
{
return m_Reference == other;
}
/// <summary>
/// Check whether the given object is an InputActionProperty referencing the same action.
/// </summary>
/// <param name="obj">An object or <c>null</c>.</param>
/// <returns>True if the given <paramref name="obj"/> is an InputActionProperty equivalent to this one.</returns>
/// <seealso cref="Equals(InputActionProperty)"/>
public override bool Equals(object obj)
{
if (m_UseReference)
return Equals(obj as InputActionReference);
return Equals(obj as InputAction);
}
/// <summary>
/// Compute a hash code for the object.
/// </summary>
/// <returns>A hash code.</returns>
public override int GetHashCode()
{
if (m_UseReference)
return m_Reference != null ? m_Reference.GetHashCode() : 0;
return m_Action != null ? m_Action.GetHashCode() : 0;
}
/// <summary>
/// Compare the two properties for equivalence.
/// </summary>
/// <param name="left">The first property.</param>
/// <param name="right">The second property.</param>
/// <returns>True if the two action properties are equivalent.</returns>
/// <seealso cref="Equals(InputActionProperty)"/>
public static bool operator==(InputActionProperty left, InputActionProperty right)
{
return left.Equals(right);
}
/// <summary>
/// Compare the two properties for not being equivalent.
/// </summary>
/// <param name="left">The first property.</param>
/// <param name="right">The second property.</param>
/// <returns>True if the two action properties are not equivalent.</returns>
/// <seealso cref="Equals(InputActionProperty)"/>
public static bool operator!=(InputActionProperty left, InputActionProperty right)
{
return !left.Equals(right);
}
[SerializeField] private bool m_UseReference;
[SerializeField] private InputAction m_Action;
[SerializeField] private InputActionReference m_Reference;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,306 @@
using System;
////REVIEW: Can we somehow make this a simple struct? The one problem we have is that we can't put struct instances as sub-assets into
//// the import (i.e. InputActionImporter can't do AddObjectToAsset with them). However, maybe there's a way around that. The thing
//// is that we really want to store the asset reference plus the action GUID on the *user* side, i.e. the referencing side. Right
//// now, what happens is that InputActionImporter puts these objects along with the reference and GUID they contain in the
//// *imported* object, i.e. right with the asset. This partially defeats the whole purpose of having these objects and it means
//// that now the GUID doesn't really matter anymore. Rather, it's the file ID that now has to be stable.
////
//// If we always store the GUID and asset reference on the user side, we can put the serialized data *anywhere* and it'll remain
//// save and proper no matter what we do in InputActionImporter.
////REVIEW: should this throw if you try to assign an action that is not a singleton?
////REVIEW: akin to this, also have an InputActionMapReference?
namespace UnityEngine.InputSystem
{
/// <summary>
/// References a specific <see cref="InputAction"/> in an <see cref="InputActionMap"/>
/// stored inside an <see cref="InputActionAsset"/>.
/// </summary>
/// <remarks>
/// The difference to a plain reference directly to an <see cref="InputAction"/> object is
/// that an InputActionReference can be serialized without causing the referenced <see cref="InputAction"/>
/// to be serialized as well. The reference will remain intact even if the action or the map
/// that contains the action is renamed.
///
/// References can be set up graphically in the editor by dropping individual actions from the project
/// browser onto a reference field.
/// </remarks>
/// <seealso cref="InputActionProperty"/>
/// <seealso cref="InputAction"/>
/// <seealso cref="InputActionAsset"/>
public class InputActionReference : ScriptableObject
{
/// <summary>
/// The asset that the referenced action is part of. Null if the reference
/// is not initialized or if the asset has been deleted.
/// </summary>
/// <value>InputActionAsset of the referenced action.</value>
public InputActionAsset asset => m_Action?.m_ActionMap != null ? m_Action.m_ActionMap.asset : m_Asset;
/// <summary>
/// The action that the reference resolves to. Null if the action cannot be found.
/// </summary>
/// <value>The action that reference points to.</value>
/// <remarks>
/// Actions are resolved on demand based on their internally stored IDs.
/// </remarks>
public InputAction action
{
get
{
// Note that we need to check multiple things here that could invalidate the validity of the reference:
// 1) m_Action != null, this indicates if we have a resolved cached reference.
// 2) m_Action.actionMap != null, this would fail if the action has been removed from an action map
// and converted into a "singleton action". This would render the reference invalid since the action
// is no longer indirectly bound to m_Asset.
// 3) m_Action.actionMap.asset == m_Asset, needs to be checked to make sure that its action map
// have not been moved to another asset which would invalidate the reference since reference is
// defined by action GUID and asset reference.
// 4) m_Asset, a Unity object life-time check that would fail if the asset has been deleted.
if (m_Action != null && m_Action.actionMap != null && m_Action.actionMap.asset == m_Asset && m_Asset)
return m_Action;
// Attempt to resolve action based on asset and GUID.
return (m_Action = m_Asset ? m_Asset.FindAction(new Guid(m_ActionId)) : null);
}
}
/// <summary>
/// Initialize the reference to refer to the given action.
/// </summary>
/// <param name="action">An input action. Must be contained in an <see cref="InputActionMap"/>
/// that is itself contained in an <see cref="InputActionAsset"/>. Can be <c>null</c> in which
/// case the reference is reset to its default state which does not reference an action.</param>
/// <exception cref="InvalidOperationException"><paramref name="action"/> is not contained in an
/// <see cref="InputActionMap"/> that is itself contained in an <see cref="InputActionAsset"/>.</exception>
/// <exception cref="InvalidOperationException">If attempting to mutate a reference object
/// that is backed by an .inputactions asset. This is not allowed to prevent side-effects.</exception>
public void Set(InputAction action)
{
if (action == null)
{
m_Asset = null;
m_ActionId = null;
m_Action = null;
name = string.Empty; // Scriptable object default name is empty string.
return;
}
var map = action.actionMap;
if (map == null || map.asset == null)
throw new InvalidOperationException(
$"Action '{action}' must be part of an InputActionAsset in order to be able to create an InputActionReference for it");
SetInternal(map.asset, action);
}
/// <summary>
/// Look up an action in the given asset and initialize the reference to
/// point to it.
/// </summary>
/// <param name="asset">An .inputactions asset.</param>
/// <param name="mapName">Name of the <see cref="InputActionMap"/> in <paramref name="asset"/>
/// (see <see cref="InputActionAsset.actionMaps"/>). Case-insensitive.</param>
/// <param name="actionName">Name of the action in <paramref name="mapName"/>. Case-insensitive.</param>
/// <exception cref="ArgumentNullException"><paramref name="asset"/> is <c>null</c> -or-
/// <paramref name="mapName"/> is <c>null</c> or empty -or- <paramref name="actionName"/>
/// is <c>null</c> or empty.</exception>
/// <exception cref="InvalidOperationException">If attempting to mutate a reference object
/// that is backed by by .inputactions asset. This is not allowed to prevent side-effects.</exception>
/// <exception cref="ArgumentException">No action map called <paramref name="mapName"/> could
/// be found in <paramref name="asset"/> -or- no action called <paramref name="actionName"/>
/// could be found in the action map called <paramref name="mapName"/> in <paramref name="asset"/>.</exception>
public void Set(InputActionAsset asset, string mapName, string actionName)
{
if (asset == null)
throw new ArgumentNullException(nameof(asset));
if (string.IsNullOrEmpty(mapName))
throw new ArgumentNullException(nameof(mapName));
if (string.IsNullOrEmpty(actionName))
throw new ArgumentNullException(nameof(actionName));
var actionMap = asset.FindActionMap(mapName);
if (actionMap == null)
throw new ArgumentException($"No action map '{mapName}' in '{asset}'", nameof(mapName));
var foundAction = actionMap.FindAction(actionName);
if (foundAction == null)
throw new ArgumentException($"No action '{actionName}' in map '{mapName}' of asset '{asset}'",
nameof(actionName));
SetInternal(asset, foundAction);
}
private void SetInternal(InputActionAsset assetArg, InputAction actionArg)
{
CheckImmutableReference();
// If we are setting the reference in edit-mode, we want the state to be reflected in the serialized
// object and hence assign serialized fields. This is a destructive operation.
m_Asset = assetArg;
m_ActionId = actionArg.id.ToString();
m_Action = actionArg;
name = GetDisplayName(actionArg);
}
/// <summary>
/// Return a string representation of the reference useful for debugging.
/// </summary>
/// <returns>A string representation of the reference.</returns>
public override string ToString()
{
var value = action; // Indirect resolve
if (value == null)
return base.ToString();
if (value.actionMap != null)
return m_Asset != null ? $"{m_Asset.name}:{value.actionMap.name}/{value.name}" : $"{value.actionMap.name}/{value.name}";
return m_Asset != null ? $"{m_Asset.name}:{m_ActionId}" : m_ActionId;
}
private static string GetDisplayName(InputAction action)
{
return !string.IsNullOrEmpty(action?.actionMap?.name)
? $"{action.actionMap?.name}/{action.name}"
: action?.name;
}
/// <summary>
/// Return a string representation useful for showing in UI.
/// </summary>
internal string ToDisplayName()
{
return string.IsNullOrEmpty(name) ? GetDisplayName(action) : name;
}
/// <summary>
/// Convert an InputActionReference to the InputAction it points to.
/// </summary>
/// <param name="reference">An InputActionReference object. Can be null.</param>
/// <returns>The value of <see cref="action"/> from <paramref name="reference"/>. Can be null.</returns>
public static implicit operator InputAction(InputActionReference reference)
{
return reference?.action;
}
/// <summary>
/// Create a new InputActionReference object that references the given action.
/// </summary>
/// <param name="action">An input action. Must be contained in an <see cref="InputActionMap"/>
/// that is itself contained in an <see cref="InputActionAsset"/>. Can be <c>null</c> in which
/// case the reference is reset to its default state which does not reference an action.</param>
/// <returns>A new InputActionReference referencing <paramref name="action"/>.</returns>
public static InputActionReference Create(InputAction action)
{
var reference = CreateInstance<InputActionReference>();
reference.Set(action);
return reference;
}
/// <summary>
/// Clears the cached <see cref="m_Action"/> field for all current <see cref="InputActionReference"/> objects.
/// </summary>
/// <remarks>
/// This method is used to clear the Action references when exiting PlayMode since those objects are no
/// longer valid.
/// </remarks>
internal static void InvalidateAll()
{
// It might be possible that Object.FindObjectOfTypeAll(true) would be sufficient here since we only
// need to invalidate non-serialized data on active/loaded objects. This returns a lot more, but for
// now we keep it like this to not change more than we have to. Optimizing this can be done separately.
var allActionRefs = Resources.FindObjectsOfTypeAll(typeof(InputActionReference));
foreach (var obj in allActionRefs)
((InputActionReference)obj).Invalidate();
}
/// <summary>
/// Clears the cached <see cref="m_Action"/> field for this <see cref="InputActionReference"/> instance.
/// </summary>
/// <remarks>
/// After calling this, the next call to <see cref="action"/> will resolve a new <see cref="InputAction"/>
/// reference from the existing <see cref="InputActionAsset"/> just as if using it for the first time.
/// The serialized <see cref="m_Asset"/> and <see cref="m_ActionId"/> fields are not touched and will continue
/// to hold their current values. Also the name remains valid since the underlying reference data is unmodified.
/// </remarks>
internal void Invalidate()
{
m_Action = null;
}
[SerializeField] internal InputActionAsset m_Asset;
// Can't serialize System.Guid and Unity's GUID is editor only so these
// go out as strings.
[SerializeField] internal string m_ActionId;
/// <summary>
/// The resolved, cached input action.
/// </summary>
[NonSerialized] private InputAction m_Action;
/// <summary>
/// Equivalent to <see cref="InputActionReference.action"/>.
/// </summary>
/// <returns>The associated action reference if its a valid reference, else <c>null</c>.</returns>
public InputAction ToInputAction()
{
return action;
}
/// <summary>
/// Checks if this input action reference instance can be safely mutated without side effects.
/// </summary>
/// <remarks>
/// This check isn't needed in player builds since ScriptableObject would never be persisted if mutated
/// in a player.
/// </remarks>
/// <exception cref="InvalidOperationException">Thrown if this input action reference is part of an
/// input actions asset and mutating it would have side-effects on the projects assets.</exception>
private void CheckImmutableReference()
{
#if UNITY_EDITOR
// Note that we do a lot of checking here, but it is only for a rather slim (unintended) use case in
// editor and not in final builds. The alternative would be to set a non-serialized field on the reference
// when importing assets which would simplify this class, but it adds complexity to import stage and
// is more difficult to assess from a asset version portability perspective.
static bool CanSetReference(InputActionReference reference)
{
// "Immutable" input action references are always sub-assets of InputActionAsset.
var isSubAsset = UnityEditor.AssetDatabase.IsSubAsset(reference);
if (!isSubAsset)
return true;
// If we cannot get the path of our reference, we cannot be a persisted asset within an InputActionAsset.
var path = UnityEditor.AssetDatabase.GetAssetPath(reference);
if (path == null)
return true;
// If we cannot get the main asset we cannot be a persisted asset within an InputActionAsset.
// Also we check that it is the expected type.
var mainAsset = UnityEditor.AssetDatabase.LoadMainAssetAtPath(path);
if (!mainAsset)
return true;
// We can only allow setting the reference if it is not part of an persisted InputActionAsset.
return (mainAsset is not InputActionAsset);
}
// Prevent accidental mutation of the source asset if this InputActionReference is a persisted object
// residing as a sub-asset within a .inputactions asset.
// This is not needed for players since scriptable objects aren't serialized back from within a player.
if (!CanSetReference(this))
{
throw new InvalidOperationException("Attempting to modify an immutable InputActionReference instance " +
"that is part of an .inputactions asset. This is not allowed since it would modify the source " +
"asset in which the reference is serialized and potentially corrupt it. " +
"Instead use InputActionReference.Create(action) to create a new mutable " +
"in-memory instance or serialize it as a separate asset if the intent is for changes to " +
"survive domain reloads.");
}
#endif // UNITY_EDITOR
}
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,741 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.InputSystem.LowLevel;
////REVIEW: why not switch to this being the default mechanism? seems like this could allow us to also solve
//// the actions-update-when-not-expected problem; plus give us access to easy polling
////REVIEW: should this automatically unsubscribe itself on disposal?
////TODO: make it possible to persist this same way that it should be possible to persist InputEventTrace
////TODO: make this one thread-safe
////TODO: add random access capability
////TODO: protect traces against controls changing configuration (if state layouts change, we're affected)
namespace UnityEngine.InputSystem.Utilities
{
/// <summary>
/// Records the triggering of actions into a sequence of events that can be replayed at will.
/// </summary>
/// <remarks>
/// This is an alternate way to the callback-based responses (such as <see cref="InputAction.performed"/>)
/// of <see cref="InputAction">input actions</see>. Instead of executing response code right away whenever
/// an action triggers, an <see cref="RecordAction">event is recorded</see> which can then be queried on demand.
///
/// The recorded data will stay valid even if the bindings on the actions are changed (e.g. by enabling a different
/// set of bindings through altering <see cref="InputAction.bindingMask"/> or <see cref="InputActionMap.devices"/> or
/// when modifying the paths of bindings altogether). Note, however, that when this happens, a trace will have
/// to make a private copy of the data that stores the binding resolution state. This means that there can be
/// GC allocation spike when reconfiguring actions that have recorded data in traces.
///
/// <example>
/// <code>
/// var trace = new InputActionTrace();
///
/// // Subscribe trace to single action.
/// // (Use UnsubscribeFrom to unsubscribe)
/// trace.SubscribeTo(myAction);
///
/// // Subscribe trace to entire action map.
/// // (Use UnsubscribeFrom to unsubscribe)
/// trace.SubscribeTo(myActionMap);
///
/// // Subscribe trace to all actions in the system.
/// trace.SubscribeToAll();
///
/// // Record a single triggering of an action.
/// myAction.performed +=
/// ctx =>
/// {
/// if (ctx.ReadValue&lt;float&gt;() &gt; 0.5f)
/// trace.RecordAction(ctx);
/// };
///
/// // Output trace to console.
/// Debug.Log(string.Join(",\n", trace));
///
/// // Walk through all recorded actions and then clear trace.
/// foreach (var record in trace)
/// {
/// Debug.Log($"{record.action} was {record.phase} by control {record.control} at {record.time}");
///
/// // To read out the value, you either have to know the value type or read the
/// // value out as a generic byte buffer. Here we assume that the value type is
/// // float.
///
/// Debug.Log("Value: " + record.ReadValue&lt;float&gt;());
///
/// // An alternative is read the value as an object. In this case, you don't have
/// // to know the value type but there will be a boxed object allocation.
/// Debug.Log("Value: " + record.ReadValueAsObject());
/// }
/// trace.Clear();
///
/// // Unsubscribe trace from everything.
/// trace.UnsubscribeFromAll();
///
/// // Release memory held by trace.
/// trace.Dispose();
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputAction.started"/>
/// <seealso cref="InputAction.performed"/>
/// <seealso cref="InputAction.canceled"/>
/// <seealso cref="InputSystem.onActionChange"/>
public sealed class InputActionTrace : IEnumerable<InputActionTrace.ActionEventPtr>, IDisposable
{
////REVIEW: this is of limited use without having access to ActionEvent
/// <summary>
/// Directly access the underlying raw memory queue.
/// </summary>
public InputEventBuffer buffer => m_EventBuffer;
/// <summary>
/// Returns the number of events in the associated event buffer.
/// </summary>
public int count => m_EventBuffer.eventCount;
/// <summary>
/// Constructs a new default initialized <c>InputActionTrace</c>.
/// </summary>
/// <remarks>
/// When you use this constructor, the new InputActionTrace object does not start recording any actions.
/// To record actions, you must explicitly set them up after creating the object.
/// Alternatively, you can use one of the other constructor overloads which begin recording actions immediately.
/// </remarks>
/// <seealso cref="SubscribeTo(InputAction)"/>
/// <seealso cref="SubscribeTo(InputActionMap)"/>
/// <seealso cref="SubscribeToAll"/>
public InputActionTrace()
{
}
/// <summary>
/// Constructs a new <c>InputActionTrace</c> that records <paramref name="action"/>.
/// </summary>
/// <param name="action">The action to be recorded.</param>
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="action"/> is <c>null</c>.</exception>
public InputActionTrace(InputAction action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
SubscribeTo(action);
}
/// <summary>
/// Constructs a new <c>InputActionTrace</c> that records all actions in <paramref name="actionMap"/>.
/// </summary>
/// <param name="actionMap">The action-map containing actions to be recorded.</param>
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="action"/> is <c>null</c>.</exception>
public InputActionTrace(InputActionMap actionMap)
{
if (actionMap == null)
throw new ArgumentNullException(nameof(actionMap));
SubscribeTo(actionMap);
}
/// <summary>
/// Record any action getting triggered anywhere.
/// </summary>
/// <remarks>
/// This does not require the trace to actually hook into every single action or action map in the system.
/// Instead, the trace will listen to <see cref="InputSystem.onActionChange"/> and automatically record
/// every triggered action.
/// </remarks>
/// <seealso cref="SubscribeTo(InputAction)"/>
/// <seealso cref="SubscribeTo(InputActionMap)"/>
public void SubscribeToAll()
{
if (m_SubscribedToAll)
return;
HookOnActionChange();
m_SubscribedToAll = true;
// Remove manually created subscriptions.
while (m_SubscribedActions.length > 0)
UnsubscribeFrom(m_SubscribedActions[m_SubscribedActions.length - 1]);
while (m_SubscribedActionMaps.length > 0)
UnsubscribeFrom(m_SubscribedActionMaps[m_SubscribedActionMaps.length - 1]);
}
/// <summary>
/// Unsubscribes from all actions currently being recorded.
/// </summary>
/// <seealso cref="UnsubscribeFrom(InputAction)"/>
/// <seealso cref="UnsubscribeFrom(InputActionMap)"/>
public void UnsubscribeFromAll()
{
// Only unhook from OnActionChange if we don't have any recorded actions. If we do have
// any, we still need the callback to be notified about when binding data changes.
if (count == 0)
UnhookOnActionChange();
m_SubscribedToAll = false;
while (m_SubscribedActions.length > 0)
UnsubscribeFrom(m_SubscribedActions[m_SubscribedActions.length - 1]);
while (m_SubscribedActionMaps.length > 0)
UnsubscribeFrom(m_SubscribedActionMaps[m_SubscribedActionMaps.length - 1]);
}
/// <summary>
/// Subscribes to <paramref name="action"/>.
/// </summary>
/// <param name="action">The action to be recorded.</param>
/// <remarks>
/// > [!NOTE]
/// > This method does not prevent you from subscribing to the same action multiple times.
/// > If you subscribe to the same action multiple times, your event buffer will contain duplicate entries.
/// </remarks>
/// <exception cref="ArgumentNullException">If <paramref name="action"/> is <c>null</c>.</exception>
/// <seealso cref="SubscribeTo(InputActionMap)"/>
/// <seealso cref="SubscribeToAll"/>
public void SubscribeTo(InputAction action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
if (m_CallbackDelegate == null)
m_CallbackDelegate = RecordAction;
action.performed += m_CallbackDelegate;
action.started += m_CallbackDelegate;
action.canceled += m_CallbackDelegate;
m_SubscribedActions.AppendWithCapacity(action);
}
/// <summary>
/// Subscribes to all actions contained within <paramref name="actionMap"/>.
/// </summary>
/// <param name="actionMap">The action-map containing all actions to be recorded.</param>
/// <remarks>
/// > [!NOTE]
/// > This method does not prevent you from subscribing to the same action multiple times.
/// > If you subscribe to the same action multiple times, your event buffer will contain duplicate entries.
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="actionMap"/> is null.</exception>
/// <seealso cref="SubscribeTo(InputAction)"/>
/// <seealso cref="SubscribeToAll"/>
public void SubscribeTo(InputActionMap actionMap)
{
if (actionMap == null)
throw new ArgumentNullException(nameof(actionMap));
if (m_CallbackDelegate == null)
m_CallbackDelegate = RecordAction;
actionMap.actionTriggered += m_CallbackDelegate;
m_SubscribedActionMaps.AppendWithCapacity(actionMap);
}
/// <summary>
/// Unsubscribes from an action, if that action was previously subscribed to.
/// </summary>
/// <param name="action">The action to unsubscribe from.</param>
/// <remarks>
/// > [!NOTE]
/// > This method has no side effects if you attempt to unsubscribe from an action that you have not previously subscribed to.
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="action"/> is <c>null</c>.</exception>
/// <seealso cref="UnsubscribeFrom(InputActionMap)"/>
/// <seealso cref="UnsubscribeFromAll"/>
public void UnsubscribeFrom(InputAction action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
if (m_CallbackDelegate == null)
return;
action.performed -= m_CallbackDelegate;
action.started -= m_CallbackDelegate;
action.canceled -= m_CallbackDelegate;
var index = m_SubscribedActions.IndexOfReference(action);
if (index != -1)
m_SubscribedActions.RemoveAtWithCapacity(index);
}
/// <summary>
/// Unsubscribes from all actions included in <paramref name="actionMap"/>.
/// </summary>
/// <param name="actionMap">The action-map containing actions to unsubscribe from.</param>
/// <remarks>
/// > [!NOTE]
/// > This method has no side effects if you attempt to unsubscribe from an action-map that you have not previously subscribed to.
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="actionMap"/> is <c>null</c>.</exception>
/// <seealso cref="UnsubscribeFrom(InputAction)"/>
/// <seealso cref="UnsubscribeFromAll"/>
public void UnsubscribeFrom(InputActionMap actionMap)
{
if (actionMap == null)
throw new ArgumentNullException(nameof(actionMap));
if (m_CallbackDelegate == null)
return;
actionMap.actionTriggered -= m_CallbackDelegate;
var index = m_SubscribedActionMaps.IndexOfReference(actionMap);
if (index != -1)
m_SubscribedActionMaps.RemoveAtWithCapacity(index);
}
/// <summary>
/// Record the triggering of an action as an <see cref="ActionEventPtr">action event</see>.
/// </summary>
/// <param name="context"></param>
/// <see cref="InputAction.performed"/>
/// <see cref="InputAction.started"/>
/// <see cref="InputAction.canceled"/>
/// <see cref="InputActionMap.actionTriggered"/>
public unsafe void RecordAction(InputAction.CallbackContext context)
{
// Find/add state.
var stateIndex = m_ActionMapStates.IndexOfReference(context.m_State);
if (stateIndex == -1)
stateIndex = m_ActionMapStates.AppendWithCapacity(context.m_State);
// Make sure we get notified if there's a change to binding setups.
HookOnActionChange();
// Allocate event.
var valueSizeInBytes = context.valueSizeInBytes;
var eventPtr =
(ActionEvent*)m_EventBuffer.AllocateEvent(ActionEvent.GetEventSizeWithValueSize(valueSizeInBytes));
// Initialize event.
ref var triggerState = ref context.m_State.actionStates[context.m_ActionIndex];
eventPtr->baseEvent.type = ActionEvent.Type;
eventPtr->baseEvent.time = triggerState.time;
eventPtr->stateIndex = stateIndex;
eventPtr->controlIndex = triggerState.controlIndex;
eventPtr->bindingIndex = triggerState.bindingIndex;
eventPtr->interactionIndex = triggerState.interactionIndex;
eventPtr->startTime = triggerState.startTime;
eventPtr->phase = triggerState.phase;
// Store value.
// NOTE: If the action triggered from a composite, this stores the value as
// read from the composite.
// NOTE: Also, the value we store is a fully processed value.
var valueBuffer = eventPtr->valueData;
context.ReadValue(valueBuffer, valueSizeInBytes);
}
/// <summary>
/// Clears all recorded data.
/// </summary>
/// <remarks>
/// > [!NOTE]
/// > This method does not unsubscribe any actions that the instance is listening to, so after clearing the recorded data, new input on those subscribed actions will continue to be recorded.
/// </remarks>
public void Clear()
{
m_EventBuffer.Reset();
m_ActionMapStates.ClearWithCapacity();
}
~InputActionTrace()
{
DisposeInternal();
}
/// <inheritdoc/>
public override string ToString()
{
if (count == 0)
return "[]";
var str = new StringBuilder();
str.Append('[');
var isFirst = true;
foreach (var eventPtr in this)
{
if (!isFirst)
str.Append(",\n");
str.Append(eventPtr.ToString());
isFirst = false;
}
str.Append(']');
return str.ToString();
}
/// <inheritdoc/>
public void Dispose()
{
UnsubscribeFromAll();
DisposeInternal();
}
private void DisposeInternal()
{
// Nuke clones we made of InputActionMapStates.
for (var i = 0; i < m_ActionMapStateClones.length; ++i)
m_ActionMapStateClones[i].Dispose();
m_EventBuffer.Dispose();
m_ActionMapStates.Clear();
m_ActionMapStateClones.Clear();
if (m_ActionChangeDelegate != null)
{
InputSystem.onActionChange -= m_ActionChangeDelegate;
m_ActionChangeDelegate = null;
}
}
/// <summary>
/// Returns an enumerator that enumerates all action events recorded for this instance.
/// </summary>
/// <returns>Enumerator instance, never <c>null</c>.</returns>
/// <seealso cref="ActionEventPtr"/>
public IEnumerator<ActionEventPtr> GetEnumerator()
{
return new Enumerator(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
private bool m_SubscribedToAll;
private bool m_OnActionChangeHooked;
private InlinedArray<InputAction> m_SubscribedActions;
private InlinedArray<InputActionMap> m_SubscribedActionMaps;
private InputEventBuffer m_EventBuffer;
private InlinedArray<InputActionState> m_ActionMapStates;
private InlinedArray<InputActionState> m_ActionMapStateClones;
private Action<InputAction.CallbackContext> m_CallbackDelegate;
private Action<object, InputActionChange> m_ActionChangeDelegate;
private void HookOnActionChange()
{
if (m_OnActionChangeHooked)
return;
if (m_ActionChangeDelegate == null)
m_ActionChangeDelegate = OnActionChange;
InputSystem.onActionChange += m_ActionChangeDelegate;
m_OnActionChangeHooked = true;
}
private void UnhookOnActionChange()
{
if (!m_OnActionChangeHooked)
return;
InputSystem.onActionChange -= m_ActionChangeDelegate;
m_OnActionChangeHooked = false;
}
private void OnActionChange(object actionOrMapOrAsset, InputActionChange change)
{
// If we're subscribed to all actions, check if an action got triggered.
if (m_SubscribedToAll)
{
switch (change)
{
case InputActionChange.ActionStarted:
case InputActionChange.ActionPerformed:
case InputActionChange.ActionCanceled:
Debug.Assert(actionOrMapOrAsset is InputAction, "Expected an action");
var triggeredAction = (InputAction)actionOrMapOrAsset;
var actionIndex = triggeredAction.m_ActionIndexInState;
var stateForAction = triggeredAction.m_ActionMap.m_State;
var context = new InputAction.CallbackContext
{
m_State = stateForAction,
m_ActionIndex = actionIndex,
};
RecordAction(context);
return;
}
}
// We're only interested in changes to the binding resolution state of actions.
if (change != InputActionChange.BoundControlsAboutToChange)
return;
// Grab the associated action map(s).
if (actionOrMapOrAsset is InputAction action)
CloneActionStateBeforeBindingsChange(action.m_ActionMap);
else if (actionOrMapOrAsset is InputActionMap actionMap)
CloneActionStateBeforeBindingsChange(actionMap);
else if (actionOrMapOrAsset is InputActionAsset actionAsset)
foreach (var actionMapInAsset in actionAsset.actionMaps)
CloneActionStateBeforeBindingsChange(actionMapInAsset);
else
Debug.Assert(false, "Expected InputAction, InputActionMap or InputActionAsset");
}
private void CloneActionStateBeforeBindingsChange(InputActionMap actionMap)
{
// Grab the state.
var state = actionMap.m_State;
if (state == null)
{
// Bindings have not been resolved yet for this action map. We shouldn't even be
// on the notification list in this case, but just in case, ignore.
return;
}
// See if we're using the given state.
var stateIndex = m_ActionMapStates.IndexOfReference(state);
if (stateIndex == -1)
return;
// Yes, we are so make our own private copy of its current state.
// NOTE: We do not put these local InputActionMapStates on the global list.
var clone = state.Clone();
m_ActionMapStateClones.Append(clone);
m_ActionMapStates[stateIndex] = clone;
}
/// <summary>
/// A wrapper around <see cref="ActionEvent"/> that automatically translates all the
/// information in events into their high-level representations.
/// </summary>
/// <remarks>
/// For example, instead of returning <see cref="ActionEvent.controlIndex">control indices</see>,
/// it automatically resolves and returns the respective <see cref="InputControl">controls</see>.
/// </remarks>
public unsafe struct ActionEventPtr
{
internal InputActionState m_State;
internal ActionEvent* m_Ptr;
/// <summary>
/// The <see cref="InputAction"/> associated with this action event.
/// </summary>
public InputAction action => m_State.GetActionOrNull(m_Ptr->bindingIndex);
/// <summary>
/// The <see cref="InputActionPhase"/> associated with this action event.
/// </summary>
/// <seealso cref="InputAction.phase"/>
/// <seealso cref="InputAction.CallbackContext.phase"/>
public InputActionPhase phase => m_Ptr->phase;
/// <summary>
/// The <see cref="InputControl"/> instance associated with this action event.
/// </summary>
public InputControl control => m_State.controls[m_Ptr->controlIndex];
/// <summary>
/// The <see cref="IInputInteraction"/> instance associated with this action event if applicable, or <c>null</c> if the action event is not associated with an input interaction.
/// </summary>
public IInputInteraction interaction
{
get
{
var index = m_Ptr->interactionIndex;
if (index == InputActionState.kInvalidIndex)
return null;
return m_State.interactions[index];
}
}
/// <summary>
/// The time, in seconds since your game or app started, that the event occurred.
/// </summary>
/// <remarks>
/// Times are in seconds and progress linearly in real-time. The timeline is the same as for <see cref="Time.realtimeSinceStartup"/>.
/// </remarks>
public double time => m_Ptr->baseEvent.time;
/// <summary>
/// The time, in seconds since your game or app started, that the <see cref="phase"/> transitioned into <see cref="InputActionPhase.Started"/>.
/// </summary>
public double startTime => m_Ptr->startTime;
/// <summary>
/// The duration, in seconds, that has elapsed between when this event was generated and when the
/// action <see cref="phase"/> transitioned to <see cref="InputActionPhase.Started"/> and has remained active.
/// </summary>
public double duration => time - startTime;
/// <summary>
/// The size, in bytes, of the value associated with this action event.
/// </summary>
public int valueSizeInBytes => m_Ptr->valueSizeInBytes;
/// <summary>
/// Reads the value associated with this event as an <c>object</c>.
/// </summary>
/// <returns><c>object</c> representing the value of this action event.</returns>
/// <seealso cref="ReadOnlyArray{TValue}"/>
/// <seealso cref="ReadValue(void*, int)"/>
public object ReadValueAsObject()
{
if (m_Ptr == null)
throw new InvalidOperationException("ActionEventPtr is invalid");
var valuePtr = m_Ptr->valueData;
// Check if the value came from a composite.
var bindingIndex = m_Ptr->bindingIndex;
if (m_State.bindingStates[bindingIndex].isPartOfComposite)
{
// Yes, so have to put the value/struct data we read into a boxed
// object based on the value type of the composite.
var compositeBindingIndex = m_State.bindingStates[bindingIndex].compositeOrCompositeBindingIndex;
var compositeIndex = m_State.bindingStates[compositeBindingIndex].compositeOrCompositeBindingIndex;
var composite = m_State.composites[compositeIndex];
Debug.Assert(composite != null, "NULL composite instance");
var valueType = composite.valueType;
if (valueType == null)
throw new InvalidOperationException($"Cannot read value from Composite '{composite}' which does not have a valueType set");
return Marshal.PtrToStructure(new IntPtr(valuePtr), valueType);
}
// Expecting action to only trigger from part bindings or bindings outside of composites.
Debug.Assert(!m_State.bindingStates[bindingIndex].isComposite, "Action should not have triggered directly from a composite binding");
// Read value through InputControl.
var valueSizeInBytes = m_Ptr->valueSizeInBytes;
return control.ReadValueFromBufferAsObject(valuePtr, valueSizeInBytes);
}
/// <summary>
/// Reads the value associated with this event into the contiguous memory buffer defined by <c>[buffer, buffer + bufferSize)</c>.
/// </summary>
/// <param name="buffer">Pointer to the contiguous memory buffer to write value data to.</param>
/// <param name="bufferSize">The size, in bytes, of the contiguous buffer pointed to by <paramref name="buffer"/>.</param>
/// <exception cref="NullReferenceException">If <paramref name="buffer"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">If the given <paramref name="bufferSize"/> is less than the number of bytes required to write the event value to <paramref name="buffer"/>.</exception>
/// <seealso cref="ReadValueAsObject"/>
/// <seealso cref="ReadValue{TValue}"/>
public void ReadValue(void* buffer, int bufferSize)
{
var valueSizeInBytes = m_Ptr->valueSizeInBytes;
////REVIEW: do we want more checking than this?
if (bufferSize < valueSizeInBytes)
throw new ArgumentException(
$"Expected buffer of at least {valueSizeInBytes} bytes but got buffer of just {bufferSize} bytes instead",
nameof(bufferSize));
UnsafeUtility.MemCpy(buffer, m_Ptr->valueData, valueSizeInBytes);
}
/// <summary>
/// Reads the value associated with this event as an object of type <typeparamref name="TValue"/>.
/// </summary>
/// <typeparam name="TValue">The event value type to be used.</typeparam>
/// <returns>Object of type <typeparamref name="TValue"/>.</returns>
/// <exception cref="InvalidOperationException">In case the size of <typeparamref name="TValue"/> does not match the size of the value associated with this event.</exception>
public TValue ReadValue<TValue>()
where TValue : struct
{
var valueSizeInBytes = m_Ptr->valueSizeInBytes;
////REVIEW: do we want more checking than this?
if (UnsafeUtility.SizeOf<TValue>() != valueSizeInBytes)
throw new InvalidOperationException(
$"Cannot read a value of type '{typeof(TValue).Name}' with size {UnsafeUtility.SizeOf<TValue>()} from event on action '{action}' with value size {valueSizeInBytes}");
var result = new TValue();
var resultPtr = UnsafeUtility.AddressOf(ref result);
UnsafeUtility.MemCpy(resultPtr, m_Ptr->valueData, valueSizeInBytes);
return result;
}
/// <inheritdoc/>
public override string ToString()
{
if (m_Ptr == null)
return "<null>";
var actionName = action.actionMap != null ? $"{action.actionMap.name}/{action.name}" : action.name;
return $"{{ action={actionName} phase={phase} time={time} control={control} value={ReadValueAsObject()} interaction={interaction} duration={duration} }}";
}
}
private unsafe struct Enumerator : IEnumerator<ActionEventPtr>
{
private readonly InputActionTrace m_Trace;
private readonly ActionEvent* m_Buffer;
private readonly int m_EventCount;
private ActionEvent* m_CurrentEvent;
private int m_CurrentIndex;
public Enumerator(InputActionTrace trace)
{
m_Trace = trace;
m_Buffer = (ActionEvent*)trace.m_EventBuffer.bufferPtr.data;
m_EventCount = trace.m_EventBuffer.eventCount;
m_CurrentEvent = null;
m_CurrentIndex = 0;
}
public bool MoveNext()
{
if (m_CurrentIndex == m_EventCount)
return false;
if (m_CurrentEvent == null)
{
m_CurrentEvent = m_Buffer;
return m_CurrentEvent != null;
}
Debug.Assert(m_CurrentEvent != null);
++m_CurrentIndex;
if (m_CurrentIndex == m_EventCount)
return false;
m_CurrentEvent = (ActionEvent*)InputEvent.GetNextInMemory((InputEvent*)m_CurrentEvent);
return true;
}
public void Reset()
{
m_CurrentEvent = null;
m_CurrentIndex = 0;
}
public void Dispose()
{
}
public ActionEventPtr Current
{
get
{
var state = m_Trace.m_ActionMapStates[m_CurrentEvent->stateIndex];
return new ActionEventPtr
{
m_State = state,
m_Ptr = m_CurrentEvent,
};
}
}
object IEnumerator.Current => Current;
}
}
}

View File

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

View File

@@ -0,0 +1,206 @@
namespace UnityEngine.InputSystem
{
/// <summary>
/// Determines the behavior with which an <see cref="InputAction"/> triggers.
/// </summary>
/// <remarks>
/// While all actions essentially function the same way, there are differences in how an action
/// will react to changes in values on the controls it is bound to.
///
/// The most straightforward type of behavior is <see cref="PassThrough"/> which does not expect
/// any kind of value change pattern but simply triggers the action on every single value change.
/// A pass-through action will not use <see cref="InputAction.started"/> or
/// <see cref="InputAction.canceled"/> except on bindings that have an interaction added to them.
/// Pass-through actions are most useful for sourcing input from arbitrary many controls and
/// simply piping all input through without much processing on the side of the action.
///
/// <example>
/// <code>
/// // An action that triggers every time any button on the gamepad is
/// // pressed or released.
/// var action = new InputAction(
/// type: InputActionType.PassThrough,
/// binding: "&lt;Gamepad&gt;/&lt;Button&gt;");
///
/// action.performed +=
/// ctx =>
/// {
/// var button = (ButtonControl)ctx.control;
/// if (button.wasPressedThisFrame)
/// Debug.Log($"Button {ctx.control} was pressed");
/// else if (button.wasReleasedThisFrame)
/// Debug.Log($"Button {ctx.control} was released");
/// // NOTE: We may get calls here in which neither the if nor the else
/// // clause are true here. A button like the gamepad left and right
/// // triggers, for example, do not just have a binary on/off state
/// // but rather a [0..1] value range.
/// };
///
/// action.Enable();
/// </code>
/// </example>
///
/// Note that pass-through actions do not perform any kind of disambiguation of input
/// which makes them great for just forwarding input from any connected controls but
/// makes them a poor choice when only one input should be generated from however
/// many connected controls there are. For more details, see <a
/// href="../manual/ActionBindings.html#conflicting-inputs">here</a>.
///
/// The other two behavior types are <see cref="Button"/> and <see cref="Value"/>.
///
/// A <see cref="Value"/> action starts (<see cref="InputAction.started"/>) as soon as its
/// input moves away from its default value. After that it immediately performs (<see cref="InputAction.performed"/>)
/// and every time the input changes value it performs again except if the input moves back
/// to the default value -- in which case the action cancels (<see cref="InputAction.canceled"/>).
///
/// Also, unlike both <see cref="Button"/> and <see cref="PassThrough"/> actions, <see cref="Value"/>
/// actions perform what's called "initial state check" on the first input update after the action
/// was enabled. What this does is check controls bound to the action and if they are already actuated
/// (that is, at non-default value), the action will immediately be started and performed. What
/// this means in practice is that when a value action is bound to, say, the left stick on a
/// gamepad and the stick is already moved out of its resting position, then the action will
/// immediately trigger instead of first requiring the stick to be moved slightly.
///
/// <see cref="Button"/> and <see cref="PassThrough"/> actions, on the other hand, perform
/// no such initial state check. For buttons, for example, this means that if a button is
/// already pressed when an action is enabled, it first has to be released and then
/// pressed again for the action to be triggered.
///
/// <example>
/// <code>
/// // An action that starts when the left stick on the gamepad is actuated
/// // and stops when the stick is released.
/// var action = new InputAction(
/// type: InputActionType.Value,
/// binding: "&lt;Gamepad&gt;/leftStick");
///
/// action.started +=
/// ctx =>
/// {
/// Debug.Log("--- Stick Starts ---");
/// };
/// action.performed +=
/// ctx =>
/// {
/// Debug.Log("Stick Value: " + ctx.ReadValue&lt;Vector2D&gt;();
/// };
/// action.canceled +=
/// ctx =>
/// {
/// Debug.Log("# Stick Released");
/// };
///
/// action.Enable();
/// </code>
/// </example>
///
/// A <see cref="Button"/> action essentially operates like a <see cref="Value"/> action except
/// that it does not perform an initial state check.
///
/// One final noteworthy difference of both <see cref="Button"/> and <see cref="Value"/> compared
/// to <see cref="PassThrough"/> is that both of them perform what is referred to as "disambiguation"
/// when multiple actions are bound to the control. <see cref="PassThrough"/> does not care how
/// many controls are bound to the action -- it simply passes every input through as is, no matter
/// where it comes from.
///
/// <see cref="Button"/> and <see cref="Value"/>, on the other hand, will treat input differently
/// if it is coming from several sources at the same time. Note that this can only happen when there
/// are multiple controls bound to a single actions -- either by a single binding resolving to
/// more than one control (e.g. <c>"*/{PrimaryAction}"</c>) or by multiple bindings all targeting
/// the same action and being active at the same time. If only a single control is bound to an
/// action, then the disambiguation code is automatically bypassed.
///
/// Disambiguation works the following way: when an action has not yet been started, it will react
/// to the first input that has a non-default value. Once it receives such an input, it will start
/// tracking the source of that input. While the action is in-progress, if it receives input from
/// a source other than the control it is currently tracking, it will check whether the input has
/// a greater magnitude (see <see cref="InputControl.EvaluateMagnitude()"/>) than the control the
/// action is already tracking. If so, the action will switch from its current control to the control
/// with the stronger input.
///
/// Note that this process does also works in reverse. When the control currently driving the action
/// lowers its value below that of another control that is also actuated and bound to the action,
/// the action will switch to that control.
///
/// Put simply, a <see cref="Button"/> or <see cref="Value"/> action bound to multiple controls will
/// always track the control with the strongest input.
/// </remarks>
/// <seealso cref="InputAction.type"/>
public enum InputActionType
{
/// <summary>
/// An action that reads a single value from its connected sources. If multiple bindings
/// actuate at the same time, performs disambiguation (see <see
/// href="../manual/ActionBindings.html#conflicting-inputs"/>) to detect the highest value contributor
/// at any one time.
///
/// A value action starts (<see cref="InputActionPhase.Started"/>) and then performs (<see cref="InputActionPhase.Performed"/>)
/// as soon as a bound control changes to a non-default value. For example, if an action is bound to <see cref="Gamepad.leftStick"/>
/// and the stick moves from (0,0) to (0.5,0.5), the action starts and performs.
///
/// After being started, the action will perform on every value change that is not the default value. In the example here, if
/// the stick goes to (0.75,0.75) and then to (1,1), the action will perform twice.
///
/// Finally, if the control value changes back to the default value, the action is canceled (<see cref="InputActionPhase.Canceled"/>).
/// Meaning that if the stick moves back to (0,0), <see cref="InputAction.canceled"/> will be triggered.
/// </summary>
Value,
/// <summary>
/// An action that acts as a trigger.
///
/// A button action has a defined trigger point that corresponds to <see cref="InputActionPhase.Performed"/>.
/// After being performed, the action goes back to waiting state to await the next triggering.
///
/// Note that a button action may still use <see cref="InputActionPhase.Started"/> and does not necessarily
/// trigger immediately on input. For example, if <see cref="Interactions.HoldInteraction"/> is used, the
/// action will start as soon as a bound button crosses its press threshold but will not trigger until the
/// button is held for the set hold duration (<see cref="Interactions.HoldInteraction.duration"/>).
///
/// Irrespective of which type an action is set to, it is possible to find out whether it was or is considered
/// pressed and/or released using <see cref="InputAction.IsPressed"/>, <see cref="InputAction.WasPressedThisFrame"/>,
/// and <see cref="InputAction.WasReleasedThisFrame"/>.
///
/// <example>
/// <code>
/// action.IsPressed();
/// action.WasPressedThisFrame();
/// action.WasReleasedThisFrame();
/// </code>
/// </example>
/// </summary>
Button,
/// <summary>
/// An action that has no specific type of behavior and instead acts as a simple pass-through for
/// any value change on any bound control. In effect, this turns an action from a single value producer into a mere
/// input "sink".
///
/// This is in some ways similar to <see cref="Value"/>. However, there are two key differences.
///
/// For one, the action will not perform any disambiguation when bound to multiple controls concurrently.
/// This means that if, for example, the action is bound to both the left and the right stick on a <see cref="Gamepad"/>,
/// and the left stick goes to (0.5,0.5) and the right stick then goes to (0.25,0.25), the action will perform
/// twice yielding a value of (0.5,0.5) first and a value of (0.25, 0.25) next. This is different from <see cref="Value"/>
/// where upon actuation to (0.5,0.5), the left stick would get to drive the action and the actuation of the right
/// stick would be ignored as it does not exceed the magnitude of the actuation on the left stick.
///
/// The second key difference is that only <see cref="InputActionPhase.Performed"/> is used and will get triggered
/// on every value change regardless of what the value is. This is different from <see cref="Value"/> where the
/// action will trigger <see cref="InputActionPhase.Started"/> when moving away from its default value and will
/// trigger <see cref="InputActionPhase.Canceled"/> when going back to the default value.
///
/// Note that a pass-through action my still get cancelled and thus see <see cref="InputAction.canceled"/> getting called.
/// This happens when a factor other than input on a device causes an action in progress to be cancelled. An example
/// of this is when an action is disabled (see <see cref="InputAction.Disable"/>) or when focus is lost (see <see cref="InputSettings.backgroundBehavior"/>)
/// and a device connection to an action is reset (see <see cref="InputSystem.ResetDevice"/>).
///
/// Also note that for a pass-through action, calling <see cref="InputAction.ReadValue{TValue}"/> is often not
/// very useful as it will only return the value of the very last control that fed into the action. For pass-through
/// actions, it is usually best to listen to <see cref="InputAction.performed"/> in order to be notified about every
/// single value change. Where this is not necessary, it is generally better to employ a <see cref="Value"/> action
/// instead.
/// </summary>
PassThrough,
}
}

View File

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

View File

@@ -0,0 +1,914 @@
using System;
using System.Linq;
using System.Text;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
////REVIEW: do we really need overridable processors and interactions?
// Downsides to the current approach:
// - Being able to address entire batches of controls through a single control is awesome. Especially
// when combining it type-kind of queries (e.g. "<MyDevice>/<Button>"). However, it complicates things
// in quite a few areas. There's quite a few bits in InputActionState that could be simplified if a
// binding simply maps to a control.
namespace UnityEngine.InputSystem
{
/// <summary>
/// A mapping of controls to an action.
/// </summary>
/// <remarks>
/// Each binding represents a value received from controls (see <see cref="InputControl"/>).
/// There are two main types of bindings: "normal" bindings and "composite" bindings.
///
/// Normal bindings directly bind to control(s) by means of <see cref="path"/> which is a "control path"
/// (see <see cref="InputControlPath"/> for details about how to form paths). At runtime, the
/// path of such a binding may match none, one, or multiple controls. Each control matched by the
/// path will feed input into the binding.
///
/// Composite bindings do not bind to controls themselves. Instead, they receive their input
/// from their "part" bindings and then return a value representing a "composition" of those
/// inputs. What composition specifically is performed depends on the type of the composite.
/// <see cref="Composites.AxisComposite"/>, for example, will return a floating-point axis value
/// computed from the state of two buttons.
///
/// The action that is triggered by a binding is determined by its <see cref="action"/> property.
/// The resolution to an <see cref="InputAction"/> depends on where the binding is used. For example,
/// bindings that are part of <see cref="InputActionMap.bindings"/> will resolve action names to
/// actions in the same <see cref="InputActionMap"/>.
///
/// A binding can also be used as a form of search mask or filter. In this use, <see cref="path"/>,
/// <see cref="action"/>, and <see cref="groups"/> become search criteria that are matched
/// against other bindings. See <see cref="Matches(InputBinding)"/> for details. This use
/// is employed in places such as <see cref="InputActionRebindingExtensions"/> as well as in
/// binding masks on actions (<see cref="InputAction.bindingMask"/>), action maps (<see
/// cref="InputActionMap.bindingMask"/>), and assets (<see cref="InputActionAsset.bindingMask"/>).
/// </remarks>
[Serializable]
public struct InputBinding : IEquatable<InputBinding>
{
/// <summary>
/// Character that is used to separate elements in places such as <see cref="groups"/>,
/// <see cref="interactions"/>, and <see cref="processors"/>.
/// </summary>
/// Some strings on bindings represent lists of elements. An example is <see cref="groups"/>
/// which may associate a binding with several binding groups, each one delimited by the
/// separator.
///
/// <remarks>
/// <example>
/// <code>
/// // A binding that belongs to the "Keyboard&amp;Mouse" and "Gamepad" group.
/// new InputBinding
/// {
/// path = "*/{PrimaryAction}",
/// groups = "Keyboard&amp;Mouse;Gamepad"
/// };
/// </code>
/// </example>
/// </remarks>
public const char Separator = ';';
internal const string kSeparatorString = ";";
/// <summary>
/// Optional name for the binding.
/// </summary>
/// <value>Name of the binding.</value>
/// <remarks>
/// For bindings that are part of composites (see <see cref="isPartOfComposite"/>), this is
/// the name of the field on the binding composite object that should be initialized with
/// the control target of the binding.
/// </remarks>
public string name
{
get => m_Name;
set => m_Name = value;
}
/// <summary>
/// Unique ID of the binding.
/// </summary>
/// <value>Unique ID of the binding.</value>
/// <remarks>
/// This can be used, for example, when storing binding overrides in local user configurations.
/// Using the binding ID, an override can remain associated with one specific binding.
/// </remarks>
public Guid id
{
get
{
////REVIEW: this is inconsistent with InputActionMap and InputAction which generate IDs, if necessary
if (string.IsNullOrEmpty(m_Id))
return default;
return new Guid(m_Id);
}
set => m_Id = value.ToString();
}
/// <summary>
/// Control path being bound to.
/// </summary>
/// <value>Path of control(s) to source input from.</value>
/// <remarks>
/// Bindings reference <see cref="InputControl"/>s using a regular expression-like
/// language. See <see cref="InputControlPath"/> for details.
///
/// If the binding is a composite (<see cref="isComposite"/>), the path is the composite
/// string instead. For example, for a <see cref="Composites.Vector2Composite"/>, the
/// path could be something like <c>"Vector2(normalize=false)"</c>.
///
/// The path of a binding may be non-destructively override at runtime using <see cref="overridePath"/>
/// which unlike this property is not serialized. <see cref="effectivePath"/> represents the
/// final, effective path.
/// </remarks>
/// <example>
/// <code>
/// // A binding that references the left mouse button.
/// new InputBinding { path = "&lt;Mouse&gt;/leftButton" }
/// </code>
/// </example>
/// <seealso cref="overridePath"/>
/// <seealso cref="InputControlPath"/>
/// <seealso cref="InputControlPath.Parse"/>
/// <seealso cref="InputControl.path"/>
/// <seealso cref="InputSystem.FindControl"/>
public string path
{
get => m_Path;
set => m_Path = value;
}
/// <summary>
/// If the binding is overridden, this is the overriding path.
/// Otherwise it is <c>null</c>.
/// </summary>
/// <value>Path to override the <see cref="path"/> property with.</value>
/// <remarks>
/// Unlike the <see cref="path"/> property, the value of the override path is not serialized.
/// If set, it will take precedence and determine the result of <see cref="effectivePath"/>.
///
/// This property can be set to an empty string to disable the binding. During resolution,
/// bindings with an empty <see cref="effectivePath"/> will get skipped.
///
/// To set the override on an existing binding, use the methods supplied by <see cref="InputActionRebindingExtensions"/>
/// such as <see cref="InputActionRebindingExtensions.ApplyBindingOverride(InputAction,string,string,string)"/>.
///
/// <example>
/// <code>
/// // Override the binding to &lt;Gamepad&gt;/buttonSouth on
/// // myAction with a binding to &lt;Gamepad&gt;/buttonNorth.
/// myAction.ApplyBindingOverride(
/// new InputBinding
/// {
/// path = "&lt;Gamepad&gt;/buttonSouth",
/// overridePath = "&lt;Gamepad&gt;/buttonNorth"
/// });
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="path"/>
/// <seealso cref="overrideInteractions"/>
/// <seealso cref="overrideProcessors"/>
/// <seealso cref="hasOverrides"/>
/// <seealso cref="InputActionRebindingExtensions.SaveBindingOverridesAsJson(IInputActionCollection2)"/>
/// <seealso cref="InputActionRebindingExtensions.LoadBindingOverridesFromJson(IInputActionCollection2,string,bool)"/>
/// <seealso cref="InputActionRebindingExtensions.ApplyBindingOverride(InputAction,int,InputBinding)"/>
public string overridePath
{
get => m_OverridePath;
set => m_OverridePath = value;
}
/// <summary>
/// Optional list of interactions and their parameters.
/// </summary>
/// <value>Interactions to put on the binding.</value>
/// <remarks>
/// Each element in the list is a name of an interaction (as registered with
/// <see cref="InputSystem.RegisterInteraction{T}"/>) followed by an optional
/// list of parameters.
///
/// For example, <c>"slowTap(duration=1.2,pressPoint=0.123)"</c> is one element
/// that puts a <see cref="Interactions.SlowTapInteraction"/> on the binding and
/// sets <see cref="Interactions.SlowTapInteraction.duration"/> to 1.2 and
/// <see cref="Interactions.SlowTapInteraction.pressPoint"/> to 0.123.
///
/// Multiple interactions can be put on a binding by separating them with a comma.
/// For example, <c>"tap,slowTap(duration=1.2)"</c> puts both a
/// <see cref="Interactions.TapInteraction"/> and <see cref="Interactions.SlowTapInteraction"/>
/// on the binding. See <see cref="IInputInteraction"/> for why the order matters.
/// </remarks>
/// <seealso cref="IInputInteraction"/>
/// <seealso cref="overrideInteractions"/>
/// <seealso cref="hasOverrides"/>
/// <seealso cref="InputActionRebindingExtensions.SaveBindingOverridesAsJson(IInputActionCollection2)"/>
/// <seealso cref="InputActionRebindingExtensions.LoadBindingOverridesFromJson(IInputActionCollection2,string,bool)"/>
/// <seealso cref="InputActionRebindingExtensions.ApplyBindingOverride(InputAction,int,InputBinding)"/>
public string interactions
{
get => m_Interactions;
set => m_Interactions = value;
}
/// <summary>
/// Interaction settings to override <see cref="interactions"/> with.
/// </summary>
/// <value>Override string for <see cref="interactions"/> or <c>null</c>.</value>
/// <remarks>
/// If this is not <c>null</c>, it replaces the value of <see cref="interactions"/>.
/// </remarks>
/// <seealso cref="effectiveInteractions"/>
/// <seealso cref="interactions"/>
/// <seealso cref="overridePath"/>
/// <seealso cref="overrideProcessors"/>
/// <seealso cref="hasOverrides"/>
/// <seealso cref="InputActionRebindingExtensions.SaveBindingOverridesAsJson(IInputActionCollection2)"/>
/// <seealso cref="InputActionRebindingExtensions.LoadBindingOverridesFromJson(IInputActionCollection2,string,bool)"/>
/// <seealso cref="InputActionRebindingExtensions.ApplyBindingOverride(InputAction,int,InputBinding)"/>
public string overrideInteractions
{
get => m_OverrideInteractions;
set => m_OverrideInteractions = value;
}
/// <summary>
/// Optional list of processors to apply to control values.
/// </summary>
/// <value>Value processors to apply to the binding.</value>
/// <remarks>
/// This string has the same format as <see cref="InputControlAttribute.processors"/>.
/// </remarks>
/// <seealso cref="InputProcessor{TValue}"/>
/// <seealso cref="overrideProcessors"/>
public string processors
{
get => m_Processors;
set => m_Processors = value;
}
/// <summary>
/// Processor settings to override <see cref="processors"/> with.
/// </summary>
/// <value>Override string for <see cref="processors"/> or <c>null</c>.</value>
/// <remarks>
/// If this is not <c>null</c>, it replaces the value of <see cref="processors"/>.
/// </remarks>
/// <seealso cref="effectiveProcessors"/>
/// <seealso cref="processors"/>
/// <seealso cref="overridePath"/>
/// <seealso cref="overrideInteractions"/>
/// <seealso cref="hasOverrides"/>
public string overrideProcessors
{
get => m_OverrideProcessors;
set => m_OverrideProcessors = value;
}
/// <summary>
/// Optional list of binding groups that the binding belongs to.
/// </summary>
/// <value>List of binding groups or <c>null</c>.</value>
/// <remarks>
/// This is used, for example, to divide bindings into <see cref="InputControlScheme"/>s.
/// Each control scheme is associated with a unique binding group through <see
/// cref="InputControlScheme.bindingGroup"/>.
///
/// A binding may be associated with multiple groups by listing each group name
/// separate by a semicolon (<see cref="Separator"/>).
///
/// <example>
/// <code>
/// new InputBinding
/// {
/// path = "*/{PrimaryAction}",
/// // Associate the binding both with the "KeyboardMouse" and
/// // the "Gamepad" group.
/// groups = "KeyboardMouse;Gamepad",
/// }
/// </code>
/// </example>
///
/// Note that the system places no restriction on what binding groups are used
/// for in practice. Their use by <see cref="InputControlScheme"/> is only one
/// possible one, but which groups to apply and how to use them is ultimately
/// up to you.
/// </remarks>
/// <seealso cref="InputControlScheme.bindingGroup"/>
public string groups
{
get => m_Groups;
set => m_Groups = value;
}
/// <summary>
/// Name or ID of the action triggered by the binding.
/// </summary>
/// <remarks>
/// This is null if the binding does not trigger an action.
///
/// For InputBindings that are used as masks, this can be a "mapName/actionName" combination
/// or "mapName/*" to match all actions in the given map.
/// </remarks>
/// <seealso cref="InputAction.name"/>
/// <seealso cref="InputAction.id"/>
public string action
{
get => m_Action;
set => m_Action = value;
}
/// <summary>
/// Whether the binding is a composite.
/// </summary>
/// <value>True if the binding is a composite.</value>
/// <remarks>
/// Composite bindings to not bind to controls to themselves but rather source their
/// input from one or more "part binding" (see <see cref="isPartOfComposite"/>).
///
/// See <see cref="InputBindingComposite{TValue}"/> for more details.
/// </remarks>
/// <seealso cref="InputBindingComposite{TValue}"/>
public bool isComposite
{
get => (m_Flags & Flags.Composite) == Flags.Composite;
set
{
if (value)
m_Flags |= Flags.Composite;
else
m_Flags &= ~Flags.Composite;
}
}
/// <summary>
/// Whether the binding is a "part binding" of a composite.
/// </summary>
/// <value>True if the binding is part of a composite.</value>
/// <remarks>
/// The bindings that make up a composite are laid out sequentially in <see cref="InputActionMap.bindings"/>.
/// First comes the composite itself which is flagged with <see cref="isComposite"/>. It mentions
/// the composite and its parameters in its <see cref="path"/> property. After the composite itself come
/// the part bindings. All subsequent bindings marked as <c>isPartOfComposite</c> will be associated
/// with the composite.
/// </remarks>
/// <seealso cref="isComposite"/>
/// <seealso cref="InputBindingComposite{TValue}"/>
public bool isPartOfComposite
{
get => (m_Flags & Flags.PartOfComposite) == Flags.PartOfComposite;
set
{
if (value)
m_Flags |= Flags.PartOfComposite;
else
m_Flags &= ~Flags.PartOfComposite;
}
}
/// <summary>
/// True if any of the override properties, that is, <see cref="overridePath"/>, <see cref="overrideProcessors"/>,
/// and/or <see cref="overrideInteractions"/>, are set (not <c>null</c>).
/// </summary>
public bool hasOverrides => overridePath != null || overrideProcessors != null || overrideInteractions != null;
/// <summary>
/// Initialize a new binding.
/// </summary>
/// <param name="path">Path for the binding.</param>
/// <param name="action">Action to trigger from the binding.</param>
/// <param name="groups">Semicolon-separated list of binding <see cref="InputBinding.groups"/> the binding is associated with.</param>
/// <param name="processors">Comma-separated list of <see cref="InputBinding.processors"/> to apply to the binding.</param>
/// <param name="interactions">Comma-separated list of <see cref="InputBinding.interactions"/> to apply to the
/// binding.</param>
/// <param name="name">Optional name for the binding.</param>
public InputBinding(string path, string action = null, string groups = null, string processors = null,
string interactions = null, string name = null)
{
m_Path = path;
m_Action = action;
m_Groups = groups;
m_Processors = processors;
m_Interactions = interactions;
m_Name = name;
m_Id = default;
m_Flags = default;
m_OverridePath = default;
m_OverrideInteractions = default;
m_OverrideProcessors = default;
}
public string GetNameOfComposite()
{
if (!isComposite)
return null;
return NameAndParameters.Parse(effectivePath).name;
}
internal void GenerateId()
{
m_Id = Guid.NewGuid().ToString();
}
internal void RemoveOverrides()
{
m_OverridePath = null;
m_OverrideInteractions = null;
m_OverrideProcessors = null;
}
public static InputBinding MaskByGroup(string group)
{
return new InputBinding {groups = group};
}
public static InputBinding MaskByGroups(params string[] groups)
{
return new InputBinding {groups = string.Join(kSeparatorString, groups.Where(x => !string.IsNullOrEmpty(x)))};
}
[SerializeField] private string m_Name;
[SerializeField] internal string m_Id;
[Tooltip("Path of the control to bind to. Matched at runtime to controls from InputDevices present at the time.\n\nCan either be "
+ "graphically from the control picker dropdown UI or edited manually in text mode by clicking the 'T' button. Internally, both "
+ "methods result in control path strings that look like, for example, \"<Gamepad>/buttonSouth\".")]
[SerializeField] private string m_Path;
[SerializeField] private string m_Interactions;
[SerializeField] private string m_Processors;
[SerializeField] internal string m_Groups;
[SerializeField] private string m_Action;
[SerializeField] internal Flags m_Flags;
[NonSerialized] private string m_OverridePath;
[NonSerialized] private string m_OverrideInteractions;
[NonSerialized] private string m_OverrideProcessors;
/// <summary>
/// This is the bindings path which is effectively being used.
/// </summary>
/// <remarks>
/// This is either <see cref="overridePath"/> if that is set, or <see cref="path"/> otherwise.
/// </remarks>
public string effectivePath => overridePath ?? path;
/// <summary>
/// This is the interaction config which is effectively being used.
/// </summary>
/// <remarks>
/// This is either <see cref="overrideInteractions"/> if that is set, or <see cref="interactions"/> otherwise.
/// </remarks>
public string effectiveInteractions => overrideInteractions ?? interactions;
/// <summary>
/// This is the processor config which is effectively being used.
/// </summary>
/// <remarks>
/// This is either <see cref="overrideProcessors"/> if that is set, or <see cref="processors"/> otherwise.
/// </remarks>
public string effectiveProcessors => overrideProcessors ?? processors;
internal bool isEmpty =>
string.IsNullOrEmpty(effectivePath) && string.IsNullOrEmpty(action) &&
string.IsNullOrEmpty(groups);
/// <summary>
/// Check whether the binding is equivalent to the given binding.
/// </summary>
/// <param name="other">Another binding.</param>
/// <returns>True if the two bindings are equivalent.</returns>
/// <remarks>
/// Bindings are equivalent if their <see cref="effectivePath"/>, <see cref="effectiveInteractions"/>,
/// and <see cref="effectiveProcessors"/>, plus their <see cref="action"/> and <see cref="groups"/>
/// properties are the same. Note that the string comparisons ignore both case and culture.
/// </remarks>
public bool Equals(InputBinding other)
{
return string.Equals(effectivePath, other.effectivePath, StringComparison.InvariantCultureIgnoreCase) &&
string.Equals(effectiveInteractions, other.effectiveInteractions, StringComparison.InvariantCultureIgnoreCase) &&
string.Equals(effectiveProcessors, other.effectiveProcessors, StringComparison.InvariantCultureIgnoreCase) &&
string.Equals(groups, other.groups, StringComparison.InvariantCultureIgnoreCase) &&
string.Equals(action, other.action, StringComparison.InvariantCultureIgnoreCase);
}
/// <summary>
/// Compare the binding to the given object.
/// </summary>
/// <param name="obj">An object. May be <c>null</c>.</param>
/// <returns>True if the given object is an <c>InputBinding</c> that equals this one.</returns>
/// <seealso cref="Equals(InputBinding)"/>
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
return false;
return obj is InputBinding binding && Equals(binding);
}
/// <summary>
/// Compare the two bindings for equality.
/// </summary>
/// <param name="left">The first binding.</param>
/// <param name="right">The second binding.</param>
/// <returns>True if the two bindings are equal.</returns>
/// <seealso cref="Equals(InputBinding)"/>
public static bool operator==(InputBinding left, InputBinding right)
{
return left.Equals(right);
}
/// <summary>
/// Compare the two bindings for inequality.
/// </summary>
/// <param name="left">The first binding.</param>
/// <param name="right">The second binding.</param>
/// <returns>True if the two bindings are not equal.</returns>
/// <seealso cref="Equals(InputBinding)"/>
public static bool operator!=(InputBinding left, InputBinding right)
{
return !(left == right);
}
/// <summary>
/// Compute a hash code for the binding.
/// </summary>
/// <returns>A hash code.</returns>
public override int GetHashCode()
{
unchecked
{
var hashCode = (effectivePath != null ? effectivePath.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (effectiveInteractions != null ? effectiveInteractions.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (effectiveProcessors != null ? effectiveProcessors.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (groups != null ? groups.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (action != null ? action.GetHashCode() : 0);
return hashCode;
}
}
/// <summary>
/// Return a string representation of the binding useful for debugging.
/// </summary>
/// <returns>A string representation of the binding.</returns>
/// <example>
/// <code>
/// var binding = new InputBinding
/// {
/// action = "fire",
/// path = "&lt;Gamepad&gt;/buttonSouth",
/// groups = "Gamepad"
/// };
///
/// // Returns "fire: &lt;Gamepad&gt;/buttonSouth [Gamepad]".
/// binding.ToString();
/// </code>
/// </example>
public override string ToString()
{
var builder = new StringBuilder();
// Add action.
if (!string.IsNullOrEmpty(action))
{
builder.Append(action);
builder.Append(':');
}
// Add path.
var path = effectivePath;
if (!string.IsNullOrEmpty(path))
builder.Append(path);
// Add groups.
if (!string.IsNullOrEmpty(groups))
{
builder.Append('[');
builder.Append(groups);
builder.Append(']');
}
return builder.ToString();
}
/// <summary>
/// A set of flags to turn individual default behaviors of <see cref="InputBinding.ToDisplayString(DisplayStringOptions,InputControl)"/> off.
/// </summary>
[Flags]
public enum DisplayStringOptions
{
/// <summary>
/// Do not use short names of controls as set up by <see cref="InputControlAttribute.shortDisplayName"/>
/// and the <c>"shortDisplayName"</c> property in JSON. This will, for example, not use LMB instead of "left Button"
/// on <see cref="Mouse.leftButton"/>.
/// </summary>
DontUseShortDisplayNames = 1 << 0,
/// <summary>
/// By default device names are omitted from display strings. With this option, they are included instead.
/// For example, <c>"A"</c> will be <c>"A [Gamepad]"</c> instead.
/// </summary>
DontOmitDevice = 1 << 1,
/// <summary>
/// By default, interactions on bindings are included in the resulting display string. For example, a binding to
/// the gamepad's A button that has a "Hold" interaction on it, would come out as "Hold A". This can be suppressed
/// with this option in which case the same setup would come out as just "A".
/// </summary>
DontIncludeInteractions = 1 << 2,
/// <summary>
/// By default, <see cref="effectivePath"/> is used for generating a display name. Using this option, the display
/// string can be forced to <see cref="path"/> instead.
/// </summary>
IgnoreBindingOverrides = 1 << 3,
}
/// <summary>
/// Turn the binding into a string suitable for display in a UI.
/// </summary>
/// <param name="options">Optional set of formatting options.</param>
/// <param name="control">Optional control to which the binding has been resolved. If this is supplied,
/// the resulting string can reflect things such as the current keyboard layout or hardware/platform-specific
/// naming of controls (e.g. Xbox vs PS4 controllers as opposed to naming things generically based on the
/// <see cref="Gamepad"/> layout).</param>
/// <returns>A string representation of the binding suitable for display in a UI.</returns>
/// <remarks>
/// This method works only for bindings that are not composites. If the method is called on a binding
/// that is a composite (<see cref="isComposite"/> is true), an empty string will be returned. To automatically
/// handle composites, use <see cref="InputActionRebindingExtensions.GetBindingDisplayString(InputAction,DisplayStringOptions,string)"/>
/// instead.
///
/// <example>
/// <code>
/// var gamepadBinding = new InputBinding("&lt;Gamepad&gt;/buttonSouth");
/// var mouseBinding = new InputBinding("&lt;Mouse&gt;/leftButton");
/// var keyboardBinding = new InputBinding("&lt;Keyboard&gt;/a");
///
/// // Prints "A" except on PS4 where it prints "Cross".
/// Debug.Log(gamepadBinding.ToDisplayString());
///
/// // Prints "LMB".
/// Debug.Log(mouseBinding.ToDisplayString());
///
/// // Print "Left Button".
/// Debug.Log(mouseBinding.ToDisplayString(DisplayStringOptions.DontUseShortDisplayNames));
///
/// // Prints the character associated with the "A" key on the current keyboard layout.
/// Debug.Log(keyboardBinding, control: Keyboard.current);
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputControlPath.ToHumanReadableString(string,InputControlPath.HumanReadableStringOptions,InputControl)"/>
/// <seealso cref="InputActionRebindingExtensions.GetBindingDisplayString(InputAction,int,InputBinding.DisplayStringOptions)"/>
public string ToDisplayString(DisplayStringOptions options = default, InputControl control = default)
{
return ToDisplayString(out var _, out var _, options, control);
}
/// <summary>
/// Turn the binding into a string suitable for display in a UI.
/// </summary>
/// <param name="options">Optional set of formatting options.</param>
/// <param name="control">Optional control to which the binding has been resolved. If this is supplied,
/// the resulting string can reflect things such as the current keyboard layout or hardware/platform-specific
/// naming of controls (e.g. Xbox vs PS4 controllers as opposed to naming things generically based on the
/// <see cref="Gamepad"/> layout).</param>
/// <returns>A string representation of the binding suitable for display in a UI.</returns>
/// <remarks>
/// This method is the same as <see cref="ToDisplayString(DisplayStringOptions,InputControl)"/> except that it
/// will also return the name of the device layout and path of the control, if applicable to the binding. This is
/// useful when needing more context on the resulting display string, for example to decide on an icon to display
/// instead of the textual display string.
///
/// <example>
/// <code>
/// var displayString = new InputBinding("&lt;Gamepad&gt;/dpad/up")
/// .ToDisplayString(out deviceLayout, out controlPath);
///
/// // Will print "D-Pad Up".
/// Debug.Log(displayString);
///
/// // Will print "Gamepad".
/// Debug.Log(deviceLayout);
///
/// // Will print "dpad/up".
/// Debug.Log(controlPath);
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputControlPath.ToHumanReadableString(string,out string,out string,InputControlPath.HumanReadableStringOptions,InputControl)"/>
/// <seealso cref="InputActionRebindingExtensions.GetBindingDisplayString(InputAction,int,out string,out string,InputBinding.DisplayStringOptions)"/>
public string ToDisplayString(out string deviceLayoutName, out string controlPath, DisplayStringOptions options = default,
InputControl control = default)
{
if (isComposite)
{
deviceLayoutName = null;
controlPath = null;
return string.Empty;
}
var readableStringOptions = default(InputControlPath.HumanReadableStringOptions);
if ((options & DisplayStringOptions.DontOmitDevice) == 0)
readableStringOptions |= InputControlPath.HumanReadableStringOptions.OmitDevice;
if ((options & DisplayStringOptions.DontUseShortDisplayNames) == 0)
readableStringOptions |= InputControlPath.HumanReadableStringOptions.UseShortNames;
var pathToUse = (options & DisplayStringOptions.IgnoreBindingOverrides) != 0
? path
: effectivePath;
var result = InputControlPath.ToHumanReadableString(pathToUse, out deviceLayoutName, out controlPath, readableStringOptions, control);
if (!string.IsNullOrEmpty(effectiveInteractions) && (options & DisplayStringOptions.DontIncludeInteractions) == 0)
{
var interactionString = string.Empty;
var parsedInteractions = NameAndParameters.ParseMultiple(effectiveInteractions);
foreach (var element in parsedInteractions)
{
var interaction = element.name;
var interactionDisplayName = InputInteraction.GetDisplayName(interaction);
// An interaction can avoid being mentioned explicitly by just setting its display
// name to an empty string.
if (string.IsNullOrEmpty(interactionDisplayName))
continue;
////TODO: this will need support for localization
if (!string.IsNullOrEmpty(interactionString))
interactionString = $"{interactionString} or {interactionDisplayName}";
else
interactionString = interactionDisplayName;
}
if (!string.IsNullOrEmpty(interactionString))
result = $"{interactionString} {result}";
}
return result;
}
internal bool TriggersAction(InputAction action)
{
// Match both name and ID on binding.
return string.Compare(action.name, this.action, StringComparison.InvariantCultureIgnoreCase) == 0
|| this.action == action.m_Id;
}
////TODO: also support matching by name (taking the binding tree into account so that components
//// of composites can be referenced through their parent)
/// <summary>
/// Check whether <paramref name="binding"/> matches the mask
/// represented by the current binding.
/// </summary>
/// <param name="binding">An input binding.</param>
/// <returns>True if <paramref name="binding"/> is matched by the mask represented
/// by <c>this</c>.</returns>
/// <remarks>
/// In this method, the current binding acts as a "mask". When used this way, only
/// three properties of the binding are taken into account: <see cref="path"/>,
/// <see cref="groups"/>, and <see cref="action"/>.
///
/// For each of these properties, the method checks whether they are set on the current
/// binding and, if so, matches them against the respective property in <paramref name="binding"/>.
///
/// The way this matching works is that the value of the property in the current binding is
/// allowed to be a semicolon-separated list where each element specifies one possible value
/// that will produce a match.
///
/// Note that all comparisons are case-insensitive.
///
/// <example>
/// <code>
/// // Create a couple bindings which we can test against.
/// var keyboardBinding = new InputBinding
/// {
/// path = "&lt;Keyboard&gt;/space",
/// groups = "Keyboard",
/// action = "Fire"
/// };
/// var gamepadBinding = new InputBinding
/// {
/// path = "&lt;Gamepad&gt;/buttonSouth",
/// groups = "Gamepad",
/// action = "Jump"
/// };
/// var touchBinding = new InputBinding
/// {
/// path = "&lt;Touchscreen&gt;/*/tap",
/// groups = "Touch",
/// action = "Jump"
/// };
///
/// // Example 1: Match any binding in the "Keyboard" or "Gamepad" group.
/// var mask1 = new InputBinding
/// {
/// // We put two elements in the list here and separate them with a semicolon.
/// groups = "Keyboard;Gamepad"
/// };
///
/// mask1.Matches(keyboardBinding); // True
/// mask1.Matches(gamepadBinding); // True
/// mask1.Matches(touchBinding); // False
///
/// // Example 2: Match any binding to the "Jump" or the "Roll" action
/// // (the latter we don't actually have a binding for)
/// var mask2 = new InputBinding
/// {
/// action = "Jump;Roll"
/// };
///
/// mask2.Matches(keyboardBinding); // False
/// mask2.Matches(gamepadBinding); // True
/// mask2.Matches(touchBinding); // True
///
/// // Example: Match any binding to the space or enter key in the
/// // "Keyboard" group.
/// var mask3 = new InputBinding
/// {
/// path = "&lt;Keyboard&gt;/space;&lt;Keyboard&gt;/enter",
/// groups = "Keyboard"
/// };
///
/// mask3.Matches(keyboardBinding); // True
/// mask3.Matches(gamepadBinding); // False
/// mask3.Matches(touchBinding); // False
/// </code>
/// </example>
/// </remarks>
public bool Matches(InputBinding binding)
{
return Matches(ref binding);
}
// Internally we pass by reference to not unnecessarily copy the struct.
internal bool Matches(ref InputBinding binding, MatchOptions options = default)
{
// Match name.
if (!string.IsNullOrEmpty(name))
{
if (string.IsNullOrEmpty(binding.name)
|| !StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(name, binding.name, Separator))
return false;
}
// Match path.
if (path != null)
{
////REVIEW: should this use binding.effectivePath?
////REVIEW: should this support matching by prefix only? should this use control path matching instead of just string comparisons?
////TODO: handle things like ignoring leading '/'
if (binding.path == null
|| !StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(path, binding.path, Separator))
return false;
}
// Match action.
if (action != null)
{
////TODO: handle "map/action" format
////TODO: handle "map/*" format
////REVIEW: this will not be able to handle cases where one binding references an action by ID and the other by name but both do mean the same action
if (binding.action == null
|| !StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(action, binding.action, Separator))
return false;
}
// Match groups.
if (groups != null)
{
var haveGroupsOnBinding = !string.IsNullOrEmpty(binding.groups);
if (!haveGroupsOnBinding && (options & MatchOptions.EmptyGroupMatchesAny) == 0)
return false;
if (haveGroupsOnBinding
&& !StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(groups, binding.groups, Separator))
return false;
}
// Match ID.
if (!string.IsNullOrEmpty(m_Id))
{
if (binding.id != id)
return false;
}
return true;
}
[Flags]
internal enum MatchOptions
{
EmptyGroupMatchesAny = 1 << 0,
}
[Flags]
internal enum Flags
{
None = 0,
Composite = 1 << 2,
PartOfComposite = 1 << 3,
}
}
}

View File

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

View File

@@ -0,0 +1,366 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.Scripting;
////TODO: support nested composites
////REVIEW: composites probably need a reset method, too (like interactions), so that they can be stateful
////REVIEW: isn't this about arbitrary value processing? can we open this up more and make it
//// not just be about composing multiple bindings?
////REVIEW: when we get blittable type constraints, we can probably do away with the pointer-based ReadValue version
namespace UnityEngine.InputSystem
{
////TODO: clarify whether this can have state or not
/// <summary>
/// A binding that synthesizes a value from from several component bindings.
/// </summary>
/// <remarks>
/// This is the base class for composite bindings. See <see cref="InputBindingComposite{TValue}"/>
/// for more details about composites and for how to define custom composites.
/// </remarks>
/// <seealso cref="InputSystem.RegisterBindingComposite{T}"/>
/// <seealso cref="InputActionRebindingExtensions.GetParameterValue(InputAction,string,InputBinding)"/>
/// <seealso cref="InputActionRebindingExtensions.ApplyParameterOverride(InputActionMap,string,PrimitiveValue,InputBinding)"/>
/// <seealso cref="InputBinding.isComposite"/>
public abstract class InputBindingComposite
{
/// <summary>
/// The type of value returned by the composite.
/// </summary>
/// <value>Type of value returned by the composite.</value>
/// <remarks>
/// Just like each <see cref="InputControl"/> has a specific type of value it
/// will return, each composite has a specific type of value it will return.
/// This is usually implicitly defined by the type parameter of <see
/// cref="InputBindingComposite{TValue}"/>.
/// </remarks>
/// <seealso cref="InputControl.valueType"/>
/// <seealso cref="InputAction.CallbackContext.valueType"/>
public abstract Type valueType { get; }
/// <summary>
/// Size of a value read by <see cref="ReadValue"/>.
/// </summary>
/// <value>Size of values stored in memory buffers by <see cref="ReadValue"/>.</value>
/// <remarks>
/// This is usually implicitly defined by the size of values derived
/// from the type argument to <see cref="InputBindingComposite{TValue}"/>. E.g.
/// if the type argument is <c>Vector2</c>, this property will be 8.
/// </remarks>
/// <seealso cref="InputControl.valueSizeInBytes"/>
/// <seealso cref="InputAction.CallbackContext.valueSizeInBytes"/>
public abstract int valueSizeInBytes { get; }
/// <summary>
/// Read a value from the composite without having to know the value type (unlike
/// <see cref="InputBindingComposite{TValue}.ReadValue(ref InputBindingCompositeContext)"/> and
/// without allocating GC heap memory (unlike <see cref="ReadValueAsObject"/>).
/// </summary>
/// <param name="context">Callback context for the binding composite. Use this
/// to access the values supplied by part bindings.</param>
/// <param name="buffer">Buffer that receives the value read for the composite.</param>
/// <param name="bufferSize">Size of the buffer allocated at <paramref name="buffer"/>.</param>
/// <exception cref="ArgumentException"><paramref name="bufferSize"/> is smaller than
/// <see cref="valueSizeInBytes"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="buffer"/> is <c>null</c>.</exception>
/// <remarks>
/// This API will be used if someone calls <see cref="InputAction.CallbackContext.ReadValue(void*,int)"/>
/// with the action leading to the composite.
///
/// By deriving from <see cref="InputBindingComposite{TValue}"/>, this will automatically
/// be implemented for you.
/// </remarks>
/// <seealso cref="InputAction.CallbackContext.ReadValue"/>
public abstract unsafe void ReadValue(ref InputBindingCompositeContext context, void* buffer, int bufferSize);
/// <summary>
/// Read the value of the composite as a boxed object. This allows reading the value
/// without having to know the value type and without having to deal with raw byte buffers.
/// </summary>
/// <param name="context">Callback context for the binding composite. Use this
/// to access the values supplied by part bindings.</param>
/// <returns>The current value of the composite according to the state passed in through
/// <paramref name="context"/>.</returns>
/// <remarks>
/// This API will be used if someone calls <see cref="InputAction.CallbackContext.ReadValueAsObject"/>
/// with the action leading to the composite.
///
/// By deriving from <see cref="InputBindingComposite{TValue}"/>, this will automatically
/// be implemented for you.
/// </remarks>
public abstract object ReadValueAsObject(ref InputBindingCompositeContext context);
/// <summary>
/// Determine the current level of actuation of the composite.
/// </summary>
/// <param name="context">Callback context for the binding composite. Use this
/// to access the values supplied by part bindings.</param>
/// <returns></returns>
/// <remarks>
/// This method by default returns -1, meaning that the composite does not support
/// magnitudes. You can override the method to add support for magnitudes.
///
/// See <see cref="InputControl.EvaluateMagnitude()"/> for details of how magnitudes
/// work.
/// </remarks>
/// <seealso cref="InputControl.EvaluateMagnitude()"/>
public virtual float EvaluateMagnitude(ref InputBindingCompositeContext context)
{
return -1;
}
/// <summary>
/// Called after binding resolution for an <see cref="InputActionMap"/> is complete.
/// </summary>
/// <remarks>
/// Some composites do not have predetermine value types. Two examples of this are
/// <see cref="Composites.OneModifierComposite"/> and <see cref="Composites.TwoModifiersComposite"/>, which
/// both have a <c>"binding"</c> part that can be bound to arbitrary controls. This means that the
/// value type of these bindings can only be determined at runtime.
///
/// Overriding this method allows accessing the actual controls bound to each part
/// at runtime.
///
/// <example>
/// <code>
/// [InputControl] public int binding;
///
/// protected override void FinishSetup(ref InputBindingContext context)
/// {
/// // Get all controls bound to the 'binding' part.
/// var controls = context.controls
/// .Where(x => x.part == binding)
/// .Select(x => x.control);
/// }
/// </code>
/// </example>
/// </remarks>
protected virtual void FinishSetup(ref InputBindingCompositeContext context)
{
}
// Avoid having to expose internal modifier.
internal void CallFinishSetup(ref InputBindingCompositeContext context)
{
FinishSetup(ref context);
}
internal static TypeTable s_Composites;
internal static Type GetValueType(string composite)
{
if (string.IsNullOrEmpty(composite))
throw new ArgumentNullException(nameof(composite));
var compositeType = s_Composites.LookupTypeRegistration(composite);
if (compositeType == null)
return null;
return TypeHelpers.GetGenericTypeArgumentFromHierarchy(compositeType, typeof(InputBindingComposite<>), 0);
}
/// <summary>
/// Return the name of the control layout that is expected for the given part (e.g. "Up") on the given
/// composite (e.g. "Dpad").
/// </summary>
/// <param name="composite">Registration name of the composite.</param>
/// <param name="part">Name of the part.</param>
/// <returns>The layout name (such as "Button") expected for the given part on the composite or null if
/// there is no composite with the given name or no part on the composite with the given name.</returns>
/// <remarks>
/// Expected control layouts can be set on composite parts by setting the <see cref="InputControlAttribute.layout"/>
/// property on them.
/// </remarks>
/// <example>
/// <code>
/// InputBindingComposite.GetExpectedControlLayoutName("Dpad", "Up") // Returns "Button"
///
/// // This is how Dpad communicates that:
/// [InputControl(layout = "Button")] public int up;
/// </code>
/// </example>
public static string GetExpectedControlLayoutName(string composite, string part)
{
if (string.IsNullOrEmpty(composite))
throw new ArgumentNullException(nameof(composite));
if (string.IsNullOrEmpty(part))
throw new ArgumentNullException(nameof(part));
var compositeType = s_Composites.LookupTypeRegistration(composite);
if (compositeType == null)
return null;
////TODO: allow it being properties instead of just fields
var field = compositeType.GetField(part,
BindingFlags.Instance | BindingFlags.IgnoreCase | BindingFlags.Public);
if (field == null)
return null;
var attribute = field.GetCustomAttribute<InputControlAttribute>(false);
return attribute?.layout;
}
internal static IEnumerable<string> GetPartNames(string composite)
{
if (string.IsNullOrEmpty(composite))
throw new ArgumentNullException(nameof(composite));
var compositeType = s_Composites.LookupTypeRegistration(composite);
if (compositeType == null)
yield break;
foreach (var field in compositeType.GetFields(BindingFlags.Instance | BindingFlags.Public))
{
var controlAttribute = field.GetCustomAttribute<InputControlAttribute>();
if (controlAttribute != null)
yield return field.Name;
}
}
internal static string GetDisplayFormatString(string composite)
{
if (string.IsNullOrEmpty(composite))
throw new ArgumentNullException(nameof(composite));
var compositeType = s_Composites.LookupTypeRegistration(composite);
if (compositeType == null)
return null;
var displayFormatAttribute = compositeType.GetCustomAttribute<DisplayStringFormatAttribute>();
if (displayFormatAttribute == null)
return null;
return displayFormatAttribute.formatString;
}
}
/// <summary>
/// A binding composite arranges several bindings such that they form a "virtual control".
/// </summary>
/// <typeparam name="TValue">Type of value returned by the composite. This must be a "blittable"
/// type, that is, a type whose values can simply be copied around.</typeparam>
/// <remarks>
/// Composite bindings are a special type of <see cref="InputBinding"/>. Whereas normally
/// an input binding simply references a set of controls and returns whatever input values are
/// generated by those controls, a composite binding sources input from several controls and
/// derives a new value from that.
///
/// A good example for that is a classic WASD keyboard binding:
///
/// <example>
/// <code>
/// var moveAction = new InputAction(name: "move");
/// moveAction.AddCompositeBinding("Vector2")
/// .With("Up", "&lt;Keyboard&gt;/w")
/// .With("Down", "&lt;Keyboard&gt;/s")
/// .With("Left", "&lt;Keyboard&gt;/a")
/// .With("Right", "&lt;Keyboard&gt;/d")
/// </code>
/// </example>
///
/// Here, each direction is represented by a separate binding. "Up" is bound to "W", "Down"
/// is bound to "S", and so on. Each direction individually returns a 0 or 1 depending
/// on whether it is pressed or not.
///
/// However, as a composite, the binding to the "move" action returns a combined <c>Vector2</c>
/// that is computed from the state of each of the directional controls. This is what composites
/// do. They take inputs from their "parts" to derive an input for the binding as a whole.
///
/// Note that the properties and methods defined in <see cref="InputBindingComposite"/> and this
/// class will generally be called internally by the input system and are not generally meant
/// to be called directly from user land.
///
/// The set of composites available in the system is extensible. While some composites are
/// such as <see cref="Composites.Vector2Composite"/> and <see cref="Composites.ButtonWithOneModifier"/>
/// are available out of the box, new composites can be implemented by anyone and simply be autodiscover
/// or manually registered with <see cref="InputSystem.RegisterBindingComposite{T}"/>.
///
/// See the "Custom Composite" sample (can be installed from package manager UI) for a detailed example
/// of how to create a custom composite.
/// </remarks>
/// <seealso cref="InputSystem.RegisterBindingComposite{T}"/>
public abstract class InputBindingComposite<TValue> : InputBindingComposite
where TValue : struct
{
/// <summary>
/// The type of value returned by the composite, i.e. <c>typeof(TValue)</c>.
/// </summary>
/// <value>Returns <c>typeof(TValue)</c>.</value>
public override Type valueType => typeof(TValue);
/// <summary>
/// The size of values returned by the composite, i.e. <c>sizeof(TValue)</c>.
/// </summary>
/// <value>Returns <c>sizeof(TValue)</c>.</value>
public override int valueSizeInBytes => UnsafeUtility.SizeOf<TValue>();
/// <summary>
/// Read a value for the composite given the supplied context.
/// </summary>
/// <param name="context">Callback context for the binding composite. Use this
/// to access the values supplied by part bindings.</param>
/// <returns>The current value of the composite according to the state made
/// accessible through <paramref name="context"/>.</returns>
/// <remarks>
/// This is the main method to implement in custom composites.
///
/// <example>
/// <code>
/// public class CustomComposite : InputBindingComposite&lt;float&gt;
/// {
/// [InputControl(layout = "Button")]
/// public int button;
///
/// public float scaleFactor = 1;
///
/// public override float ReadValue(ref InputBindingComposite context)
/// {
/// return context.ReadValue&lt;float&gt;(button) * scaleFactor;
/// }
/// }
/// </code>
/// </example>
///
/// The other method to consider overriding is <see cref="InputBindingComposite.EvaluateMagnitude"/>.
/// </remarks>
/// <seealso cref="InputAction.ReadValue{TValue}"/>
/// <seealso cref="InputAction.CallbackContext.ReadValue{TValue}"/>
public abstract TValue ReadValue(ref InputBindingCompositeContext context);
/// <inheritdoc />
public override unsafe void ReadValue(ref InputBindingCompositeContext context, void* buffer, int bufferSize)
{
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
var valueSize = UnsafeUtility.SizeOf<TValue>();
if (bufferSize < valueSize)
throw new ArgumentException(
$"Expected buffer of at least {UnsafeUtility.SizeOf<TValue>()} bytes but got buffer of only {bufferSize} bytes instead",
nameof(bufferSize));
var value = ReadValue(ref context);
var valuePtr = UnsafeUtility.AddressOf(ref value);
UnsafeUtility.MemCpy(buffer, valuePtr, valueSize);
}
/// <inheritdoc />
public override unsafe object ReadValueAsObject(ref InputBindingCompositeContext context)
{
var value = default(TValue);
var valuePtr = UnsafeUtility.AddressOf(ref value);
ReadValue(ref context, valuePtr, UnsafeUtility.SizeOf<TValue>());
return value;
}
}
}

View File

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

View File

@@ -0,0 +1,355 @@
using System;
using System.Collections.Generic;
namespace UnityEngine.InputSystem
{
/// <summary>
/// Contextual data made available when processing values of composite bindings.
/// </summary>
/// <remarks>
/// An instance of this struct is passed to <see
/// cref="InputBindingComposite{TValue}.ReadValue(ref InputBindingCompositeContext)"/>.
/// Use it to access contextual data such as the value for individual part bindings.
///
/// Note that an instance of this struct should never be held on to past the duration
/// of the call to <c>ReadValue</c>. The data it retrieves is only valid during
/// the callback.
/// </remarks>
/// <seealso cref="InputBindingComposite"/>
/// <seealso cref="InputBindingComposite{TValue}"/>
/// <seealso cref="InputBindingComposite{TValue}.ReadValue(ref InputBindingCompositeContext)"/>
public struct InputBindingCompositeContext
{
/// <summary>
/// Information about a control bound to a part of a composite.
/// </summary>
public struct PartBinding
{
/// <summary>
/// Identifier of the part. This is the numeric identifier stored in the public
/// fields of the composite by the input system.
/// </summary>
public int part { get; set; }
/// <summary>
/// The control bound to the part.
/// </summary>
public InputControl control { get; set; }
}
/// <summary>
/// Enumerate all the controls that are part of the composite.
/// </summary>
/// <seealso cref="InputBindingComposite.FinishSetup"/>
public IEnumerable<PartBinding> controls
{
get
{
if (m_State == null)
yield break;
var totalBindingCount = m_State.totalBindingCount;
for (var bindingIndex = m_BindingIndex + 1; bindingIndex < totalBindingCount; ++bindingIndex)
{
var bindingState = m_State.GetBindingState(bindingIndex);
if (!bindingState.isPartOfComposite)
break;
var controlStartIndex = bindingState.controlStartIndex;
for (var i = 0; i < bindingState.controlCount; ++i)
{
var control = m_State.controls[controlStartIndex + i];
yield return new PartBinding
{
part = bindingState.partIndex,
control = control
};
}
}
}
}
public float EvaluateMagnitude(int partNumber)
{
return m_State.EvaluateCompositePartMagnitude(m_BindingIndex, partNumber);
}
/// <summary>
/// Read the value of the giving part binding.
/// </summary>
/// <param name="partNumber">Number of the part to read. This is assigned
/// automatically by the input system and should be treated as an opaque
/// identifier. See the example below.</param>
/// <typeparam name="TValue">Type of value to read. This must match the
/// value type expected from controls bound to the part.</typeparam>
/// <returns>The value read from the part bindings.</returns>
/// <exception cref="InvalidOperationException">The given <typeparamref name="TValue"/>
/// value type does not match the actual value type of the control(s) bound
/// to the part.</exception>
/// <remarks>
/// If no control is bound to the given part, the return value will always
/// be <c>default(TValue)</c>. If a single control is bound to the part, the
/// value will be that of the control. If multiple controls are bound to a
/// part, the return value will be that greatest one according to <c>IComparable</c>
/// implemented by <typeparamref name="TValue"/>.
///
/// Note that this method only works with values that are <c>IComparable</c>.
/// To read a value type that is not <c>IComparable</c> or to supply a custom
/// comparer, use <see cref="ReadValue{TValue,TComparer}(int,TComparer)"/>.
///
/// If an invalid <paramref name="partNumber"/> is supplied, the return value
/// will simply be <c>default(TValue)</c>. No exception is thrown.
///
/// <example>
/// <code>
/// public class MyComposite : InputBindingComposite&lt;float&gt;
/// {
/// // Defines a "part" binding for the composite. Each part can be
/// // bound to arbitrary many times (including not at all). The "layout"
/// // property of the attribute we supply determines what kind of
/// // control is expected to be bound to the part.
/// //
/// // When initializing a composite instance, the input system will
/// // automatically assign part numbers and store them in the fields
/// // we define here.
/// [InputControl(layout = "Button")]
/// public int firstPart;
///
/// // Defines a second part.
/// [InputControl(layout = "Vector2")]
/// public int secondPart;
///
/// public override float ReadValue(ref InputBindingCompositeContext context)
/// {
/// // Read the button.
/// var firstValue = context.ReadValue&lt;float&gt;();
///
/// // Read the vector.
/// var secondValue = context.ReadValue&lt;Vector2&gt;();
///
/// // Perform some computation based on the inputs. Here, we just
/// // scale the vector by the value we got from the button.
/// return secondValue * firstValue;
/// }
/// }
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="ReadValue{TValue,TComparer}(int,TComparer)"/>
/// <seealso cref="InputControl{TValue}.ReadValue"/>
public unsafe TValue ReadValue<TValue>(int partNumber)
where TValue : struct, IComparable<TValue>
{
if (m_State == null)
return default;
return m_State.ReadCompositePartValue<TValue, DefaultComparer<TValue>>
(m_BindingIndex, partNumber, null, out _);
}
/// <summary>
/// Same as <see cref="ReadValue{TValue}(int)"/> but also return the control
/// from which the value was read.
/// </summary>
/// <param name="partNumber">Number of the part to read. This is assigned
/// automatically by the input system and should be treated as an opaque
/// identifier.</param>
/// <param name="sourceControl">Receives the <see cref="InputControl"/> from
/// which the value was read. If multiple controls are bound to the given part,
/// this is the control whose value was ultimately selected. Will be set to
/// <c>null</c> if <paramref name="partNumber"/> is not a valid part or if no
/// controls are bound to the part.</param>
/// <typeparam name="TValue">Type of value to read. This must match the
/// value type expected from controls bound to the part.</typeparam>
/// <returns>The value read from the part bindings.</returns>
/// <remarks>
/// Like <see cref="ReadValue{TValue}(int)"/>, this method relies on using <c>IComparable</c>
/// implemented by <typeparamref name="TValue"/> to determine the greatest value
/// if multiple controls are bound to the specified part.
/// </remarks>
/// <seealso cref="ReadValue{TValue}(int)"/>
public unsafe TValue ReadValue<TValue>(int partNumber, out InputControl sourceControl)
where TValue : struct, IComparable<TValue>
{
if (m_State == null)
{
sourceControl = null;
return default;
}
var value = m_State.ReadCompositePartValue<TValue, DefaultComparer<TValue>>(m_BindingIndex, partNumber,
null, out var controlIndex);
if (controlIndex != InputActionState.kInvalidIndex)
sourceControl = m_State.controls[controlIndex];
else
sourceControl = null;
return value;
}
////TODO: once we can break the API, remove the versions that rely on comparers and do everything through magnitude
/// <summary>
/// Read the value of the given part bindings and use the given <paramref name="comparer"/>
/// to determine which value to return if multiple controls are bound to the part.
/// </summary>
/// <param name="partNumber">Number of the part to read. This is assigned
/// automatically by the input system and should be treated as an opaque
/// identifier.</param>
/// <param name="comparer">Instance of <typeparamref name="TComparer"/> for comparing
/// multiple values.</param>
/// <typeparam name="TValue">Type of value to read. This must match the
/// value type expected from controls bound to the part.</typeparam>
/// <returns>The value read from the part bindings.</returns>
/// <typeparam name="TComparer">Comparer to use if multiple controls are bound to
/// the given part. All values will be compared using <c>TComparer.Compare</c> and
/// the greatest value will be returned.</typeparam>
/// <returns>The value read from the part bindings.</returns>
/// <remarks>
/// This method is a useful alternative to <see cref="ReadValue{TValue}(int)"/> for
/// value types that do not implement <c>IComparable</c> or when the default comparison
/// behavior is undesirable.
///
/// <example>
/// <code>
/// public class CompositeWithVector2Part : InputBindingComposite&lt;Vector2&gt;
/// {
/// [InputControl(layout = "Vector2")]
/// public int part;
///
/// public override Vector2 ReadValue(ref InputBindingCompositeContext context)
/// {
/// // Return the Vector3 with the greatest magnitude.
/// return context.ReadValue&lt;Vector2, Vector2MagnitudeComparer&gt;(part);
/// }
/// }
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="Utilities.Vector2MagnitudeComparer"/>
/// <seealso cref="Utilities.Vector3MagnitudeComparer"/>
public unsafe TValue ReadValue<TValue, TComparer>(int partNumber, TComparer comparer = default)
where TValue : struct
where TComparer : IComparer<TValue>
{
if (m_State == null)
return default;
return m_State.ReadCompositePartValue<TValue, TComparer>(
m_BindingIndex, partNumber, null, out _, comparer);
}
/// <summary>
/// Like <see cref="ReadValue{TValue,TComparer}(int,TComparer)"/> but also return
/// the control from which the value has ultimately been read.
/// </summary>
/// <param name="partNumber">Number of the part to read. This is assigned
/// automatically by the input system and should be treated as an opaque
/// identifier.</param>
/// <param name="sourceControl">Receives the <see cref="InputControl"/> from
/// which the value was read. If multiple controls are bound to the given part,
/// this is the control whose value was ultimately selected. Will be set to
/// <c>null</c> if <paramref name="partNumber"/> is not a valid part or if no
/// controls are bound to the part.</param>
/// <param name="comparer">Instance of <typeparamref name="TComparer"/> for comparing
/// multiple values.</param>
/// <typeparam name="TValue">Type of value to read. This must match the
/// value type expected from controls bound to the part.</typeparam>
/// <returns>The value read from the part bindings.</returns>
/// <typeparam name="TComparer">Comparer to use if multiple controls are bound to
/// the given part. All values will be compared using <c>TComparer.Compare</c> and
/// the greatest value will be returned.</typeparam>
/// <returns>The value read from the part bindings.</returns>
public unsafe TValue ReadValue<TValue, TComparer>(int partNumber, out InputControl sourceControl, TComparer comparer = default)
where TValue : struct
where TComparer : IComparer<TValue>
{
if (m_State == null)
{
sourceControl = null;
return default;
}
var value = m_State.ReadCompositePartValue<TValue, TComparer>(m_BindingIndex, partNumber, null,
out var controlIndex, comparer);
if (controlIndex != InputActionState.kInvalidIndex)
sourceControl = m_State.controls[controlIndex];
else
sourceControl = null;
return value;
}
/// <summary>
/// Like <see cref="ReadValue{TValue}(int)"/> but treat bound controls as buttons. This means
/// that custom <see cref="Controls.ButtonControl.pressPoint"/> are respected and that floating-point
/// values from non-ButtonControls will be compared to <see cref="InputSettings.defaultButtonPressPoint"/>.
/// </summary>
/// <param name="partNumber">Number of the part to read. This is assigned
/// automatically by the input system and should be treated as an opaque
/// identifier.</param>
/// <returns>True if any button bound to the part is pressed.</returns>
/// <remarks>
/// This method expects all controls bound to the part to be of type <c>InputControl&lt;float&gt;</c>.
///
/// This method is different from just calling <see cref="ReadValue{TValue}(int)"/> with a <c>float</c>
/// parameter and comparing the result to <see cref="InputSettings.defaultButtonPressPoint"/> in that
/// custom press points set on individual ButtonControls will be respected.
/// </remarks>
/// <seealso cref="Controls.ButtonControl"/>
/// <seealso cref="InputSettings.defaultButtonPressPoint"/>
public unsafe bool ReadValueAsButton(int partNumber)
{
if (m_State == null)
return default;
////REVIEW: wouldn't this have to take release points into account now?
var buttonValue = false;
m_State.ReadCompositePartValue<float, DefaultComparer<float>>(m_BindingIndex, partNumber, &buttonValue,
out _);
return buttonValue;
}
public unsafe void ReadValue(int partNumber, void* buffer, int bufferSize)
{
m_State?.ReadCompositePartValue(m_BindingIndex, partNumber, buffer, bufferSize);
}
public object ReadValueAsObject(int partNumber)
{
return m_State.ReadCompositePartValueAsObject(m_BindingIndex, partNumber);
}
/// <summary>
/// Return the timestamp (see <see cref="LowLevel.InputEvent.time"/>) for when the given
/// binding part crossed the button press threshold (see <see cref="Controls.ButtonControl.pressPoint"/>).
/// </summary>
/// <param name="partNumber">Number of the part to read. This is assigned
/// automatically by the input system and should be treated as an opaque
/// identifier.</param>
/// <returns>Returns the time at which the given part binding moved into "press" state or 0 if there's
/// current no press.</returns>
/// <remarks>
/// If the given part has more than a single binding and/or more than a single bound control, the <em>earliest</em>
/// press time is returned.
/// </remarks>
public double GetPressTime(int partNumber)
{
return m_State.GetCompositePartPressTime(m_BindingIndex, partNumber);
}
internal InputActionState m_State;
internal int m_BindingIndex;
private struct DefaultComparer<TValue> : IComparer<TValue>
where TValue : IComparable<TValue>
{
public int Compare(TValue x, TValue y)
{
return x.CompareTo(y);
}
}
}
}

View File

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

View File

@@ -0,0 +1,742 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Unity.Collections;
using UnityEngine.InputSystem.Utilities;
////TODO: reuse interaction, processor, and composite instances from prior resolves
namespace UnityEngine.InputSystem
{
/// <summary>
/// Heart of the binding resolution machinery. Consumes lists of bindings
/// and spits out out a list of resolved bindings together with their needed
/// execution state.
/// </summary>
/// <remarks>
/// One or more <see cref="InputActionMap">action maps</see> can be added to the same
/// resolver. The result is a combination of the binding state of all maps.
///
/// The data set up by a resolver is for consumption by <see cref="InputActionState"/>.
/// Essentially, InputBindingResolver does all the wiring and <see cref="InputActionState"/>
/// does all the actual execution based on the resulting data.
/// </remarks>
/// <seealso cref="InputActionState.Initialize"/>
internal struct InputBindingResolver : IDisposable
{
public int totalProcessorCount;
public int totalCompositeCount;
public int totalInteractionCount;
public int totalMapCount => memory.mapCount;
public int totalActionCount => memory.actionCount;
public int totalBindingCount => memory.bindingCount;
public int totalControlCount => memory.controlCount;
public InputActionMap[] maps;
public InputControl[] controls;
public InputActionState.UnmanagedMemory memory;
public IInputInteraction[] interactions;
public InputProcessor[] processors;
public InputBindingComposite[] composites;
/// <summary>
/// Binding mask used to globally mask out bindings.
/// </summary>
/// <remarks>
/// This is empty by default.
///
/// The bindings of each map will be <see cref="InputBinding.Matches">matched</see> against this
/// binding. Any bindings that don't match will get skipped and not resolved to controls.
///
/// Note that regardless of whether a binding will be resolved to controls or not, it will get
/// an entry in <see cref="memory"/>. Otherwise we would have to have a more complicated
/// mapping from <see cref="InputActionMap.bindings"/> to a binding state in <see cref="memory"/>.
/// </remarks>
public InputBinding? bindingMask;
private bool m_IsControlOnlyResolve;
/// <summary>
/// Release native memory held by the resolver.
/// </summary>
public void Dispose()
{
memory.Dispose();
}
/// <summary>
/// Steal the already allocated arrays from the given state.
/// </summary>
/// <param name="state">Action map state that was previously created.</param>
/// <param name="isFullResolve">If false, the only thing that is allowed to change in the re-resolution
/// is the list of controls. In other words, devices may have been added or removed but otherwise the configuration
/// is exactly the same as in the last resolve. If true, anything may have changed and the resolver will only reuse
/// allocations but not contents.</param>
public void StartWithPreviousResolve(InputActionState state, bool isFullResolve)
{
Debug.Assert(state != null, "Received null state");
Debug.Assert(!state.isProcessingControlStateChange,
"Cannot re-resolve bindings for an InputActionState that is currently executing an action callback; binding resolution must be deferred to until after the callback has completed");
m_IsControlOnlyResolve = !isFullResolve;
maps = state.maps;
interactions = state.interactions;
processors = state.processors;
composites = state.composites;
controls = state.controls;
// Clear the arrays so that we don't leave references around.
if (isFullResolve)
{
if (maps != null)
Array.Clear(maps, 0, state.totalMapCount);
if (interactions != null)
Array.Clear(interactions, 0, state.totalInteractionCount);
if (processors != null)
Array.Clear(processors, 0, state.totalProcessorCount);
if (composites != null)
Array.Clear(composites, 0, state.totalCompositeCount);
}
if (controls != null) // Always clear this one as every resolve will change it.
Array.Clear(controls, 0, state.totalControlCount);
// Null out the arrays on the state so that there is no strange bugs with
// the state reading from arrays that no longer belong to it.
state.maps = null;
state.interactions = null;
state.processors = null;
state.composites = null;
state.controls = null;
}
/// <summary>
/// Resolve and add all bindings and actions from the given map.
/// </summary>
/// <param name="actionMap"></param>
/// <remarks>
/// This is where all binding resolution happens for actions. The method walks through the binding array
/// in <paramref name="actionMap"/> and adds any controls, interactions, processors, and composites as it goes.
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1809:AvoidExcessiveLocals", Justification = "TODO: Refactor later.")]
public unsafe void AddActionMap(InputActionMap actionMap)
{
Debug.Assert(actionMap != null, "Received null map");
InputSystem.EnsureInitialized();
var actionsInThisMap = actionMap.m_Actions;
var bindingsInThisMap = actionMap.m_Bindings;
var bindingCountInThisMap = bindingsInThisMap?.Length ?? 0;
var actionCountInThisMap = actionsInThisMap?.Length ?? 0;
var mapIndex = totalMapCount;
// Keep track of indices for this map.
var actionStartIndex = totalActionCount;
var bindingStartIndex = totalBindingCount;
var controlStartIndex = totalControlCount;
var interactionStartIndex = totalInteractionCount;
var processorStartIndex = totalProcessorCount;
var compositeStartIndex = totalCompositeCount;
// Allocate an initial block of memory. We probably will have to re-allocate once
// at the end to accommodate interactions and controls added from the map.
var newMemory = new InputActionState.UnmanagedMemory();
newMemory.Allocate(
mapCount: totalMapCount + 1,
actionCount: totalActionCount + actionCountInThisMap,
bindingCount: totalBindingCount + bindingCountInThisMap,
// We reallocate for the following once we know the final count.
interactionCount: totalInteractionCount,
compositeCount: totalCompositeCount,
controlCount: totalControlCount);
if (memory.isAllocated)
newMemory.CopyDataFrom(memory);
////TODO: make sure composite objects get all the bindings they need
////TODO: handle case where we have bindings resolving to the same control
//// (not so clear cut what to do there; each binding may have a different interaction setup, for example)
var currentCompositeBindingIndex = InputActionState.kInvalidIndex;
var currentCompositeIndex = InputActionState.kInvalidIndex;
var currentCompositePartCount = 0;
var currentCompositeActionIndexInMap = InputActionState.kInvalidIndex;
InputAction currentCompositeAction = null;
var bindingMaskOnThisMap = actionMap.m_BindingMask;
var devicesForThisMap = actionMap.devices;
var isSingletonAction = actionMap.m_SingletonAction != null;
// Can't use `using` as we need to use it with `ref`.
var resolvedControls = new InputControlList<InputControl>(Allocator.Temp);
// We gather all controls in temporary memory and then move them over into newMemory once
// we're done resolving.
try
{
for (var n = 0; n < bindingCountInThisMap; ++n)
{
var bindingStatesPtr = newMemory.bindingStates;
ref var unresolvedBinding = ref bindingsInThisMap[n];
var bindingIndex = bindingStartIndex + n;
var isComposite = unresolvedBinding.isComposite;
var isPartOfComposite = !isComposite && unresolvedBinding.isPartOfComposite;
var bindingState = &bindingStatesPtr[bindingIndex];
try
{
////TODO: if it's a composite, check if any of the children matches our binding masks (if any) and skip composite if none do
var firstControlIndex = 0; // numControls dictates whether this is a valid index or not.
var firstInteractionIndex = InputActionState.kInvalidIndex;
var firstProcessorIndex = InputActionState.kInvalidIndex;
var actionIndexForBinding = InputActionState.kInvalidIndex;
var partIndex = InputActionState.kInvalidIndex;
var numControls = 0;
var numInteractions = 0;
var numProcessors = 0;
// Make sure that if it's part of a composite, we are actually part of a composite.
if (isPartOfComposite && currentCompositeBindingIndex == InputActionState.kInvalidIndex)
throw new InvalidOperationException(
$"Binding '{unresolvedBinding}' is marked as being part of a composite but the preceding binding is not a composite");
// Try to find action.
//
// NOTE: We ignore actions on bindings that are part of composites. We only allow
// actions to be triggered from the composite itself.
var actionIndexInMap = InputActionState.kInvalidIndex;
var actionName = unresolvedBinding.action;
InputAction action = null;
if (!isPartOfComposite)
{
if (isSingletonAction)
{
// Singleton actions always ignore names.
actionIndexInMap = 0;
}
else if (!string.IsNullOrEmpty(actionName))
{
////REVIEW: should we fail here if we don't manage to find the action
actionIndexInMap = actionMap.FindActionIndex(actionName);
}
if (actionIndexInMap != InputActionState.kInvalidIndex)
action = actionsInThisMap[actionIndexInMap];
}
else
{
actionIndexInMap = currentCompositeActionIndexInMap;
action = currentCompositeAction;
}
// If it's a composite, start a chain.
if (isComposite)
{
currentCompositeBindingIndex = bindingIndex;
currentCompositeAction = action;
currentCompositeActionIndexInMap = actionIndexInMap;
}
// Determine if the binding is disabled.
// Disabled if path is empty.
var path = unresolvedBinding.effectivePath;
var bindingIsDisabled = string.IsNullOrEmpty(path)
// Also, if we can't find the action to trigger for the binding, we just go and disable
// the binding.
|| action == null
// Also, disabled if binding doesn't match with our binding mask (might be empty).
|| (!isComposite && bindingMask != null &&
!bindingMask.Value.Matches(ref unresolvedBinding,
InputBinding.MatchOptions.EmptyGroupMatchesAny))
// Also, disabled if binding doesn't match the binding mask on the map (might be empty).
|| (!isComposite && bindingMaskOnThisMap != null &&
!bindingMaskOnThisMap.Value.Matches(ref unresolvedBinding,
InputBinding.MatchOptions.EmptyGroupMatchesAny))
// Finally, also disabled if binding doesn't match the binding mask on the action (might be empty).
|| (!isComposite && action?.m_BindingMask != null &&
!action.m_BindingMask.Value.Matches(ref unresolvedBinding,
InputBinding.MatchOptions.EmptyGroupMatchesAny));
// If the binding isn't disabled, look up controls now. We do this first as we may still disable the
// binding if it doesn't resolve to any controls or resolves only to controls already bound to by
// other bindings.
//
// NOTE: We continuously add controls here to `resolvedControls`. Once we've completed our
// pass over the bindings in the map, `resolvedControls` will have all the controls for
// the current map.
if (!bindingIsDisabled && !isComposite)
{
firstControlIndex = memory.controlCount + resolvedControls.Count;
if (devicesForThisMap != null)
{
// Search in devices for only this map.
var list = devicesForThisMap.Value;
for (var i = 0; i < list.Count; ++i)
{
var device = list[i];
if (!device.added)
continue; // Skip devices that have been removed.
numControls += InputControlPath.TryFindControls(device, path, 0, ref resolvedControls);
}
}
else
{
// Search globally.
numControls = InputSystem.FindControls(path, ref resolvedControls);
}
}
// If the binding isn't disabled, resolve its controls, processors, and interactions.
if (!bindingIsDisabled)
{
// NOTE: When isFullResolve==false, it is *imperative* that we do count processor and interaction
// counts here come out exactly the same as in the previous full resolve.
// Instantiate processors.
var processorString = unresolvedBinding.effectiveProcessors;
if (!string.IsNullOrEmpty(processorString))
{
// Add processors from binding.
firstProcessorIndex = InstantiateWithParameters(InputProcessor.s_Processors, processorString,
ref processors, ref totalProcessorCount, actionMap, ref unresolvedBinding);
if (firstProcessorIndex != InputActionState.kInvalidIndex)
numProcessors = totalProcessorCount - firstProcessorIndex;
}
if (!string.IsNullOrEmpty(action.m_Processors))
{
// Add processors from action.
var index = InstantiateWithParameters(InputProcessor.s_Processors, action.m_Processors, ref processors,
ref totalProcessorCount, actionMap, ref unresolvedBinding);
if (index != InputActionState.kInvalidIndex)
{
if (firstProcessorIndex == InputActionState.kInvalidIndex)
firstProcessorIndex = index;
numProcessors += totalProcessorCount - index;
}
}
// Instantiate interactions.
if (isPartOfComposite)
{
// Composite's part use composite interactions
if (currentCompositeBindingIndex != InputActionState.kInvalidIndex)
{
firstInteractionIndex = bindingStatesPtr[currentCompositeBindingIndex].interactionStartIndex;
numInteractions = bindingStatesPtr[currentCompositeBindingIndex].interactionCount;
}
}
else
{
var interactionString = unresolvedBinding.effectiveInteractions;
if (!string.IsNullOrEmpty(interactionString))
{
// Add interactions from binding.
firstInteractionIndex = InstantiateWithParameters(InputInteraction.s_Interactions, interactionString,
ref interactions, ref totalInteractionCount, actionMap, ref unresolvedBinding);
if (firstInteractionIndex != InputActionState.kInvalidIndex)
numInteractions = totalInteractionCount - firstInteractionIndex;
}
if (!string.IsNullOrEmpty(action.m_Interactions))
{
// Add interactions from action.
var index = InstantiateWithParameters(InputInteraction.s_Interactions, action.m_Interactions,
ref interactions, ref totalInteractionCount, actionMap, ref unresolvedBinding);
if (index != InputActionState.kInvalidIndex)
{
if (firstInteractionIndex == InputActionState.kInvalidIndex)
firstInteractionIndex = index;
numInteractions += totalInteractionCount - index;
}
}
}
// If it's the start of a composite chain, create the composite.
if (isComposite)
{
// The composite binding entry itself does not resolve to any controls.
// It creates a composite binding object which is then populated from
// subsequent bindings.
// Instantiate. For composites, the path is the name of the composite.
var composite = InstantiateBindingComposite(ref unresolvedBinding, actionMap);
currentCompositeIndex =
ArrayHelpers.AppendWithCapacity(ref composites, ref totalCompositeCount, composite);
// Record where the controls for parts of the composite start.
firstControlIndex = memory.controlCount + resolvedControls.Count;
}
else
{
// If we've reached the end of a composite chain, finish
// off the current composite.
if (!isPartOfComposite && currentCompositeBindingIndex != InputActionState.kInvalidIndex)
{
currentCompositePartCount = 0;
currentCompositeBindingIndex = InputActionState.kInvalidIndex;
currentCompositeIndex = InputActionState.kInvalidIndex;
currentCompositeAction = null;
currentCompositeActionIndexInMap = InputActionState.kInvalidIndex;
}
}
}
// If the binding is part of a composite, pass the resolved controls
// on to the composite.
if (isPartOfComposite && currentCompositeBindingIndex != InputActionState.kInvalidIndex && numControls > 0)
{
// Make sure the binding is named. The name determines what in the composite
// to bind to.
if (string.IsNullOrEmpty(unresolvedBinding.name))
throw new InvalidOperationException(
$"Binding '{unresolvedBinding}' that is part of composite '{composites[currentCompositeIndex]}' is missing a name");
// Assign an index to the current part of the composite which
// can be used by the composite to read input from this part.
partIndex = AssignCompositePartIndex(composites[currentCompositeIndex], unresolvedBinding.name,
ref currentCompositePartCount);
// Keep track of total number of controls bound in the composite.
bindingStatesPtr[currentCompositeBindingIndex].controlCount += numControls;
// Force action index on part binding to be same as that of composite.
actionIndexForBinding = bindingStatesPtr[currentCompositeBindingIndex].actionIndex;
}
else if (actionIndexInMap != InputActionState.kInvalidIndex)
{
actionIndexForBinding = actionStartIndex + actionIndexInMap;
}
// Store resolved binding.
*bindingState = new InputActionState.BindingState
{
controlStartIndex = firstControlIndex,
// For composites, this will be adjusted as we add each part.
controlCount = numControls,
interactionStartIndex = firstInteractionIndex,
interactionCount = numInteractions,
processorStartIndex = firstProcessorIndex,
processorCount = numProcessors,
isComposite = isComposite,
isPartOfComposite = unresolvedBinding.isPartOfComposite,
partIndex = partIndex,
actionIndex = actionIndexForBinding,
compositeOrCompositeBindingIndex = isComposite ? currentCompositeIndex : currentCompositeBindingIndex,
mapIndex = totalMapCount,
wantsInitialStateCheck = action?.wantsInitialStateCheck ?? false
};
}
catch (Exception exception)
{
Debug.LogError(
$"{exception.GetType().Name} while resolving binding '{unresolvedBinding}' in action map '{actionMap}'");
Debug.LogException(exception);
// Don't swallow exceptions that indicate something is wrong in the code rather than
// in the data.
if (exception.IsExceptionIndicatingBugInCode())
throw;
}
}
// Re-allocate memory to accommodate controls and interaction states. The count for those
// we only know once we've completed all resolution.
var controlCountInThisMap = resolvedControls.Count;
var newTotalControlCount = memory.controlCount + controlCountInThisMap;
if (newMemory.interactionCount != totalInteractionCount ||
newMemory.compositeCount != totalCompositeCount ||
newMemory.controlCount != newTotalControlCount)
{
var finalMemory = new InputActionState.UnmanagedMemory();
finalMemory.Allocate(
mapCount: newMemory.mapCount,
actionCount: newMemory.actionCount,
bindingCount: newMemory.bindingCount,
controlCount: newTotalControlCount,
interactionCount: totalInteractionCount,
compositeCount: totalCompositeCount);
finalMemory.CopyDataFrom(newMemory);
newMemory.Dispose();
newMemory = finalMemory;
}
// Add controls to array.
var controlCountInArray = memory.controlCount;
ArrayHelpers.AppendListWithCapacity(ref controls, ref controlCountInArray, resolvedControls);
Debug.Assert(controlCountInArray == newTotalControlCount,
"Control array should have combined count of old and new controls");
// Set up control to binding index mapping.
for (var i = 0; i < bindingCountInThisMap; ++i)
{
var bindingStatesPtr = newMemory.bindingStates;
var bindingState = &bindingStatesPtr[bindingStartIndex + i];
var numControls = bindingState->controlCount;
var startIndex = bindingState->controlStartIndex;
for (var n = 0; n < numControls; ++n)
newMemory.controlIndexToBindingIndex[startIndex + n] = bindingStartIndex + i;
}
// Initialize initial interaction states.
for (var i = memory.interactionCount; i < newMemory.interactionCount; ++i)
{
ref var interactionState = ref newMemory.interactionStates[i];
interactionState.phase = InputActionPhase.Waiting;
interactionState.triggerControlIndex = InputActionState.kInvalidIndex;
}
// Initialize action data.
var runningIndexInBindingIndices = memory.bindingCount;
for (var i = 0; i < actionCountInThisMap; ++i)
{
var action = actionsInThisMap[i];
var actionIndex = actionStartIndex + i;
// Correlate action with its trigger state.
action.m_ActionIndexInState = actionIndex;
Debug.Assert(runningIndexInBindingIndices < ushort.MaxValue, "Binding start index on action exceeds limit");
newMemory.actionBindingIndicesAndCounts[actionIndex * 2] = (ushort)runningIndexInBindingIndices;
// Collect bindings for action.
var firstBindingIndexForAction = -1;
var bindingCountForAction = 0;
var numPossibleConcurrentActuations = 0;
for (var n = 0; n < bindingCountInThisMap; ++n)
{
var bindingIndex = bindingStartIndex + n;
var bindingState = &newMemory.bindingStates[bindingIndex];
if (bindingState->actionIndex != actionIndex)
continue;
if (bindingState->isPartOfComposite)
continue;
Debug.Assert(bindingIndex <= ushort.MaxValue, "Binding index exceeds limit");
newMemory.actionBindingIndices[runningIndexInBindingIndices] = (ushort)bindingIndex;
++runningIndexInBindingIndices;
++bindingCountForAction;
if (firstBindingIndexForAction == -1)
firstBindingIndexForAction = bindingIndex;
// Keep track of how many concurrent actuations we may be seeing on the action so that
// we know whether we need to enable conflict resolution or not.
if (bindingState->isComposite)
{
// Composite binding. Actuates as a whole. Check if the composite has successfully
// resolved any controls. If so, it adds one possible actuation.
if (bindingState->controlCount > 0)
++numPossibleConcurrentActuations;
}
else
{
// Normal binding. Every successfully resolved control results in one possible actuation.
numPossibleConcurrentActuations += bindingState->controlCount;
}
}
if (firstBindingIndexForAction == -1)
firstBindingIndexForAction = 0;
Debug.Assert(bindingCountForAction < ushort.MaxValue, "Binding count on action exceeds limit");
newMemory.actionBindingIndicesAndCounts[actionIndex * 2 + 1] = (ushort)bindingCountForAction;
// See if we may need conflict resolution on this action. Never needed for pass-through actions.
// Otherwise, if we have more than one bound control or have several bindings and one of them
// is a composite, we enable it.
var isPassThroughAction = action.type == InputActionType.PassThrough;
var isButtonAction = action.type == InputActionType.Button;
var mayNeedConflictResolution = !isPassThroughAction && numPossibleConcurrentActuations > 1;
// Initialize initial trigger state.
newMemory.actionStates[actionIndex] =
new InputActionState.TriggerState
{
phase = InputActionPhase.Disabled,
mapIndex = mapIndex,
controlIndex = InputActionState.kInvalidIndex,
interactionIndex = InputActionState.kInvalidIndex,
isPassThrough = isPassThroughAction,
isButton = isButtonAction,
mayNeedConflictResolution = mayNeedConflictResolution,
bindingIndex = firstBindingIndexForAction
};
}
// Store indices for map.
newMemory.mapIndices[mapIndex] =
new InputActionState.ActionMapIndices
{
actionStartIndex = actionStartIndex,
actionCount = actionCountInThisMap,
controlStartIndex = controlStartIndex,
controlCount = controlCountInThisMap,
bindingStartIndex = bindingStartIndex,
bindingCount = bindingCountInThisMap,
interactionStartIndex = interactionStartIndex,
interactionCount = totalInteractionCount - interactionStartIndex,
processorStartIndex = processorStartIndex,
processorCount = totalProcessorCount - processorStartIndex,
compositeStartIndex = compositeStartIndex,
compositeCount = totalCompositeCount - compositeStartIndex,
};
actionMap.m_MapIndexInState = mapIndex;
var finalActionMapCount = memory.mapCount;
ArrayHelpers.AppendWithCapacity(ref maps, ref finalActionMapCount, actionMap, capacityIncrement: 4);
Debug.Assert(finalActionMapCount == newMemory.mapCount,
"Final action map count should match old action map count plus one");
// As a final act, swap the new memory in.
memory.Dispose();
memory = newMemory;
}
catch (Exception)
{
// Don't leak our native memory when we throw an exception.
newMemory.Dispose();
throw;
}
finally
{
resolvedControls.Dispose();
}
}
private List<NameAndParameters> m_Parameters; // We retain this to reuse the allocation.
private int InstantiateWithParameters<TType>(TypeTable registrations, string namesAndParameters, ref TType[] array, ref int count, InputActionMap actionMap, ref InputBinding binding)
{
if (!NameAndParameters.ParseMultiple(namesAndParameters, ref m_Parameters))
return InputActionState.kInvalidIndex;
var firstIndex = count;
for (var i = 0; i < m_Parameters.Count; ++i)
{
// Look up type.
var objectRegistrationName = m_Parameters[i].name;
var type = registrations.LookupTypeRegistration(objectRegistrationName);
if (type == null)
{
Debug.LogError(
$"No {typeof(TType).Name} with name '{objectRegistrationName}' (mentioned in '{namesAndParameters}') has been registered");
continue;
}
if (!m_IsControlOnlyResolve)
{
// Instantiate it.
if (!(Activator.CreateInstance(type) is TType instance))
{
Debug.LogError(
$"Type '{type.Name}' registered as '{objectRegistrationName}' (mentioned in '{namesAndParameters}') is not an {typeof(TType).Name}");
continue;
}
// Pass parameters to it.
ApplyParameters(m_Parameters[i].parameters, instance, actionMap, ref binding, objectRegistrationName,
namesAndParameters);
// Add to list.
ArrayHelpers.AppendWithCapacity(ref array, ref count, instance);
}
else
{
Debug.Assert(type.IsInstanceOfType(array[count]), "Type of instance in array does not match expected type");
++count;
}
}
return firstIndex;
}
private static InputBindingComposite InstantiateBindingComposite(ref InputBinding binding, InputActionMap actionMap)
{
var nameAndParametersParsed = NameAndParameters.Parse(binding.effectivePath);
// Look up.
var type = InputBindingComposite.s_Composites.LookupTypeRegistration(nameAndParametersParsed.name);
if (type == null)
throw new InvalidOperationException(
$"No binding composite with name '{nameAndParametersParsed.name}' has been registered");
// Instantiate.
if (!(Activator.CreateInstance(type) is InputBindingComposite instance))
throw new InvalidOperationException(
$"Registered type '{type.Name}' used for '{nameAndParametersParsed.name}' is not an InputBindingComposite");
// Set parameters.
ApplyParameters(nameAndParametersParsed.parameters, instance, actionMap, ref binding, nameAndParametersParsed.name,
binding.effectivePath);
return instance;
}
private static void ApplyParameters(ReadOnlyArray<NamedValue> parameters, object instance, InputActionMap actionMap, ref InputBinding binding, string objectRegistrationName, string namesAndParameters)
{
foreach (var parameter in parameters)
{
// Find field.
var field = instance.GetType().GetField(parameter.name,
BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (field == null)
{
Debug.LogError(
$"Type '{instance.GetType().Name}' registered as '{objectRegistrationName}' (mentioned in '{namesAndParameters}') has no public field called '{parameter.name}'");
continue;
}
var fieldTypeCode = Type.GetTypeCode(field.FieldType);
// See if we have a parameter override.
var parameterOverride =
InputActionRebindingExtensions.ParameterOverride.Find(actionMap, ref binding, parameter.name, objectRegistrationName);
var value = parameterOverride != null
? parameterOverride.Value.value
: parameter.value;
field.SetValue(instance, value.ConvertTo(fieldTypeCode).ToObject());
}
}
private static int AssignCompositePartIndex(object composite, string name, ref int currentCompositePartCount)
{
var type = composite.GetType();
////REVIEW: check for [InputControl] attribute?
////TODO: allow this to be a property instead
// Look up field.
var field = type.GetField(name,
BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (field == null)
throw new InvalidOperationException(
$"Cannot find public field '{name}' used as parameter of binding composite '{composite}' of type '{type}'");
////REVIEW: should we wrap part numbers in a struct instead of using int?
// Type-check.
var fieldType = field.FieldType;
if (fieldType != typeof(int))
throw new InvalidOperationException(
$"Field '{name}' used as a parameter of binding composite '{composite}' must be of type 'int' but is of type '{type.Name}' instead");
////REVIEW: this creates garbage; need a better solution to get to zero garbage during re-resolving
// See if we've already assigned a part index. This can happen if there are multiple bindings
// for the same named slot on the composite (e.g. multiple "Negative" bindings on an axis composite).
var partIndex = (int)field.GetValue(composite);
if (partIndex == 0)
{
// No, not assigned yet. Create new part index.
partIndex = ++currentCompositePartCount;
field.SetValue(composite, partIndex);
}
return partIndex;
}
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,370 @@
using System;
using UnityEngine.InputSystem.LowLevel;
namespace UnityEngine.InputSystem
{
/// <summary>
/// Information passed to <see cref="IInputInteraction">interactions</see>
/// when their associated controls trigger.
/// </summary>
/// <seealso cref="IInputInteraction.Process"/>
public struct InputInteractionContext
{
/// <summary>
/// The action associated with the binding.
/// </summary>
/// <remarks>
/// If the binding is not associated with an action, this is <c>null</c>.
/// </remarks>
/// <seealso cref="InputBinding.action"/>
public InputAction action => m_State.GetActionOrNull(ref m_TriggerState);
/// <summary>
/// The bound control that changed its state to trigger the binding associated
/// with the interaction.
/// </summary>
/// <remarks>
/// In case the binding associated with the interaction is a composite, this is
/// one of the controls that are part of the composite.
/// </remarks>
/// <seealso cref="InputBinding.path"/>
public InputControl control => m_State.GetControl(ref m_TriggerState);
/// <summary>
/// The phase the interaction is currently in.
/// </summary>
/// <remarks>
/// Each interaction on a binding has its own phase independent of the action the binding is applied to.
/// If an interaction gets to "drive" an action at a particular point in time, its phase will determine
/// the phase of the action.
/// </remarks>
/// <seealso cref="InputAction.phase"/>
/// <seealso cref="Started"/>
/// <seealso cref="Waiting"/>
/// <seealso cref="Performed"/>
/// <seealso cref="Canceled"/>
public InputActionPhase phase => m_TriggerState.phase;
/// <summary>
/// Time stamp of the input event that caused <see cref="control"/> to trigger a change in the
/// state of <see cref="action"/>.
/// </summary>
/// <seealso cref="InputEvent.time"/>
public double time => m_TriggerState.time;
/// <summary>
/// Timestamp of the <see cref="InputEvent"/> that caused the interaction to transition
/// to <see cref="InputActionPhase.Started"/>.
/// </summary>
/// <seealso cref="InputEvent.time"/>
public double startTime => m_TriggerState.startTime;
/// <summary>
/// Whether the interaction's <see cref="IInputInteraction.Process"/> method has been called because
/// a timer set by <see cref="SetTimeout"/> has expired.
/// </summary>
/// <seealso cref="SetTimeout"/>
public bool timerHasExpired
{
get => (m_Flags & Flags.TimerHasExpired) != 0;
internal set
{
if (value)
m_Flags |= Flags.TimerHasExpired;
else
m_Flags &= ~Flags.TimerHasExpired;
}
}
/// <summary>
/// True if the interaction is waiting for input
/// </summary>
/// <remarks>
/// By default, an interaction will return this this phase after every time it has been performed
/// (<see cref="InputActionPhase.Performed"/>). This can be changed by using <see cref="PerformedAndStayStarted"/>
/// or <see cref="PerformedAndStayPerformed"/>.
/// </remarks>
/// <seealso cref="InputActionPhase.Waiting"/>
public bool isWaiting => phase == InputActionPhase.Waiting;
/// <summary>
/// True if the interaction has been started.
/// </summary>
/// <seealso cref="InputActionPhase.Started"/>
/// <seealso cref="Started"/>
public bool isStarted => phase == InputActionPhase.Started;
/// <summary>
/// Compute the current level of control actuation.
/// </summary>
/// <returns>The current level of control actuation (usually [0..1]) or -1 if the control is actuated
/// but does not support computing magnitudes.</returns>
/// <seealso cref="ControlIsActuated"/>
/// <seealso cref="InputControl.EvaluateMagnitude()"/>
public float ComputeMagnitude()
{
return m_TriggerState.magnitude;
}
/// <summary>
/// Return true if the control that triggered the interaction has been actuated beyond the given threshold.
/// </summary>
/// <param name="threshold">Threshold that must be reached for the control to be considered actuated. If this is zero,
/// the threshold must be exceeded. If it is any positive value, the value must be at least matched.</param>
/// <returns>True if the trigger control is actuated.</returns>
/// <seealso cref="InputControlExtensions.IsActuated"/>
/// <seealso cref="ComputeMagnitude"/>
public bool ControlIsActuated(float threshold = 0)
{
return InputActionState.IsActuated(ref m_TriggerState, threshold);
}
/// <summary>
/// Mark the interaction has having begun.
/// </summary>
/// <remarks>
/// This affects the current interaction only. There might be multiple interactions on a binding
/// and arbitrary many interactions might concurrently be in started state. However, only one interaction
/// (usually the one that starts first) is allowed to drive the action's state as a whole. If an interaction
/// that is currently driving an action is canceled, however, the next interaction in the list that has
/// been started will take over and continue driving the action.
///
/// <example>
/// <code>
/// public class MyInteraction : IInputInteraction&lt;float&gt;
/// {
/// public void Process(ref IInputInteractionContext context)
/// {
/// if (context.isWaiting &amp;&amp; context.ControlIsActuated())
/// {
/// // We've waited for input and got it. Start the interaction.
/// context.Started();
/// }
/// else if (context.isStarted &amp;&amp; !context.ControlIsActuated())
/// {
/// // Interaction has been completed.
/// context.Performed();
/// }
/// }
///
/// public void Reset()
/// {
/// // No reset code needed. We're not keeping any state locally in the interaction.
/// }
/// }
/// </code>
/// </example>
/// </remarks>
public void Started()
{
m_TriggerState.startTime = time;
m_State.ChangePhaseOfInteraction(InputActionPhase.Started, ref m_TriggerState);
}
/// <summary>
/// Marks the interaction as being performed and then transitions back to <see cref="InputActionPhase.Waiting"/>
/// to wait for input. This behavior is desirable for interaction events that are instant and reflect
/// a transitional interaction pattern such as <see cref="Interactions.PressInteraction"/> or <see cref="Interactions.TapInteraction"/>.
/// </summary>
/// <remarks>
/// Note that this affects the current interaction only. There might be multiple interactions on a binding
/// and arbitrary many interactions might concurrently be in started state. However, only one interaction
/// (usually the one that starts first) is allowed to drive the action's state as a whole. If an interaction
/// that is currently driving an action is canceled, however, the next interaction in the list that has
/// been started will take over and continue driving the action.
/// </remarks>
public void Performed()
{
if (m_TriggerState.phase == InputActionPhase.Waiting)
m_TriggerState.startTime = time;
m_State.ChangePhaseOfInteraction(InputActionPhase.Performed, ref m_TriggerState);
}
/// <summary>
/// Marks the interaction as being performed and then transitions into I <see cref="InputActionPhase.Started"/>
/// to wait for an initial trigger condition to be true before being performed again. This behavior
/// may be desirable for interaction events that reflect transitional interaction patterns but should
/// be considered as started until a cancellation condition is true, such as releasing a button.
/// </summary>
public void PerformedAndStayStarted()
{
if (m_TriggerState.phase == InputActionPhase.Waiting)
m_TriggerState.startTime = time;
m_State.ChangePhaseOfInteraction(InputActionPhase.Performed, ref m_TriggerState,
phaseAfterPerformed: InputActionPhase.Started);
}
/// <summary>
/// Marks the interaction as being performed and then stays in that state waiting for an input to
/// cancel the interactions active state. This behavior is desirable for interaction events that
/// are active for a duration until a cancellation condition is true, such as <see cref="Interactions.HoldInteraction"/> or <see cref="Interactions.TapInteraction"/> where releasing
/// the associated button cancels the interaction..
/// </summary>
public void PerformedAndStayPerformed()
{
if (m_TriggerState.phase == InputActionPhase.Waiting)
m_TriggerState.startTime = time;
m_State.ChangePhaseOfInteraction(InputActionPhase.Performed, ref m_TriggerState,
phaseAfterPerformed: InputActionPhase.Performed);
}
/// <summary>
/// Marks the interaction as being interrupted or aborted. This is relevant to signal that the interaction
/// pattern was not completed, for example, the user pressed and then released a button before the minimum
/// time required for a <see cref="Interactions.HoldInteraction"/> to complete.
/// </summary>
/// <remarks>
/// This is used by most existing interactions to cancel the transitions in the interaction state machine
/// when a condition required to proceed turned false or other indirect requirements were not met, such as
/// time-based conditions.
/// </remarks>
public void Canceled()
{
if (m_TriggerState.phase != InputActionPhase.Canceled)
m_State.ChangePhaseOfInteraction(InputActionPhase.Canceled, ref m_TriggerState);
}
/// <summary>
/// Put the interaction back into <see cref="InputActionPhase.Waiting"/> state.
/// </summary>
/// <seealso cref="InputAction.phase"/>
/// <seealso cref="InputActionPhase"/>
/// <seealso cref="Started"/>
/// <seealso cref="Performed"/>
/// <seealso cref="Canceled"/>
public void Waiting()
{
if (m_TriggerState.phase != InputActionPhase.Waiting)
m_State.ChangePhaseOfInteraction(InputActionPhase.Waiting, ref m_TriggerState);
}
/// <summary>
/// Start a timeout that triggers within <paramref name="seconds"/>.
/// </summary>
/// <param name="seconds">Number of seconds before the timeout is triggered.</param>
/// <remarks>
/// An interaction might wait a set amount of time for something to happen and then
/// do something depending on whether it did or did not happen. By calling this method,
/// a timeout is installed such that in the input update that the timer expires in, the
/// interaction's <see cref="IInputInteraction.Process"/> method is called with <see cref="timerHasExpired"/>
/// being true.
///
/// Changing the phase of the interaction while a timeout is running will implicitly cancel
/// the timeout. For example, you must call <see cref="Started()"/> before calling `SetTimeout()`.
///
/// <example>
/// <code>
/// // Let's say we're writing a Process() method for an interaction that,
/// // after a control has been actuated, waits for 1 second for it to be
/// // released again. If that happens, the interaction performs. If not,
/// // it cancels.
/// public void Process(ref InputInteractionContext context)
/// {
/// // timerHasExpired will be true if we get called when our timeout
/// // has expired.
/// if (context.timerHasExpired)
/// {
/// // The user did not release the control quickly enough.
/// // Our interaction is not successful, so cancel.
/// context.Canceled();
/// return;
/// }
///
/// if (context.ControlIsActuated())
/// {
/// if (!context.isStarted)
/// {
/// // The control has been actuated. We want to give the user a max
/// // of 1 second to release it. So we start the interaction now and then
/// // set the timeout.
/// context.Started();
/// context.SetTimeout(1);
/// }
/// }
/// else
/// {
/// // Control has been released. If we're currently waiting for a release,
/// // it has come in time before out timeout expired. In other words, the
/// // interaction has been successfully performed. We call Performed()
/// // which implicitly removes our ongoing timeout.
/// if (context.isStarted)
/// context.Performed();
/// }
/// }
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="timerHasExpired"/>
public void SetTimeout(float seconds)
{
m_State.StartTimeout(seconds, ref m_TriggerState);
}
/// <summary>
/// Override the default timeout value used by <see cref="InputAction.GetTimeoutCompletionPercentage"/>.
/// </summary>
/// <param name="seconds">Amount of total successive timeouts TODO</param>
/// <exception cref="ArgumentException"></exception>
/// <remarks>
/// By default, timeout completion will be entirely determine by the timeout that is currently
/// running, if any. However, some interactions (such as <see cref="Interactions.MultiTapInteraction"/>)
/// will have to run multiple timeouts in succession. Thus, completion of a single timeout is not
/// the same as completion of the interaction.
///
/// You can use this method to account for this.
///
/// Whenever a timeout completes, the timeout duration will automatically be accumulated towards
/// the total timeout completion time.
///
/// <example>
/// <code>
/// // Let's say we're starting our first timeout and we know that we will run three timeouts
/// // in succession of 2 seconds each. By calling SetTotalTimeoutCompletionTime(), we can account for this.
/// SetTotalTimeoutCompletionTime(3 * 2);
///
/// // Start the first timeout. When this timeout expires, it will automatically
/// // count one second towards the total timeout completion time.
/// SetTimeout(2);
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="InputAction.GetTimeoutCompletionPercentage"/>
public void SetTotalTimeoutCompletionTime(float seconds)
{
if (seconds <= 0)
throw new ArgumentException("Seconds must be a positive value", nameof(seconds));
m_State.SetTotalTimeoutCompletionTime(seconds, ref m_TriggerState);
}
/// <summary>
/// Read the value of the binding that triggered processing of the interaction.
/// </summary>
/// <typeparam name="TValue">Type of value to read from the binding. Must match the value type of the control
/// or composite in effect for the binding.</typeparam>
/// <returns>Value read from the binding.</returns>
public TValue ReadValue<TValue>()
where TValue : struct
{
return m_State.ReadValue<TValue>(m_TriggerState.bindingIndex, m_TriggerState.controlIndex);
}
internal InputActionState m_State;
internal Flags m_Flags;
internal InputActionState.TriggerState m_TriggerState;
internal int mapIndex => m_TriggerState.mapIndex;
internal int controlIndex => m_TriggerState.controlIndex;
internal int bindingIndex => m_TriggerState.bindingIndex;
internal int interactionIndex => m_TriggerState.interactionIndex;
[Flags]
internal enum Flags
{
TimerHasExpired = 1 << 1
}
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b90e4423894dce34f83bb15a3ba411ba
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,139 @@
using System;
using System.ComponentModel;
using UnityEngine.InputSystem.Controls;
#if UNITY_EDITOR
using UnityEngine.InputSystem.Editor;
using UnityEngine.UIElements;
#endif
namespace UnityEngine.InputSystem.Interactions
{
/// <summary>
/// Performs the action if the control is pressed and held for at least the
/// set duration (which defaults to <see cref="InputSettings.defaultHoldTime"/>).
/// </summary>
/// <remarks>
/// The action is started when the control is pressed. If the control is released before the
/// set <see cref="duration"/>, the action is canceled. As soon as the hold time is reached,
/// the action performs. The action then stays performed until the control is released, at
/// which point the action cancels.
///
/// <example>
/// <code>
/// // Action that requires A button on gamepad to be held for half a second.
/// var action = new InputAction(binding: "&lt;Gamepad&gt;/buttonSouth", interactions: "hold(duration=0.5)");
/// </code>
/// </example>
/// </remarks>
[DisplayName("Hold")]
public class HoldInteraction : IInputInteraction
{
/// <summary>
/// Duration in seconds that the control must be pressed for the hold to register.
/// </summary>
/// <remarks>
/// If this is less than or equal to 0 (the default), <see cref="InputSettings.defaultHoldTime"/> is used.
///
/// Duration is expressed in real time and measured against the timestamps of input events
/// (<see cref="LowLevel.InputEvent.time"/>) not against game time (<see cref="Time.time"/>).
/// </remarks>
public float duration;
/// <summary>
/// Magnitude threshold that must be crossed by an actuated control for the control to
/// be considered pressed.
/// </summary>
/// <remarks>
/// If this is less than or equal to 0 (the default), <see cref="InputSettings.defaultButtonPressPoint"/> is used instead.
/// </remarks>
/// <seealso cref="InputControl.EvaluateMagnitude()"/>
public float pressPoint;
private float durationOrDefault => duration > 0.0 ? duration : InputSystem.settings.defaultHoldTime;
private float pressPointOrDefault => pressPoint > 0.0 ? pressPoint : ButtonControl.s_GlobalDefaultButtonPressPoint;
private double m_TimePressed;
/// <inheritdoc />
public void Process(ref InputInteractionContext context)
{
if (context.timerHasExpired)
{
context.PerformedAndStayPerformed();
return;
}
switch (context.phase)
{
case InputActionPhase.Waiting:
if (context.ControlIsActuated(pressPointOrDefault))
{
m_TimePressed = context.time;
context.Started();
context.SetTimeout(durationOrDefault);
}
break;
case InputActionPhase.Started:
// If we've reached our hold time threshold, perform the hold.
// We do this regardless of what state the control changed to.
if (context.time - m_TimePressed >= durationOrDefault)
{
context.PerformedAndStayPerformed();
}
if (!context.ControlIsActuated())
{
// Control is no longer actuated so we're done.
context.Canceled();
}
break;
case InputActionPhase.Performed:
if (!context.ControlIsActuated(pressPointOrDefault))
context.Canceled();
break;
}
}
/// <inheritdoc />
public void Reset()
{
m_TimePressed = 0;
}
}
#if UNITY_EDITOR
/// <summary>
/// UI that is displayed when editing <see cref="HoldInteraction"/> in the editor.
/// </summary>
internal class HoldInteractionEditor : InputParameterEditor<HoldInteraction>
{
protected override void OnEnable()
{
m_PressPointSetting.Initialize("Press Point",
"Float value that an axis control has to cross for it to be considered pressed.",
"Default Button Press Point",
() => target.pressPoint, v => target.pressPoint = v, () => ButtonControl.s_GlobalDefaultButtonPressPoint);
m_DurationSetting.Initialize("Hold Time",
"Time (in seconds) that a control has to be held in order for it to register as a hold.",
"Default Hold Time",
() => target.duration, x => target.duration = x, () => InputSystem.settings.defaultHoldTime);
}
public override void OnGUI()
{
}
public override void OnDrawVisualElements(VisualElement root, Action onChangedCallback)
{
m_PressPointSetting.OnDrawVisualElements(root, onChangedCallback);
m_DurationSetting.OnDrawVisualElements(root, onChangedCallback);
}
private CustomOrDefaultSetting m_PressPointSetting;
private CustomOrDefaultSetting m_DurationSetting;
}
#endif
}

View File

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

View File

@@ -0,0 +1,226 @@
using System;
using UnityEngine.InputSystem.Controls;
#if UNITY_EDITOR
using UnityEngine.InputSystem.Editor;
using UnityEngine.UIElements;
#endif
////TODO: add ability to respond to any of the taps in the sequence (e.g. one response for single tap, another for double tap)
////TODO: add ability to perform on final press rather than on release
////TODO: change this so that the interaction stays performed when the tap count is reached until the button is released
namespace UnityEngine.InputSystem.Interactions
{
////REVIEW: Why is this deriving from IInputInteraction<float>? It goes by actuation just like Hold etc.
/// <summary>
/// Interaction that requires multiple taps (press and release within <see cref="tapTime"/>) spaced no more
/// than <see cref="tapDelay"/> seconds apart. This equates to a chain of <see cref="TapInteraction"/> with
/// a maximum delay between each tap.
/// </summary>
/// <remarks>
/// The interaction goes into <see cref="InputActionPhase.Started"/> on the first press and then will not
/// trigger again until either the full tap sequence is performed (in which case the interaction triggers
/// <see cref="InputActionPhase.Performed"/>) or the multi-tap is aborted by a timeout being hit (in which
/// case the interaction will trigger <see cref="InputActionPhase.Canceled"/>).
/// </remarks>
public class MultiTapInteraction : IInputInteraction<float>
{
/// <summary>
/// The time in seconds within which the control needs to be pressed and released to perform the interaction.
/// </summary>
/// <remarks>
/// If this value is equal to or smaller than zero, the input system will use (<see cref="InputSettings.defaultTapTime"/>) instead.
/// </remarks>
[Tooltip("The maximum time (in seconds) allowed to elapse between pressing and releasing a control for it to register as a tap.")]
public float tapTime;
/// <summary>
/// The time in seconds which is allowed to pass between taps.
/// </summary>
/// <remarks>
/// If this time is exceeded, the multi-tap interaction is canceled.
/// If this value is equal to or smaller than zero, the input system will use the duplicate value of <see cref="tapTime"/> instead.
/// </remarks>
[Tooltip("The maximum delay (in seconds) allowed between each tap. If this time is exceeded, the multi-tap is canceled.")]
public float tapDelay;
/// <summary>
/// The number of taps required to perform the interaction.
/// </summary>
/// <remarks>
/// How many taps need to be performed in succession. Two means double-tap, three means triple-tap, and so on.
/// </remarks>
[Tooltip("How many taps need to be performed in succession. Two means double-tap, three means triple-tap, and so on.")]
public int tapCount = 2;
/// <summary>
/// Magnitude threshold that must be crossed by an actuated control for the control to
/// be considered pressed.
/// </summary>
/// <remarks>
/// If this is less than or equal to 0 (the default), <see cref="InputSettings.defaultButtonPressPoint"/> is used instead.
/// </remarks>
/// <seealso cref="InputControl.EvaluateMagnitude()"/>
public float pressPoint;
private float tapTimeOrDefault => tapTime > 0.0 ? tapTime : InputSystem.settings.defaultTapTime;
internal float tapDelayOrDefault => tapDelay > 0.0 ? tapDelay : InputSystem.settings.multiTapDelayTime;
private float pressPointOrDefault => pressPoint > 0 ? pressPoint : ButtonControl.s_GlobalDefaultButtonPressPoint;
private float releasePointOrDefault => pressPointOrDefault * ButtonControl.s_GlobalDefaultButtonReleaseThreshold;
/// <inheritdoc />
public void Process(ref InputInteractionContext context)
{
if (context.timerHasExpired)
{
// We use timers multiple times but no matter what, if they expire it means
// that we didn't get input in time.
context.Canceled();
return;
}
switch (m_CurrentTapPhase)
{
case TapPhase.None:
if (context.ControlIsActuated(pressPointOrDefault))
{
m_CurrentTapPhase = TapPhase.WaitingForNextRelease;
m_CurrentTapStartTime = context.time;
context.Started();
var maxTapTime = tapTimeOrDefault;
var maxDelayInBetween = tapDelayOrDefault;
context.SetTimeout(maxTapTime);
// We'll be using multiple timeouts so set a total completion time that
// effects the result of InputAction.GetTimeoutCompletionPercentage()
// such that it accounts for the total time we allocate for the interaction
// rather than only the time of one single timeout.
context.SetTotalTimeoutCompletionTime(maxTapTime * tapCount + (tapCount - 1) * maxDelayInBetween);
}
break;
case TapPhase.WaitingForNextRelease:
if (!context.ControlIsActuated(releasePointOrDefault))
{
if (context.time - m_CurrentTapStartTime <= tapTimeOrDefault)
{
++m_CurrentTapCount;
if (m_CurrentTapCount >= tapCount)
{
context.Performed();
}
else
{
m_CurrentTapPhase = TapPhase.WaitingForNextPress;
m_LastTapReleaseTime = context.time;
context.SetTimeout(tapDelayOrDefault);
}
}
else
{
context.Canceled();
}
}
break;
case TapPhase.WaitingForNextPress:
if (context.ControlIsActuated(pressPointOrDefault))
{
if (context.time - m_LastTapReleaseTime <= tapDelayOrDefault)
{
m_CurrentTapPhase = TapPhase.WaitingForNextRelease;
m_CurrentTapStartTime = context.time;
context.SetTimeout(tapTimeOrDefault);
}
else
{
context.Canceled();
}
}
break;
}
}
/// <inheritdoc />
public void Reset()
{
m_CurrentTapPhase = TapPhase.None;
m_CurrentTapCount = 0;
m_CurrentTapStartTime = 0;
m_LastTapReleaseTime = 0;
}
private TapPhase m_CurrentTapPhase;
private int m_CurrentTapCount;
private double m_CurrentTapStartTime;
private double m_LastTapReleaseTime;
private enum TapPhase
{
None,
WaitingForNextRelease,
WaitingForNextPress,
}
}
#if UNITY_EDITOR
/// <summary>
/// UI that is displayed when editing <see cref="HoldInteraction"/> in the editor.
/// </summary>
internal class MultiTapInteractionEditor : InputParameterEditor<MultiTapInteraction>
{
protected override void OnEnable()
{
m_TapTimeSetting.Initialize("Max Tap Duration",
"Time (in seconds) within with a control has to be released again for it to register as a tap. If the control is held "
+ "for longer than this time, the tap is canceled.",
"Default Tap Time",
() => target.tapTime, x => target.tapTime = x, () => InputSystem.settings.defaultTapTime);
m_TapDelaySetting.Initialize("Max Tap Spacing",
"The maximum delay (in seconds) allowed between each tap. If this time is exceeded, the multi-tap is canceled.",
"Default Tap Spacing",
() => target.tapDelay, x => target.tapDelay = x, () => InputSystem.settings.multiTapDelayTime);
m_PressPointSetting.Initialize("Press Point",
"The amount of actuation a control requires before being considered pressed. If not set, default to "
+ "'Default Button Press Point' in the global input settings.",
"Default Button Press Point",
() => target.pressPoint, v => target.pressPoint = v,
() => InputSystem.settings.defaultButtonPressPoint);
}
public override void OnGUI()
{
}
public override void OnDrawVisualElements(VisualElement root, Action onChangedCallback)
{
var tapCountField = new IntegerField(tapLabel)
{
value = target.tapCount,
tooltip = tapTooltip
};
tapCountField.RegisterValueChangedCallback(evt =>
{
target.tapCount = evt.newValue;
onChangedCallback?.Invoke();
});
root.Add(tapCountField);
m_TapDelaySetting.OnDrawVisualElements(root, onChangedCallback);
m_TapTimeSetting.OnDrawVisualElements(root, onChangedCallback);
m_PressPointSetting.OnDrawVisualElements(root, onChangedCallback);
}
private const string tapLabel = "Tap Count";
private const string tapTooltip = "How many taps need to be performed in succession. Two means double-tap, three means triple-tap, and so on.";
private CustomOrDefaultSetting m_PressPointSetting;
private CustomOrDefaultSetting m_TapTimeSetting;
private CustomOrDefaultSetting m_TapDelaySetting;
}
#endif
}

View File

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

View File

@@ -0,0 +1,244 @@
using System;
using System.ComponentModel;
using UnityEngine.InputSystem.Controls;
#if UNITY_EDITOR
using UnityEngine.InputSystem.Editor;
using UnityEngine.UIElements;
#endif
////TODO: protect against the control *hovering* around the press point; this should not fire the press repeatedly; probably need a zone around the press point
////TODO: also, for analog controls, we probably want a deadzone that gives just a tiny little buffer at the low end before the action starts
////REVIEW: shouldn't it use Canceled for release on PressAndRelease instead of triggering Performed again?
namespace UnityEngine.InputSystem.Interactions
{
/// <summary>
/// Performs the action at specific points in a button press-and-release sequence according top <see cref="behavior"/>.
/// </summary>
/// <remarks>
/// By default, uses <see cref="PressBehavior.PressOnly"/> which performs the action as soon as the control crosses the
/// button press threshold defined by <see cref="pressPoint"/>. The action then will not trigger again until the control
/// is first released.
///
/// Can be set to instead trigger on release (that is, when the control goes back below the button press threshold) using
/// <see cref="PressBehavior.ReleaseOnly"/> or can be set to trigger on both press and release using <see cref="PressBehavior.PressAndRelease"/>).
///
/// Note that using an explicit press interaction is only necessary if the goal is to either customize the press behavior
/// of a button or when binding to controls that are not buttons as such (the press interaction compares magnitudes to
/// <see cref="pressPoint"/> and thus any type of control that can deliver a magnitude can act as a button). The default
/// behavior available out of the box when binding <see cref="InputActionType.Button"/> type actions to button-type controls
/// (<see cref="UnityEngine.InputSystem.Controls.ButtonControl"/>) corresponds to using a press modifier with <see cref="behavior"/>
/// set to <see cref="PressBehavior.PressOnly"/> and <see cref="pressPoint"/> left at default.
/// </remarks>
[DisplayName("Press")]
public class PressInteraction : IInputInteraction
{
/// <summary>
/// Amount of actuation required before a control is considered pressed.
/// </summary>
/// <remarks>
/// If zero (default), defaults to <see cref="InputSettings.defaultButtonPressPoint"/>.
/// </remarks>
[Tooltip("The amount of actuation a control requires before being considered pressed. If not set, default to "
+ "'Default Press Point' in the global input settings.")]
public float pressPoint;
////REVIEW: this should really be named "pressBehavior"
/// <summary>
/// Determines how button presses trigger the action.
/// </summary>
/// <remarks>
/// By default (PressOnly), the action is performed on press.
/// With ReleaseOnly, the action is performed on release. With PressAndRelease, the action is
/// performed on press and on release.
/// </remarks>
[Tooltip("Determines how button presses trigger the action. By default (PressOnly), the action is performed on press. "
+ "With ReleaseOnly, the action is performed on release. With PressAndRelease, the action is performed on press and release.")]
public PressBehavior behavior;
private float pressPointOrDefault => pressPoint > 0 ? pressPoint : ButtonControl.s_GlobalDefaultButtonPressPoint;
private float releasePointOrDefault => pressPointOrDefault * ButtonControl.s_GlobalDefaultButtonReleaseThreshold;
private bool m_WaitingForRelease;
public void Process(ref InputInteractionContext context)
{
var actuation = context.ComputeMagnitude();
switch (behavior)
{
case PressBehavior.PressOnly:
if (m_WaitingForRelease)
{
if (actuation <= releasePointOrDefault)
{
m_WaitingForRelease = false;
if (Mathf.Approximately(0f, actuation))
context.Canceled();
else
context.Started();
}
}
else if (actuation >= pressPointOrDefault)
{
m_WaitingForRelease = true;
// Stay performed until release.
context.PerformedAndStayPerformed();
}
else if (actuation > 0 && !context.isStarted)
{
context.Started();
}
else if (Mathf.Approximately(0f, actuation) && context.isStarted)
{
context.Canceled();
}
break;
case PressBehavior.ReleaseOnly:
if (m_WaitingForRelease)
{
if (actuation <= releasePointOrDefault)
{
m_WaitingForRelease = false;
context.Performed();
context.Canceled();
}
}
else if (actuation >= pressPointOrDefault)
{
m_WaitingForRelease = true;
if (!context.isStarted)
context.Started();
}
else
{
var started = context.isStarted;
if (actuation > 0 && !started)
context.Started();
else if (Mathf.Approximately(0, actuation) && started)
context.Canceled();
}
break;
case PressBehavior.PressAndRelease:
if (m_WaitingForRelease)
{
if (actuation <= releasePointOrDefault)
{
m_WaitingForRelease = false;
context.Performed();
if (Mathf.Approximately(0, actuation))
context.Canceled();
}
}
else if (actuation >= pressPointOrDefault)
{
m_WaitingForRelease = true;
context.PerformedAndStayPerformed();
}
else
{
var started = context.isStarted;
if (actuation > 0 && !started)
context.Started();
else if (Mathf.Approximately(0, actuation) && started)
context.Canceled();
}
break;
}
}
public void Reset()
{
m_WaitingForRelease = false;
}
}
/// <summary>
/// Determines how to trigger an action based on button presses.
/// </summary>
/// <seealso cref="PressInteraction.behavior"/>
public enum PressBehavior
{
/// <summary>
/// Perform the action when the button is pressed.
/// </summary>
/// <remarks>
/// Triggers <see cref="InputAction.performed"/> when a control crosses the button press threshold.
/// </remarks>
// ReSharper disable once UnusedMember.Global
PressOnly = 0,
/// <summary>
/// Perform the action when the button is released.
/// </summary>
/// <remarks>
/// Triggers <see cref="InputAction.started"/> when a control crosses the button press threshold and
/// <see cref="InputAction.performed"/> when the control goes back below the button press threshold.
/// </remarks>
// ReSharper disable once UnusedMember.Global
ReleaseOnly = 1,
/// <summary>
/// Perform the action when the button is pressed and when the button is released.
/// </summary>
/// <remarks>
/// Triggers <see cref="InputAction.performed"/> when a control crosses the button press threshold
/// and triggers <see cref="InputAction.performed"/> again when it goes back below the button press
/// threshold.
/// </remarks>
// ReSharper disable once UnusedMember.Global
PressAndRelease = 2,
}
#if UNITY_EDITOR
/// <summary>
/// UI that is displayed when editing <see cref="PressInteraction"/> in the editor.
/// </summary>
// ReSharper disable once UnusedMember.Global
internal class PressInteractionEditor : InputParameterEditor<PressInteraction>
{
protected override void OnEnable()
{
m_PressPointSetting.Initialize("Press Point",
"The amount of actuation a control requires before being considered pressed. If not set, default to "
+ "'Default Button Press Point' in the global input settings.",
"Default Button Press Point",
() => target.pressPoint, v => target.pressPoint = v,
() => InputSystem.settings.defaultButtonPressPoint);
}
public override void OnGUI()
{
}
public override void OnDrawVisualElements(VisualElement root, Action onChangedCallback)
{
root.Add(new HelpBox(helpLabel, HelpBoxMessageType.None));
var behaviourDropdown = new EnumField(triggerLabel, target.behavior)
{
tooltip = triggerTooltip
};
behaviourDropdown.RegisterValueChangedCallback(evt =>
{
target.behavior = (PressBehavior)evt.newValue;
onChangedCallback?.Invoke();
});
root.Add(behaviourDropdown);
m_PressPointSetting.OnDrawVisualElements(root, onChangedCallback);
}
private CustomOrDefaultSetting m_PressPointSetting;
private const string helpLabel = "Note that the 'Press' interaction is only "
+ "necessary when wanting to customize button press behavior. For default press behavior, simply set the action type to 'Button' "
+ "and use the action without interactions added to it.";
private const string triggerLabel = "Trigger Behavior";
private const string triggerTooltip = "Determines how button presses trigger the action. By default (PressOnly), the action is performed on press. "
+ "With ReleaseOnly, the action is performed on release. With PressAndRelease, the action is performed on press and "
+ "canceled on release.";
}
#endif
}

View File

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

View File

@@ -0,0 +1,102 @@
using System;
using System.ComponentModel;
using UnityEngine.InputSystem.Controls;
#if UNITY_EDITOR
using UnityEngine.InputSystem.Editor;
using UnityEngine.UIElements;
#endif
////REVIEW: this is confusing when considered next to HoldInteraction; also it's confusingly named
namespace UnityEngine.InputSystem.Interactions
{
/// <summary>
/// Performs the action if the control is pressed and held for at least the set
/// duration (which defaults to <see cref="InputSettings.defaultSlowTapTime"/>)
/// and then released.
/// </summary>
[DisplayName("Long Tap")]
public class SlowTapInteraction : IInputInteraction
{
/// <summary>
/// The time in seconds within which the control needs to be pressed and released to perform the interaction.
/// </summary>
/// <remarks>
/// If this value is equal to or smaller than zero, the input system will use (<see cref="InputSettings.defaultSlowTapTime"/>) instead.
/// </remarks>
public float duration;
/// <summary>
/// The press point required to perform the interaction.
/// </summary>
/// <remarks>
/// For analog controls (such as trigger axes on a gamepad), the control needs to be engaged by at least this
/// value to perform the interaction.
/// If this value is equal to or smaller than zero, the input system will use (<see cref="InputSettings.defaultButtonPressPoint"/>) instead.
/// </remarks>
public float pressPoint;
////REVIEW: this seems stupid; shouldn't a slow tap just be anything that takes longer than TapTime?
private float durationOrDefault => duration > 0.0f ? duration : InputSystem.settings.defaultSlowTapTime;
private float pressPointOrDefault => pressPoint > 0 ? pressPoint : ButtonControl.s_GlobalDefaultButtonPressPoint;
private double m_SlowTapStartTime;
public void Process(ref InputInteractionContext context)
{
if (context.isWaiting && context.ControlIsActuated(pressPointOrDefault))
{
m_SlowTapStartTime = context.time;
context.Started();
return;
}
if (context.isStarted && !context.ControlIsActuated(pressPointOrDefault))
{
if (context.time - m_SlowTapStartTime >= durationOrDefault)
context.Performed();
else
////REVIEW: does it matter to cancel right after expiration of 'duration' or is it enough to cancel on button up like here?
context.Canceled();
}
}
public void Reset()
{
m_SlowTapStartTime = 0.0;
}
}
#if UNITY_EDITOR
internal class SlowTapInteractionEditor : InputParameterEditor<SlowTapInteraction>
{
protected override void OnEnable()
{
m_DurationSetting.Initialize("Min Tap Duration",
"Minimum time (in seconds) that a control has to be held for it to register as a slow tap. If the control is released "
+ "before this time, the slow tap is canceled.",
"Default Slow Tap Time",
() => target.duration, x => target.duration = x, () => InputSystem.settings.defaultSlowTapTime);
m_PressPointSetting.Initialize("Press Point",
"The amount of actuation a control requires before being considered pressed. If not set, default to "
+ "'Default Button Press Point' in the global input settings.",
"Default Button Press Point",
() => target.pressPoint, v => target.pressPoint = v,
() => InputSystem.settings.defaultButtonPressPoint);
}
public override void OnGUI()
{
}
public override void OnDrawVisualElements(VisualElement root, Action onChangedCallback)
{
m_DurationSetting.OnDrawVisualElements(root, onChangedCallback);
m_PressPointSetting.OnDrawVisualElements(root, onChangedCallback);
}
private CustomOrDefaultSetting m_DurationSetting;
private CustomOrDefaultSetting m_PressPointSetting;
}
#endif
}

View File

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

View File

@@ -0,0 +1,128 @@
using System;
using System.ComponentModel;
using UnityEngine.InputSystem.Controls;
#if UNITY_EDITOR
using UnityEngine.InputSystem.Editor;
using UnityEngine.UIElements;
#endif
namespace UnityEngine.InputSystem.Interactions
{
/// <summary>
/// Performs the action if the control is pressed held for at least the set
/// duration (which defaults to <see cref="InputSettings.defaultTapTime"/>)
/// and then released.
/// </summary>
[DisplayName("Tap")]
public class TapInteraction : IInputInteraction
{
////REVIEW: this should be called tapTime
/// <summary>
/// The time in seconds within which the control needs to be pressed and released to perform the interaction.
/// </summary>
/// <remarks>
/// If this value is equal to or smaller than zero, the input system will use (<see cref="InputSettings.defaultTapTime"/>) instead.
/// </remarks>
public float duration;
/// <summary>
/// The press point required to perform the interaction.
/// </summary>
/// <remarks>
/// For analog controls (such as trigger axes on a gamepad), the control needs to be engaged by at least this
/// value to perform the interaction.
/// If this value is equal to or smaller than zero, the input system will use (<see cref="InputSettings.defaultButtonPressPoint"/>) instead.
/// </remarks>
public float pressPoint;
private float durationOrDefault => duration > 0.0 ? duration : InputSystem.settings.defaultTapTime;
private float pressPointOrDefault => pressPoint > 0 ? pressPoint : ButtonControl.s_GlobalDefaultButtonPressPoint;
private float releasePointOrDefault => pressPointOrDefault * ButtonControl.s_GlobalDefaultButtonReleaseThreshold;
private double m_TapStartTime;
bool canceledFromTimerExpired;
////TODO: make sure 2d doesn't move too far
public void Process(ref InputInteractionContext context)
{
if (context.timerHasExpired)
{
context.Canceled();
// Cache the fact that we canceled the interaction due to a timer expiration.
canceledFromTimerExpired = true;
return;
}
// Check if the control is actuated but avoid starting the interaction if it was canceled due to a timeout.
// Otherwise, we would start the interaction again immediately after it is canceled due to timeout,
// particularly in analog controls such as Gamepad stick or triggers. (ISXB-627)
if (context.isWaiting && context.ControlIsActuated(pressPointOrDefault) && !canceledFromTimerExpired)
{
m_TapStartTime = context.time;
// Set timeout slightly after duration so that if tap comes in exactly at the expiration
// time, it still counts as a valid tap.
context.Started();
context.SetTimeout(durationOrDefault + 0.00001f);
return;
}
if (context.isStarted && !context.ControlIsActuated(releasePointOrDefault))
{
if (context.time - m_TapStartTime <= durationOrDefault)
{
context.Performed();
}
else
{
////REVIEW: does it matter to cancel right after expiration of 'duration' or is it enough to cancel on button up like here?
context.Canceled();
}
}
// Once the control is released, we allow the interaction to be started again.
if (!context.ControlIsActuated(releasePointOrDefault))
{
canceledFromTimerExpired = false;
}
}
public void Reset()
{
m_TapStartTime = 0;
}
}
#if UNITY_EDITOR
internal class TapInteractionEditor : InputParameterEditor<TapInteraction>
{
protected override void OnEnable()
{
m_DurationSetting.Initialize("Max Tap Duration",
"Time (in seconds) within with a control has to be released again for it to register as a tap. If the control is held "
+ "for longer than this time, the tap is canceled.",
"Default Tap Time",
() => target.duration, x => target.duration = x, () => InputSystem.settings.defaultTapTime);
m_PressPointSetting.Initialize("Press Point",
"The amount of actuation a control requires before being considered pressed. If not set, default to "
+ "'Default Button Press Point' in the global input settings.",
"Default Button Press Point",
() => target.pressPoint, v => target.pressPoint = v,
() => InputSystem.settings.defaultButtonPressPoint);
}
public override void OnGUI()
{
}
public override void OnDrawVisualElements(VisualElement root, Action onChangedCallback)
{
m_DurationSetting.OnDrawVisualElements(root, onChangedCallback);
m_PressPointSetting.OnDrawVisualElements(root, onChangedCallback);
}
private CustomOrDefaultSetting m_DurationSetting;
private CustomOrDefaultSetting m_PressPointSetting;
}
#endif
}

View File

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