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 { /// /// A floating-point axis control. /// /// /// Can optionally be configured to perform normalization. /// Stored as either a float, a short, a byte, or a single bit. /// public class AxisControl : InputControl { /// /// Clamping behavior for an axis control. /// public enum Clamp { /// /// Do not clamp values. /// None = 0, /// /// Clamp values to and /// before normalizing the value. /// BeforeNormalize = 1, /// /// Clamp values to and /// after normalizing the value. /// AfterNormalize = 2, /// /// Clamp values any value below or above /// to before normalizing the value. /// 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. /// /// Clamping behavior when reading values. by default. /// /// Clamping behavior. /// /// When a value is read from the control's state, it is first converted /// to a floating-point number. /// /// /// /// public Clamp clamp; /// /// Lower end of the clamping range when is not /// . /// /// Lower bound of clamping range. Inclusive. public float clampMin; /// /// Upper end of the clamping range when is not /// . /// /// Upper bound of clamping range. Inclusive. public float clampMax; /// /// When is set to /// and the value is outside of the range defined by and /// , this value is returned. /// /// Constant value to return when value is outside of clamping range. public float clampConstant; ////REVIEW: why not just roll this into scaleFactor? /// /// If true, the input value will be inverted, i.e. multiplied by -1. Off by default. /// /// Whether to invert the input value. public bool invert; /// /// If true, normalize the input value to [0..1] or [-1..1] (depending on the /// value of . Off by default. /// /// Whether to normalize input values or not. /// /// public bool normalize; ////REVIEW: shouldn't these come from the control min/max value by default? /// /// If is on, this is the input value that corresponds /// to 0 of the normalized [0..1] or [-1..1] range. /// /// Input value that should become 0 or -1. /// /// In other words, with on, input values are mapped from /// the range of [normalizeMin..normalizeMax] to [0..1] or [-1..1] (depending on /// ). /// public float normalizeMin; /// /// If is on, this is the input value that corresponds /// to 1 of the normalized [0..1] or [-1..1] range. /// /// Input value that should become 1. /// /// In other words, with on, input values are mapped from /// the range of [normalizeMin..normalizeMax] to [0..1] or [-1..1] (depending on /// ). /// public float normalizeMax; /// /// Where to put the zero point of the normalization range. Only relevant /// if is set to true. Defaults to 0. /// /// Zero point of normalization range. /// /// The value of this property determines where the zero point is located in the /// range established by and . /// /// If normalizeZero is placed at , the normalization /// returns a value in the [0..1] range mapped from the input value range of /// and . /// /// If normalizeZero is placed in-between and /// , normalization returns a value in the [-1..1] mapped /// from the input value range of and /// and the zero point between the two established by normalizeZero. /// public float normalizeZero; ////REVIEW: why not just have a default scaleFactor of 1? /// /// Whether the scale the input value by . Off by default. /// /// True if inputs should be scaled by . public bool scale; /// /// Value to multiple input values with. Only applied if is true. /// /// Multiplier for input values. public float scaleFactor; /// /// Apply modifications to the given value according to the parameters configured /// on the control (, , etc). /// /// Input value. /// A processed value (clamped, normalized, etc). /// /// /// /// [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; } /// /// Default-initialize the control. /// /// /// Defaults the format to . /// 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); } /// 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); } } } /// 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; } } /// public override unsafe bool CompareValue(void* firstStatePtr, void* secondStatePtr) { var currentValue = ReadValueFromState(firstStatePtr); var valueInState = ReadValueFromState(secondStatePtr); return !Mathf.Approximately(currentValue, valueInState); } /// 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; } } }