using UnityEngine; using System; using System.Collections.Generic; namespace Unity.Cinemachine { /// /// Components that hold InputAxisValue structs must implement this interface to be discoverable. /// public interface IInputAxisOwner { /// /// Describes an axis for an axis driver /// public struct AxisDescriptor { /// Delegate to get a reference to the axis being driven /// A reference to the axis being driven public delegate ref InputAxis AxisGetter(); /// The axis to drive public AxisGetter DrivenAxis; /// The name to display for the axis public string Name; /// /// This provides a hint about the intended usage of the axis. /// public enum Hints { /// No hint Default, /// Mapping should be the first dimension of a multi-dimensional action X, /// Mapping should be the second dimension of a multi-dimensional action Y }; /// Indicates what is the intended usage of the axis. public Hints Hint; } /// /// Report the input axis to be driven, and their names /// /// Axes to drive public void GetInputAxes(List axes); } /// /// Components that can generate an input axis reset must implement this interface. /// public interface IInputAxisResetSource { /// /// Register a handler that will be called when input needs to be reset /// /// Then handler to register public void RegisterResetHandler(Action handler); /// /// Unregister a handler that will be called when input needs to be reset /// /// Then handler to unregister public void UnregisterResetHandler(Action handler); /// Checks whether any reset handlers have been registered /// True if at least one reset handler is registered public bool HasResetHandler { get; } } /// Abstraction for reading the value of an input axis public interface IInputAxisReader { /// Get the current value of the axis. /// The owner GameObject, can be used for logging diagnostics /// A hint for converting a Vector2 value to a float /// The axis value public float GetValue( UnityEngine.Object context, IInputAxisOwner.AxisDescriptor.Hints hint); } /// /// Defines an input axis. This is a field that can take on any value in a range, /// with optional wrapping to form a loop. /// [Serializable] public struct InputAxis { /// The current value of the axis. You can drive this directly from a script [Tooltip("The current value of the axis. You can drive this directly from a script.")] [NoSaveDuringPlay] public float Value; /// The centered, or at-rest value of this axis. [Delayed, Tooltip("The centered, or at-rest value of this axis.")] public float Center; /// The valid range for the axis value. Value will be clamped to this range. [Tooltip("The valid range for the axis value. Value will be clamped to this range.")] [Vector2AsRange] public Vector2 Range; /// If set, then the axis will wrap around at the min/max values, forming a loop [Tooltip("If set, then the axis will wrap around at the min/max values, forming a loop")] public bool Wrap; /// Defines the settings for automatic re-centering [Serializable] public struct RecenteringSettings { /// If set, will enable automatic re-centering of the axis [Tooltip("If set, will enable automatic re-centering of the axis")] public bool Enabled; /// If no user input has been detected on the axis for this man /// seconds, re-centering will begin. [Tooltip("If no user input has been detected on the axis for this many " + "seconds, re-centering will begin.")] public float Wait; /// How long it takes to reach center once re-centering has started [Tooltip("How long it takes to reach center once re-centering has started.")] public float Time; /// Default value public static RecenteringSettings Default => new() { Wait = 1, Time = 2 }; /// Call from OnValidate: Make sure the fields are sensible public void Validate() { Wait = Mathf.Max(0, Wait); Time = Mathf.Max(0, Time); } } /// Controls automatic re-centering of axis [FoldoutWithEnabledButton] public RecenteringSettings Recentering; /// Some usages require restricted functionality. /// The possible restrictions are defined here. [Flags] public enum RestrictionFlags { /// No restrictions None = 0, /// Range and center are not editable by the user RangeIsDriven = 1, /// Indicates that re-centering this axis is not possible NoRecentering = 2, /// Axis represents a momentary spring-back control Momentary = 4, }; /// Some usages require restricted functionality. This is set here. [HideInInspector] public RestrictionFlags Restrictions; /// Clamp the value to range, taking wrap into account /// The value to clamp /// The value clamped to the axis range public float ClampValue(float v) { float r = Range.y - Range.x; if (!Wrap || r < UnityVectorExtensions.Epsilon) return Mathf.Clamp(v, Range.x, Range.y); var v1 = (v - Range.x) % r; v1 += v1 < 0 ? r : 0; return v1 + Range.x; } /// Clamp and scale the value to range 0...1, taking wrap into account /// The axis value, mapped onto [0...1] public float GetNormalizedValue() { float v = ClampValue(Value); float r = Range.y - Range.x; return (v - Range.x) / (r > UnityVectorExtensions.Epsilon ? r : 1); } /// Get the clamped axis value /// The axis value, clamped to the axis range public float GetClampedValue() => ClampValue(Value); /// Make sure the settings are well-formed public void Validate() { Range.y = Mathf.Max(Range.x, Range.y); Center = ClampValue(Center); Value = ClampValue(Value); Recentering.Validate(); } /// Reset axis to at-rest state public void Reset() { CancelRecentering(); if (Recentering.Enabled && (Restrictions & RestrictionFlags.NoRecentering) == 0) Value = ClampValue(Center); } /// An InputAxis set up as a normalized momentary control ranging from -1...1 with Center = 0 public static InputAxis DefaultMomentary => new () { Range = new Vector2(-1, 1), Restrictions = RestrictionFlags.NoRecentering | RestrictionFlags.Momentary }; /// Internal state for re-centering struct RecenteringState { public const float k_Epsilon = UnityVectorExtensions.Epsilon; public float m_RecenteringVelocity; public bool m_ForceRecenter; public float m_LastValueChangeTime; public float m_LastValue; public static float CurrentTime => CinemachineCore.CurrentUnscaledTime; } RecenteringState m_RecenteringState; /// /// Call this before calling UpdateRecentering. Will track any value changes so that the re-centering clock /// is updated properly. /// /// True if value changed. This value can be used to cancel re-centering when multiple /// input axes are coupled. public bool TrackValueChange() { var v = ClampValue(Value); if (v != m_RecenteringState.m_LastValue) { m_RecenteringState.m_LastValueChangeTime = RecenteringState.CurrentTime; m_RecenteringState.m_LastValue = v; return true; } return false; } internal void SetValueAndLastValue(float value) { Value = m_RecenteringState.m_LastValue = value; } /// Call this to manage re-centering axis value to axis center. /// This assumes that TrackValueChange() has been called already this frame. /// Current deltaTime, or -1 for immediate re-centering /// If true, cancel any re-centering currently in progress and reset the timer. public void UpdateRecentering(float deltaTime, bool forceCancel) { if ((Restrictions & (RestrictionFlags.NoRecentering | RestrictionFlags.Momentary)) != 0) return; if (forceCancel) { CancelRecentering(); return; } if (m_RecenteringState.m_ForceRecenter || (Recentering.Enabled && deltaTime < 0)) { Value = Center; CancelRecentering(); } else if (m_RecenteringState.m_ForceRecenter || (Recentering.Enabled && RecenteringState.CurrentTime - m_RecenteringState.m_LastValueChangeTime >= Recentering.Wait)) { var v = ClampValue(Value); var c = Center; var distance = Mathf.Abs(c - v); if (distance < RecenteringState.k_Epsilon || Recentering.Time < RecenteringState.k_Epsilon) { v = c; m_RecenteringState.m_RecenteringVelocity = 0; } else { // Determine the direction float r = Range.y - Range.x; if (Wrap && distance > r * 0.5f) v += Mathf.Sign(c - v) * r; // Damp our way there v = Mathf.SmoothDamp( v, c, ref m_RecenteringState.m_RecenteringVelocity, Recentering.Time * 0.5f, 9999, deltaTime); } Value = m_RecenteringState.m_LastValue = ClampValue(v); // Are we there yet? if (Mathf.Abs(Value - c) < RecenteringState.k_Epsilon) m_RecenteringState.m_ForceRecenter = false; } } /// Trigger re-centering immediately, regardless of whether re-centering /// is enabled or the wait time has elapsed. public void TriggerRecentering() => m_RecenteringState.m_ForceRecenter = true; /// Cancel any current re-centering in progress, and reset the wait time public void CancelRecentering() { m_RecenteringState.m_LastValueChangeTime = RecenteringState.CurrentTime; m_RecenteringState.m_LastValue = ClampValue(Value); m_RecenteringState.m_RecenteringVelocity = 0; m_RecenteringState.m_ForceRecenter = false; } } /// /// This object drives an input axis. /// It reads raw input, applies it to the axis value, with acceleration and deceleration. /// [Serializable] public struct DefaultInputAxisDriver { /// Internal state float m_CurrentSpeed; /// 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 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 DecelTime; /// Call from OnValidate: Make sure the fields are sensible public void Validate() { AccelTime = Mathf.Max(0, AccelTime); DecelTime = Mathf.Max(0, DecelTime); } /// Default value public static DefaultInputAxisDriver Default => new () { AccelTime = 0.2f, DecelTime = 0.2f }; /// Apply the input value to the axis value /// The InputAxisValue to update /// The input value to apply to the axis value. /// current deltaTime public void ProcessInput(ref InputAxis axis, float inputValue, float deltaTime) { const float k_Epsilon = UnityVectorExtensions.Epsilon; var dampTime = Mathf.Abs(inputValue) < Mathf.Abs(m_CurrentSpeed) ? DecelTime : AccelTime; if ((axis.Restrictions & InputAxis.RestrictionFlags.Momentary) == 0) { if (deltaTime < 0) m_CurrentSpeed = 0; else { m_CurrentSpeed += Damper.Damp(inputValue - m_CurrentSpeed, dampTime, deltaTime); // Decelerate to the end points of the range if not wrapping if (!axis.Wrap && DecelTime > k_Epsilon && Mathf.Abs(m_CurrentSpeed) > k_Epsilon) { var v0 = axis.ClampValue(axis.Value); var d = (m_CurrentSpeed > 0) ? axis.Range.y - v0 : v0 - axis.Range.x; var maxSpeed = 0.1f + 4 * d / DecelTime; if (Mathf.Abs(m_CurrentSpeed) > Mathf.Abs(maxSpeed)) m_CurrentSpeed = maxSpeed * Mathf.Sign(m_CurrentSpeed); } } axis.Value = axis.ClampValue(axis.Value + m_CurrentSpeed * deltaTime); } else { // For momentary controls, input is the desired offset from center if (deltaTime < 0) axis.Value = axis.Center; else { var desiredValue = axis.ClampValue(inputValue + axis.Center); axis.Value += Damper.Damp(desiredValue - axis.Value, dampTime, deltaTime); } } } /// Reset an axis to at-rest state /// The axis to reset public void Reset(ref InputAxis axis) { m_CurrentSpeed = 0; axis.Reset(); } } }