using UnityEngine; using System; using Cinemachine.Utility; using UnityEngine.Serialization; namespace Cinemachine { /// /// Axis state for defining how to react to player input. /// The settings here control the responsiveness of the axis to player input. /// [DocumentationSorting(DocumentationSortingAttribute.Level.UserRef)] [Serializable] public struct AxisState { /// The current value of the axis [NoSaveDuringPlay] [Tooltip("The current value of the axis.")] public float Value; /// How to interpret the Max Speed setting. public enum SpeedMode { /// /// The Max Speed setting will be interpreted as a maximum axis speed, in units/second /// MaxSpeed, /// /// The Max Speed setting will be interpreted as a direct multiplier on the input value /// InputValueGain }; /// How to interpret the Max Speed setting. [Tooltip("How to interpret the Max Speed setting: in units/second, or as a " + "direct input value multiplier")] public SpeedMode m_SpeedMode; /// How fast the axis value can travel. Increasing this number /// makes the behaviour more responsive to joystick input [Tooltip("The maximum speed of this axis in units/second, or the input value " + "multiplier, depending on the Speed Mode")] public float m_MaxSpeed; /// The amount of time in seconds it takes to accelerate to /// MaxSpeed with the supplied Axis at its maximum value [Tooltip("The amount of time in seconds it takes to accelerate to MaxSpeed " + "with the supplied Axis at its maximum value")] public float m_AccelTime; /// The amount of time in seconds it takes to decelerate /// the axis to zero if the supplied axis is in a neutral position [Tooltip("The amount of time in seconds it takes to decelerate the axis to " + "zero if the supplied axis is in a neutral position")] public float m_DecelTime; /// The name of this axis as specified in Unity Input manager. /// Setting to an empty string will disable the automatic updating of this axis [FormerlySerializedAs("m_AxisName")] [Tooltip("The name of this axis as specified in Unity Input manager. " + "Setting to an empty string will disable the automatic updating of this axis")] public string m_InputAxisName; /// The value of the input axis. A value of 0 means no input /// You can drive this directly from a /// custom input system, or you can set the Axis Name and have the value /// driven by the internal Input Manager [NoSaveDuringPlay] [Tooltip("The value of the input axis. A value of 0 means no input. " + "You can drive this directly from a custom input system, or you can set " + "the Axis Name and have the value driven by the internal Input Manager")] public float m_InputAxisValue; /// If checked, then the raw value of the input axis will be inverted /// before it is used. [FormerlySerializedAs("m_InvertAxis")] [Tooltip("If checked, then the raw value of the input axis will be inverted " + "before it is used")] public bool m_InvertInput; /// The minimum value for the axis [Tooltip("The minimum value for the axis")] public float m_MinValue; /// The maximum value for the axis [Tooltip("The maximum value for the axis")] public float m_MaxValue; /// If checked, then the axis will wrap around at the /// min/max values, forming a loop [Tooltip("If checked, then the axis will wrap around at the min/max values, " + "forming a loop")] public bool m_Wrap; /// Automatic recentering. Valid only if HasRecentering is true [Tooltip("Automatic recentering to at-rest position")] public Recentering m_Recentering; float m_CurrentSpeed; float m_LastUpdateTime; int m_LastUpdateFrame; /// Constructor with specific values /// /// /// /// /// /// /// /// /// public AxisState( float minValue, float maxValue, bool wrap, bool rangeLocked, float maxSpeed, float accelTime, float decelTime, string name, bool invert) { m_MinValue = minValue; m_MaxValue = maxValue; m_Wrap = wrap; ValueRangeLocked = rangeLocked; HasRecentering = false; m_Recentering = new Recentering(false, 1, 2); m_SpeedMode = SpeedMode.MaxSpeed; m_MaxSpeed = maxSpeed; m_AccelTime = accelTime; m_DecelTime = decelTime; Value = (minValue + maxValue) / 2; m_InputAxisName = name; m_InputAxisValue = 0; m_InvertInput = invert; m_CurrentSpeed = 0f; m_InputAxisProvider = null; m_InputAxisIndex = 0; m_LastUpdateTime = 0; m_LastUpdateFrame = 0; } /// Call from OnValidate: Make sure the fields are sensible public void Validate() { if (m_SpeedMode == SpeedMode.MaxSpeed) m_MaxSpeed = Mathf.Max(0, m_MaxSpeed); m_AccelTime = Mathf.Max(0, m_AccelTime); m_DecelTime = Mathf.Max(0, m_DecelTime); m_MaxValue = Mathf.Clamp(m_MaxValue, m_MinValue, m_MaxValue); } const float Epsilon = UnityVectorExtensions.Epsilon; /// /// Cancel current input state and reset input to 0 /// public void Reset() { m_InputAxisValue = 0; m_CurrentSpeed = 0; m_LastUpdateTime = 0; m_LastUpdateFrame = 0; } /// /// This is an interface to override default querying of Unity's legacy Input system. /// If a befaviour implementing this interface is attached to a Cinemachine virtual camera that /// requires input, that interface will be polled for input instead of the standard Input system. /// public interface IInputAxisProvider { /// Get the value of the input axis /// Which axis to query: 0, 1, or 2. These represent, respectively, the X, Y, and Z axes /// The input value of the axis queried float GetAxisValue(int axis); } IInputAxisProvider m_InputAxisProvider; int m_InputAxisIndex; /// /// Set an input provider for this axis. If an input provider is set, the /// provider will be queried when user input is needed, and the Input Axis Name /// field will be ignored. If no provider is set, then the legacy Input system /// will be queried, using the Input Axis Name. /// /// Which axis will be queried for input /// The input provider public void SetInputAxisProvider(int axis, IInputAxisProvider provider) { m_InputAxisIndex = axis; m_InputAxisProvider = provider; } /// Returns true if this axis has an InputAxisProvider, in which case /// we ignore the input axis name public bool HasInputProvider { get => m_InputAxisProvider != null; } /// /// Updates the state of this axis based on the Input axis defined /// by AxisState.m_AxisName /// /// Delta time in seconds /// Returns true if this axis's input was non-zero this Update, /// false otherwise public bool Update(float deltaTime) { // Update only once per frame if (Time.frameCount == m_LastUpdateFrame) return false; m_LastUpdateFrame = Time.frameCount; // Cheating: we want the render frame time, not the fixed frame time if (deltaTime > 0 && m_LastUpdateTime != 0) deltaTime = Time.realtimeSinceStartup - m_LastUpdateTime; m_LastUpdateTime = Time.realtimeSinceStartup; if (m_InputAxisProvider != null) m_InputAxisValue = m_InputAxisProvider.GetAxisValue(m_InputAxisIndex); else if (!string.IsNullOrEmpty(m_InputAxisName)) { try { m_InputAxisValue = CinemachineCore.GetInputAxis(m_InputAxisName); } catch (ArgumentException e) { Debug.LogError(e.ToString()); } } float input = m_InputAxisValue; if (m_InvertInput) input *= -1f; if (m_SpeedMode == SpeedMode.MaxSpeed) return MaxSpeedUpdate(input, deltaTime); // legacy mode // Direct mode update: maxSpeed interpreted as multiplier input *= m_MaxSpeed; // apply gain if (deltaTime < 0) m_CurrentSpeed = 0; else if (deltaTime > 0.0001f) { float dampTime = Mathf.Abs(input) < Mathf.Abs(m_CurrentSpeed) ? m_DecelTime : m_AccelTime; m_CurrentSpeed += Damper.Damp(input - m_CurrentSpeed, dampTime, deltaTime); // Decelerate to the end points of the range if not wrapping float range = m_MaxValue - m_MinValue; if (!m_Wrap && m_DecelTime > Epsilon && range > Epsilon) { float v0 = ClampValue(Value); float v = ClampValue(v0 + m_CurrentSpeed * deltaTime); float d = (m_CurrentSpeed > 0) ? m_MaxValue - v : v - m_MinValue; if (d < (0.1f * range) && Mathf.Abs(m_CurrentSpeed) > Epsilon) m_CurrentSpeed = Damper.Damp(v - v0, m_DecelTime, deltaTime) / deltaTime; } input = m_CurrentSpeed * deltaTime; } Value = ClampValue(Value + m_CurrentSpeed); return Mathf.Abs(input) > Epsilon; } float ClampValue(float v) { float r = m_MaxValue - m_MinValue; if (m_Wrap && r > Epsilon) { v = (v - m_MinValue) % r; v += m_MinValue + ((v < 0) ? r : 0); } return Mathf.Clamp(v, m_MinValue, m_MaxValue); } bool MaxSpeedUpdate(float input, float deltaTime) { if (m_MaxSpeed > Epsilon) { float targetSpeed = input * m_MaxSpeed; if (Mathf.Abs(targetSpeed) < Epsilon || (Mathf.Sign(m_CurrentSpeed) == Mathf.Sign(targetSpeed) && Mathf.Abs(targetSpeed) < Mathf.Abs(m_CurrentSpeed))) { // Need to decelerate float a = Mathf.Abs(targetSpeed - m_CurrentSpeed) / Mathf.Max(Epsilon, m_DecelTime); float delta = Mathf.Min(a * deltaTime, Mathf.Abs(m_CurrentSpeed)); m_CurrentSpeed -= Mathf.Sign(m_CurrentSpeed) * delta; } else { // Accelerate to the target speed float a = Mathf.Abs(targetSpeed - m_CurrentSpeed) / Mathf.Max(Epsilon, m_AccelTime); m_CurrentSpeed += Mathf.Sign(targetSpeed) * a * deltaTime; if (Mathf.Sign(m_CurrentSpeed) == Mathf.Sign(targetSpeed) && Mathf.Abs(m_CurrentSpeed) > Mathf.Abs(targetSpeed)) { m_CurrentSpeed = targetSpeed; } } } // Clamp our max speeds so we don't go crazy float maxSpeed = GetMaxSpeed(); m_CurrentSpeed = Mathf.Clamp(m_CurrentSpeed, -maxSpeed, maxSpeed); if (Mathf.Abs(m_CurrentSpeed) < Epsilon) m_CurrentSpeed = 0; Value += m_CurrentSpeed * deltaTime; bool isOutOfRange = (Value > m_MaxValue) || (Value < m_MinValue); if (isOutOfRange) { if (m_Wrap) { if (Value > m_MaxValue) Value = m_MinValue + (Value - m_MaxValue); else Value = m_MaxValue + (Value - m_MinValue); } else { Value = Mathf.Clamp(Value, m_MinValue, m_MaxValue); m_CurrentSpeed = 0f; } } return Mathf.Abs(input) > Epsilon; } // MaxSpeed may be limited as we approach the range ends, in order // to prevent a hard bump float GetMaxSpeed() { float range = m_MaxValue - m_MinValue; if (!m_Wrap && range > 0) { float threshold = range / 10f; if (m_CurrentSpeed > 0 && (m_MaxValue - Value) < threshold) { float t = (m_MaxValue - Value) / threshold; return Mathf.Lerp(0, m_MaxSpeed, t); } else if (m_CurrentSpeed < 0 && (Value - m_MinValue) < threshold) { float t = (Value - m_MinValue) / threshold; return Mathf.Lerp(0, m_MaxSpeed, t); } } return m_MaxSpeed; } /// Value range is locked, i.e. not adjustable by the user (used by editor) public bool ValueRangeLocked { get; set; } /// True if the Recentering member is valid (bcak-compatibility support: /// old versions had recentering in a separate structure) public bool HasRecentering { get; set; } /// Helper for automatic axis recentering [DocumentationSorting(DocumentationSortingAttribute.Level.UserRef)] [Serializable] public struct Recentering { /// If checked, will enable automatic recentering of the /// axis. If FALSE, recenting is disabled. [Tooltip("If checked, will enable automatic recentering of the axis. If unchecked, recenting is disabled.")] public bool m_enabled; /// If no input has been detected, the camera will wait /// this long in seconds before moving its heading to the default heading. [Tooltip("If no user input has been detected on the axis, the axis will wait this long in seconds before recentering.")] public float m_WaitTime; /// How long it takes to reach destination once recentering has started [Tooltip("How long it takes to reach destination once recentering has started.")] public float m_RecenteringTime; float m_LastUpdateTime; /// Constructor with specific field values /// /// /// public Recentering(bool enabled, float waitTime, float recenteringTime) { m_enabled = enabled; m_WaitTime = waitTime; m_RecenteringTime = recenteringTime; mLastAxisInputTime = 0; mRecenteringVelocity = 0; m_LegacyHeadingDefinition = m_LegacyVelocityFilterStrength = -1; m_LastUpdateTime = 0; } /// Call this from OnValidate() public void Validate() { m_WaitTime = Mathf.Max(0, m_WaitTime); m_RecenteringTime = Mathf.Max(0, m_RecenteringTime); } // Internal state float mLastAxisInputTime; float mRecenteringVelocity; /// /// Copy Recentering state from another Recentering component. /// /// public void CopyStateFrom(ref Recentering other) { if (mLastAxisInputTime != other.mLastAxisInputTime) other.mRecenteringVelocity = 0; mLastAxisInputTime = other.mLastAxisInputTime; } /// Cancel any recenetering in progress. public void CancelRecentering() { mLastAxisInputTime = Time.realtimeSinceStartup; mRecenteringVelocity = 0; } /// Skip the wait time and start recentering now (only if enabled). public void RecenterNow() => mLastAxisInputTime = -1; /// Bring the axis back to the centered state (only if enabled). /// The axis to recenter /// Current effective deltaTime /// The value that is considered to be centered public void DoRecentering(ref AxisState axis, float deltaTime, float recenterTarget) { // Cheating: we want the render frame time, not the fixed frame time if (deltaTime > 0) deltaTime = Time.realtimeSinceStartup - m_LastUpdateTime; m_LastUpdateTime = Time.realtimeSinceStartup; if (!m_enabled && deltaTime >= 0) return; recenterTarget = axis.ClampValue(recenterTarget); if (deltaTime < 0) { CancelRecentering(); if (m_enabled) axis.Value = recenterTarget; return; } float v = axis.ClampValue(axis.Value); float delta = recenterTarget - v; if (delta == 0) return; // Time to start recentering? if (mLastAxisInputTime >= 0 && Time.realtimeSinceStartup < (mLastAxisInputTime + m_WaitTime)) return; // nope // Determine the direction float r = axis.m_MaxValue - axis.m_MinValue; if (axis.m_Wrap && Mathf.Abs(delta) > r * 0.5f) v += Mathf.Sign(recenterTarget - v) * r; // Damp our way there if (m_RecenteringTime < 0.001f || Mathf.Abs(v - recenterTarget) < 0.001f) v = recenterTarget; else v = Mathf.SmoothDamp( v, recenterTarget, ref mRecenteringVelocity, m_RecenteringTime, 9999, deltaTime); axis.Value = axis.ClampValue(v); } // Legacy support [SerializeField] [HideInInspector] [FormerlySerializedAs("m_HeadingDefinition")] int m_LegacyHeadingDefinition; [SerializeField] [HideInInspector] [FormerlySerializedAs("m_VelocityFilterStrength")] int m_LegacyVelocityFilterStrength; internal bool LegacyUpgrade(ref int heading, ref int velocityFilter) { if (m_LegacyHeadingDefinition != -1 && m_LegacyVelocityFilterStrength != -1) { heading = m_LegacyHeadingDefinition; velocityFilter = m_LegacyVelocityFilterStrength; m_LegacyHeadingDefinition = m_LegacyVelocityFilterStrength = -1; return true; } return false; } } } }