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