UI 자동화를 위해 바인딩 기능 구현
- 유니티 에셋 인증 오류로 meta 재생성
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6adbf65a53ff9b04990c2116b3e4d5b7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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", "<Keyboard>/a")
|
||||
/// .With("Positive", "<Keyboard>/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", "<Keyboard>/a")
|
||||
/// .With("Positive", "<Keyboard>/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", "<Keyboard>/a")
|
||||
/// .With("Positive", "<Keyboard>/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
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 61fdc882d66f0f34d90450c001c0078e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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", "<Keyboard>/leftCtrl")
|
||||
/// .With("Modifier", "<Keyboard>/rightControl")
|
||||
/// .With("Button", "<Keyboard>/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", "<Gamepad>/buttonSouth")
|
||||
/// .With("Button", "<Gamepad>/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><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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 15164829aab964eedaee6bac785c2c05
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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", "<Keyboard>/leftCtrl")
|
||||
/// .With("Modifier1", "<Keyboard>/rightCtrl")
|
||||
/// .With("Modifier2", "<Keyboard>/leftShift")
|
||||
/// .With("Modifier2", "<Keyboard>/rightShift")
|
||||
/// .With("Button", "<Keyboard>/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", "<Gamepad>/buttonSouth")
|
||||
/// .With("Modifier2", "<Gamepad>/buttonWest")
|
||||
/// .With("Button", "<Gamepad>/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><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><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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 62dcc18c42c2246bdaed7fe210b77118
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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", "<Keyboard>/ctrl")
|
||||
/// .With("Binding", "<Keyboard>/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", "<Keyboard>/alt")
|
||||
/// .With("Binding", "<Mouse>/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><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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a0a9c8a3d9c4893ab5389e009563314
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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", "<Keyboard>/ctrl")
|
||||
/// .With("Modifier", "<Keyboard>/shift")
|
||||
/// .With("Binding", "<Keyboard>/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", "<Keyboard>/ctrl")
|
||||
/// .With("Modifier2", "<Keyboard>/shift")
|
||||
/// .With("Binding", "<Mouse>/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><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><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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be272fdf97df4e8daf1393dabdfa4cb8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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", "<Keyboard>/w")
|
||||
/// .With("Left", "<Keyboard>/a")
|
||||
/// .With("Down", "<Keyboard>/s")
|
||||
/// .With("Right", "<Keyboard>/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", "<Gamepad>/leftStick/up")
|
||||
/// .With("down", "<Gamepad>/leftStick/down")
|
||||
/// .With("left", "<Gamepad>/leftStick/left")
|
||||
/// .With("right", "<Gamepad>/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", "<Gamepad>/leftStick/up")
|
||||
/// .With("down", "<Gamepad>/leftStick/down")
|
||||
/// .With("left", "<Gamepad>/leftStick/left")
|
||||
/// .With("right", "<Gamepad>/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", "<Gamepad>/leftStick/up")
|
||||
/// .With("down", "<Gamepad>/leftStick/down")
|
||||
/// .With("left", "<Gamepad>/leftStick/left")
|
||||
/// .With("right", "<Gamepad>/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
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e9c0b68df9234a0bad68ad7e0391330
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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", "<Keyboard>/w")
|
||||
/// .With("Backward", "<Keyboard>/s")
|
||||
/// .With("Left", "<Keyboard>/a")
|
||||
/// .With("Right", "<Keyboard>/d")
|
||||
/// .With("Up", "<Keyboard>/q")
|
||||
/// .With("Down", "<Keyboard>/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
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cda1b45d3bda468a95dca208b99174da
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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 "<Gamepad>/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 = "<Gamepad>/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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0e297b4219a224ba5bcb4f9293e26ea9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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("<Gamepad>/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<MyInteraction>();
|
||||
/// }
|
||||
///
|
||||
/// [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<MyCustomInteraction>
|
||||
/// {
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e37175fb5b321444aa88a861f93360a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
2578
Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs
Normal file
2578
Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 39fa3d8997a24136984ca6e2c99902bc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c77cd15ef9ca14d3e928d192049c276d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
2024
Packages/com.unity.inputsystem/InputSystem/Actions/InputActionMap.cs
Normal file
2024
Packages/com.unity.inputsystem/InputSystem/Actions/InputActionMap.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 050df780a05f4754b28e85d32a88da95
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c2db5674bb583c0449389396f935abf8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0c43a6876f1eb4fdf8b9dc2fee926776
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd166cb4f7947a8498e7ad07a9c04294
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca5a63bf867c240879f2eb2179771ffb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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<float>() > 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<float>());
|
||||
///
|
||||
/// // 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ff1587ed21f442578f1ad36c4779a95
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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: "<Gamepad>/<Button>");
|
||||
///
|
||||
/// 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: "<Gamepad>/leftStick");
|
||||
///
|
||||
/// action.started +=
|
||||
/// ctx =>
|
||||
/// {
|
||||
/// Debug.Log("--- Stick Starts ---");
|
||||
/// };
|
||||
/// action.performed +=
|
||||
/// ctx =>
|
||||
/// {
|
||||
/// Debug.Log("Stick Value: " + ctx.ReadValue<Vector2D>();
|
||||
/// };
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b21f8c6bf1fd94d1fb47907209b23367
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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&Mouse" and "Gamepad" group.
|
||||
/// new InputBinding
|
||||
/// {
|
||||
/// path = "*/{PrimaryAction}",
|
||||
/// groups = "Keyboard&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 = "<Mouse>/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 <Gamepad>/buttonSouth on
|
||||
/// // myAction with a binding to <Gamepad>/buttonNorth.
|
||||
/// myAction.ApplyBindingOverride(
|
||||
/// new InputBinding
|
||||
/// {
|
||||
/// path = "<Gamepad>/buttonSouth",
|
||||
/// overridePath = "<Gamepad>/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 = "<Gamepad>/buttonSouth",
|
||||
/// groups = "Gamepad"
|
||||
/// };
|
||||
///
|
||||
/// // Returns "fire: <Gamepad>/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("<Gamepad>/buttonSouth");
|
||||
/// var mouseBinding = new InputBinding("<Mouse>/leftButton");
|
||||
/// var keyboardBinding = new InputBinding("<Keyboard>/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("<Gamepad>/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 = "<Keyboard>/space",
|
||||
/// groups = "Keyboard",
|
||||
/// action = "Fire"
|
||||
/// };
|
||||
/// var gamepadBinding = new InputBinding
|
||||
/// {
|
||||
/// path = "<Gamepad>/buttonSouth",
|
||||
/// groups = "Gamepad",
|
||||
/// action = "Jump"
|
||||
/// };
|
||||
/// var touchBinding = new InputBinding
|
||||
/// {
|
||||
/// path = "<Touchscreen>/*/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 = "<Keyboard>/space;<Keyboard>/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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 49207568c569440f97a5bfe8b4144a16
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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", "<Keyboard>/w")
|
||||
/// .With("Down", "<Keyboard>/s")
|
||||
/// .With("Left", "<Keyboard>/a")
|
||||
/// .With("Right", "<Keyboard>/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<float>
|
||||
/// {
|
||||
/// [InputControl(layout = "Button")]
|
||||
/// public int button;
|
||||
///
|
||||
/// public float scaleFactor = 1;
|
||||
///
|
||||
/// public override float ReadValue(ref InputBindingComposite context)
|
||||
/// {
|
||||
/// return context.ReadValue<float>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6efadb7e1c29b4055adbd232610a4179
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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<float>
|
||||
/// {
|
||||
/// // 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<float>();
|
||||
///
|
||||
/// // Read the vector.
|
||||
/// var secondValue = context.ReadValue<Vector2>();
|
||||
///
|
||||
/// // 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<Vector2>
|
||||
/// {
|
||||
/// [InputControl(layout = "Vector2")]
|
||||
/// public int part;
|
||||
///
|
||||
/// public override Vector2 ReadValue(ref InputBindingCompositeContext context)
|
||||
/// {
|
||||
/// // Return the Vector3 with the greatest magnitude.
|
||||
/// return context.ReadValue<Vector2, Vector2MagnitudeComparer>(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<float></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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fbe76c3f0e980e6468b895ac1133c20d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 92d6890c6702d4891a156d7d30cdbdcb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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<float>
|
||||
/// {
|
||||
/// public void Process(ref IInputInteractionContext context)
|
||||
/// {
|
||||
/// if (context.isWaiting && context.ControlIsActuated())
|
||||
/// {
|
||||
/// // We've waited for input and got it. Start the interaction.
|
||||
/// context.Started();
|
||||
/// }
|
||||
/// else if (context.isStarted && !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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12826de7f47b478428a9941b5fb5adcf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b90e4423894dce34f83bb15a3ba411ba
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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: "<Gamepad>/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
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6bd8ebd167bb9d34cabf0b443f2d1aaa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c2732a66825034f739cb072c49aa8990
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 802caa2f96f49470facf48de1e652f16
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3afbef7bebf197d43b2352b971a4e15f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: caa587f0fb5099040bf6193f93daec5d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user