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;
}
}
}
}