using System;
using UnityEngine;
using Cinemachine.Utility;
using UnityEngine.Serialization;
namespace Cinemachine
{
///
/// This is a CinemachineComponent in the the Body section of the component pipeline.
/// Its job is to position the camera in a variable relationship to a the vcam's
/// Follow target object, with offsets and damping.
///
/// This component is typically used to implement a camera that follows its target.
/// It can accept player input from an input device, which allows the player to
/// dynamically control the relationship between the camera and the target,
/// for example with a joystick.
///
/// The OrbitalTransposer introduces the concept of __Heading__, which is the direction
/// in which the target is moving, and the OrbitalTransposer will attempt to position
/// the camera in relationship to the heading, which is by default directly behind the target.
/// You can control the default relationship by adjusting the Heading Bias setting.
///
/// If you attach an input controller to the OrbitalTransposer, then the player can also
/// control the way the camera positions itself in relation to the target heading. This allows
/// the camera to move to any spot on an orbit around the target.
///
[DocumentationSorting(6, DocumentationSortingAttribute.Level.UserRef)]
[AddComponentMenu("")] // Don't display in add component menu
[RequireComponent(typeof(CinemachinePipeline))]
[SaveDuringPlay]
public class CinemachineOrbitalTransposer : CinemachineTransposer
{
///
/// How the "forward" direction is defined. Orbital offset is in relation to the forward
/// direction.
///
[DocumentationSorting(6.2f, DocumentationSortingAttribute.Level.UserRef)]
[Serializable]
public struct Heading
{
///
/// Sets the algorithm for determining the target's heading for purposes
/// of re-centering the camera
///
[DocumentationSorting(6.21f, DocumentationSortingAttribute.Level.UserRef)]
public enum HeadingDefinition
{
///
/// Target heading calculated from the difference between its position on
/// the last update and current frame.
///
PositionDelta,
///
/// Target heading calculated from its Rigidbody's velocity.
/// If no Rigidbody exists, it will fall back
/// to HeadingDerivationMode.PositionDelta
///
Velocity,
///
/// Target heading calculated from the Target Transform's euler Y angle
///
TargetForward,
///
/// Default heading is a constant world space heading.
///
WorldForward,
}
/// The method by which the 'default heading' is calculated if
/// recentering to target heading is enabled
[Tooltip("How 'forward' is defined. The camera will be placed by default behind the target. PositionDelta will consider 'forward' to be the direction in which the target is moving.")]
public HeadingDefinition m_HeadingDefinition;
/// Size of the velocity sampling window for target heading filter.
/// Used only if deriving heading from target's movement
[Range(0, 10)]
[Tooltip("Size of the velocity sampling window for target heading filter. This filters out irregularities in the target's movement. Used only if deriving heading from target's movement (PositionDelta or Velocity)")]
public int m_VelocityFilterStrength;
/// Additional Y rotation applied to the target heading.
/// When this value is 0, the camera will be placed behind the target
[Range(-180f, 180f)]
[Tooltip("Where the camera is placed when the X-axis value is zero. This is a rotation in degrees around the Y axis. When this value is 0, the camera will be placed behind the target. Nonzero offsets will rotate the zero position around the target.")]
public float m_HeadingBias;
/// Constructor
public Heading(HeadingDefinition def, int filterStrength, float bias)
{
m_HeadingDefinition = def;
m_VelocityFilterStrength = filterStrength;
m_HeadingBias = bias;
}
};
/// The definition of Forward. Camera will follow behind.
[Space]
[Tooltip("The definition of Forward. Camera will follow behind.")]
public Heading m_Heading = new Heading(Heading.HeadingDefinition.TargetForward, 4, 0);
/// Controls how automatic orbit recentering occurs
[DocumentationSorting(6.5f, DocumentationSortingAttribute.Level.UserRef)]
[Serializable]
public struct Recentering
{
/// If checked, will enable automatic recentering of the
/// camera based on the heading calculation mode. If FALSE, recenting is disabled.
[Tooltip("If checked, will enable automatic recentering of the camera based on the heading definition. 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 input has been detected, the camera will wait this long in seconds before moving its heading to the zero position.")]
public float m_RecenterWaitTime;
/// Maximum angular speed of recentering. Will accelerate into and decelerate out of this
[Tooltip("Maximum angular speed of recentering. Will accelerate into and decelerate out of this.")]
public float m_RecenteringTime;
/// Constructor with specific field values
public Recentering(bool enabled, float recenterWaitTime, float recenteringSpeed)
{
m_enabled = enabled;
m_RecenterWaitTime = recenterWaitTime;
m_RecenteringTime = recenteringSpeed;
m_LegacyHeadingDefinition = m_LegacyVelocityFilterStrength = -1;
}
/// Call this from OnValidate()
public void Validate()
{
m_RecenterWaitTime = Mathf.Max(0, m_RecenterWaitTime);
m_RecenteringTime = Mathf.Max(0, m_RecenteringTime);
}
// Legacy support
[SerializeField] [HideInInspector] [FormerlySerializedAs("m_HeadingDefinition")] private int m_LegacyHeadingDefinition;
[SerializeField] [HideInInspector] [FormerlySerializedAs("m_VelocityFilterStrength")] private int m_LegacyVelocityFilterStrength;
internal bool LegacyUpgrade(ref Heading.HeadingDefinition heading, ref int velocityFilter)
{
if (m_LegacyHeadingDefinition != -1 && m_LegacyVelocityFilterStrength != -1)
{
heading = (Heading.HeadingDefinition)m_LegacyHeadingDefinition;
velocityFilter = m_LegacyVelocityFilterStrength;
m_LegacyHeadingDefinition = m_LegacyVelocityFilterStrength = -1;
return true;
}
return false;
}
};
/// Parameters that control Automating Heading Recentering
[Tooltip("Automatic heading recentering. The settings here defines how the camera will reposition itself in the absence of player input.")]
public Recentering m_RecenterToTargetHeading = new Recentering(true, 1, 2);
/// Axis representing the current heading. Value is in degrees
/// and represents a rotation about the up vector
[Tooltip("Heading Control. The settings here control the behaviour of the camera in response to the player's input.")]
public AxisState m_XAxis = new AxisState(300f, 2f, 1f, 0f, "Mouse X", true);
// Legacy support
[SerializeField] [HideInInspector] [FormerlySerializedAs("m_Radius")] private float m_LegacyRadius = float.MaxValue;
[SerializeField] [HideInInspector] [FormerlySerializedAs("m_HeightOffset")] private float m_LegacyHeightOffset = float.MaxValue;
[SerializeField] [HideInInspector] [FormerlySerializedAs("m_HeadingBias")] private float m_LegacyHeadingBias = float.MaxValue;
protected override void OnValidate()
{
// Upgrade after a legacy deserialize
if (m_LegacyRadius != float.MaxValue
&& m_LegacyHeightOffset != float.MaxValue
&& m_LegacyHeadingBias != float.MaxValue)
{
m_FollowOffset = new Vector3(0, m_LegacyHeightOffset, -m_LegacyRadius);
m_LegacyHeightOffset = m_LegacyRadius = float.MaxValue;
m_Heading.m_HeadingBias = m_LegacyHeadingBias;
m_XAxis.m_MaxSpeed /= 10;
m_XAxis.m_AccelTime /= 10;
m_XAxis.m_DecelTime /= 10;
m_LegacyHeadingBias = float.MaxValue;
m_RecenterToTargetHeading.LegacyUpgrade(
ref m_Heading.m_HeadingDefinition, ref m_Heading.m_VelocityFilterStrength);
}
m_XAxis.Validate();
m_RecenterToTargetHeading.Validate();
base.OnValidate();
}
///
/// Drive the x-axis setting programmatically.
/// Automatic heading updating will be disabled.
///
[HideInInspector, NoSaveDuringPlay]
public bool m_HeadingIsSlave = false;
///
/// Delegate that allows the the m_XAxis object to be replaced with another one.
///
internal delegate float UpdateHeadingDelegate(
CinemachineOrbitalTransposer orbital, float deltaTime, Vector3 up);
///
/// Delegate that allows the the XAxis object to be replaced with another one.
/// To use it, just call orbital.UpdateHeading() with a reference to a
/// private AxisState object, and that AxisState object will be updated and
/// used to calculate the heading.
///
internal UpdateHeadingDelegate HeadingUpdater
= (CinemachineOrbitalTransposer orbital, float deltaTime, Vector3 up)
=> { return orbital.UpdateHeading(deltaTime, up, ref orbital.m_XAxis); };
///
/// Update the X axis and calculate the heading. This can be called by a delegate
/// with a custom axis.
/// Used for damping. If less than 0, no damping is done.
/// World Up, set by the CinemachineBrain
///
/// Axis value
///
public float UpdateHeading(float deltaTime, Vector3 up, ref AxisState axis)
{
// Only read joystick when game is playing
if (deltaTime >= 0 || CinemachineCore.Instance.IsLive(VirtualCamera))
{
bool xAxisInput = false;
xAxisInput |= axis.Update(deltaTime);
if (xAxisInput)
{
mLastHeadingAxisInputTime = Time.time;
mHeadingRecenteringVelocity = 0;
}
}
float targetHeading = GetTargetHeading(axis.Value, GetReferenceOrientation(up), deltaTime);
if (deltaTime < 0)
{
mHeadingRecenteringVelocity = 0;
if (m_RecenterToTargetHeading.m_enabled)
axis.Value = targetHeading;
}
else
{
// Recentering
if (m_BindingMode != BindingMode.SimpleFollowWithWorldUp
&& m_RecenterToTargetHeading.m_enabled
&& (Time.time > (mLastHeadingAxisInputTime + m_RecenterToTargetHeading.m_RecenterWaitTime)))
{
// Scale value determined heuristically, to account for accel/decel
float recenterTime = m_RecenterToTargetHeading.m_RecenteringTime / 3f;
if (recenterTime <= deltaTime)
axis.Value = targetHeading;
else
{
float headingError = Mathf.DeltaAngle(axis.Value, targetHeading);
float absHeadingError = Mathf.Abs(headingError);
if (absHeadingError < UnityVectorExtensions.Epsilon)
{
axis.Value = targetHeading;
mHeadingRecenteringVelocity = 0;
}
else
{
float scale = deltaTime / recenterTime;
float desiredVelocity = Mathf.Sign(headingError)
* Mathf.Min(absHeadingError, absHeadingError * scale);
// Accelerate to the desired velocity
float accel = desiredVelocity - mHeadingRecenteringVelocity;
if ((desiredVelocity < 0 && accel < 0) || (desiredVelocity > 0 && accel > 0))
desiredVelocity = mHeadingRecenteringVelocity + desiredVelocity * scale;
axis.Value += desiredVelocity;
mHeadingRecenteringVelocity = desiredVelocity;
}
}
}
}
float finalHeading = axis.Value;
if (m_BindingMode == BindingMode.SimpleFollowWithWorldUp)
axis.Value = 0;
return finalHeading;
}
private void OnEnable()
{
m_XAxis.SetThresholds(0f, 360f, true);
PreviousTarget = null;
mLastTargetPosition = Vector3.zero;
}
private float mLastHeadingAxisInputTime = 0f;
private float mHeadingRecenteringVelocity = 0f;
private Vector3 mLastTargetPosition = Vector3.zero;
private HeadingTracker mHeadingTracker;
private Rigidbody mTargetRigidBody = null;
private Transform PreviousTarget { get; set; }
private Quaternion mHeadingPrevFrame = Quaternion.identity;
private Vector3 mOffsetPrevFrame = Vector3.zero;
/// Positions the virtual camera according to the transposer rules.
/// The current camera state
/// Used for damping. If less than 0, no damping is done.
public override void MutateCameraState(ref CameraState curState, float deltaTime)
{
//UnityEngine.Profiling.Profiler.BeginSample("CinemachineOrbitalTransposer.MutateCameraState");
InitPrevFrameStateInfo(ref curState, deltaTime);
// Update the heading
if (FollowTarget != PreviousTarget)
{
PreviousTarget = FollowTarget;
mTargetRigidBody = (PreviousTarget == null) ? null : PreviousTarget.GetComponent();
mLastTargetPosition = (PreviousTarget == null) ? Vector3.zero : PreviousTarget.position;
mHeadingTracker = null;
}
float heading = HeadingUpdater(this, deltaTime, curState.ReferenceUp);
if (IsValid)
{
mLastTargetPosition = FollowTarget.position;
// Calculate the heading
if (m_BindingMode != BindingMode.SimpleFollowWithWorldUp)
heading += m_Heading.m_HeadingBias;
Quaternion headingRot = Quaternion.AngleAxis(heading, curState.ReferenceUp);
// Track the target, with damping
Vector3 offset = EffectiveOffset;
Vector3 pos;
Quaternion orient;
TrackTarget(deltaTime, curState.ReferenceUp, headingRot * offset, out pos, out orient);
// Place the camera
curState.ReferenceUp = orient * Vector3.up;
if (deltaTime >= 0)
{
Vector3 bypass = (headingRot * offset) - mHeadingPrevFrame * mOffsetPrevFrame;
bypass = orient * bypass;
curState.PositionDampingBypass = bypass;
}
orient = orient * headingRot;
curState.RawPosition = pos + orient * offset;
mHeadingPrevFrame = (m_BindingMode == BindingMode.SimpleFollowWithWorldUp) ? Quaternion.identity : headingRot;
mOffsetPrevFrame = offset;
}
//UnityEngine.Profiling.Profiler.EndSample();
}
/// API for the editor, to process a position drag from the user.
/// This implementation adds the delta to the follow offset, after zeroing out local x.
/// The amount dragged this frame
public override void OnPositionDragged(Vector3 delta)
{
Quaternion targetOrientation = GetReferenceOrientation(VcamState.ReferenceUp);
Vector3 localOffset = Quaternion.Inverse(targetOrientation) * delta;
localOffset.x = 0;
m_FollowOffset += localOffset;
m_FollowOffset = EffectiveOffset;
}
static string GetFullName(GameObject current)
{
if (current == null)
return "";
if (current.transform.parent == null)
return "/" + current.name;
return GetFullName(current.transform.parent.gameObject) + "/" + current.name;
}
// Make sure this is calld only once per frame
private float GetTargetHeading(
float currentHeading, Quaternion targetOrientation, float deltaTime)
{
if (m_BindingMode == BindingMode.SimpleFollowWithWorldUp)
return 0;
if (FollowTarget == null)
return currentHeading;
if (m_Heading.m_HeadingDefinition == Heading.HeadingDefinition.Velocity
&& mTargetRigidBody == null)
{
Debug.Log(string.Format(
"Attempted to use HeadingDerivationMode.Velocity to calculate heading for {0}. No RigidBody was present on '{1}'. Defaulting to position delta",
GetFullName(VirtualCamera.VirtualCameraGameObject), FollowTarget));
m_Heading.m_HeadingDefinition = Heading.HeadingDefinition.PositionDelta;
}
Vector3 velocity = Vector3.zero;
switch (m_Heading.m_HeadingDefinition)
{
case Heading.HeadingDefinition.PositionDelta:
velocity = FollowTarget.position - mLastTargetPosition;
break;
case Heading.HeadingDefinition.Velocity:
velocity = mTargetRigidBody.velocity;
break;
case Heading.HeadingDefinition.TargetForward:
velocity = FollowTarget.forward;
break;
default:
case Heading.HeadingDefinition.WorldForward:
return 0;
}
// Process the velocity and derive the heading from it.
int filterSize = m_Heading.m_VelocityFilterStrength * 5;
if (mHeadingTracker == null || mHeadingTracker.FilterSize != filterSize)
mHeadingTracker = new HeadingTracker(filterSize);
mHeadingTracker.DecayHistory();
Vector3 up = targetOrientation * Vector3.up;
velocity = velocity.ProjectOntoPlane(up);
if (!velocity.AlmostZero())
mHeadingTracker.Add(velocity);
velocity = mHeadingTracker.GetReliableHeading();
if (!velocity.AlmostZero())
return UnityVectorExtensions.SignedAngle(targetOrientation * Vector3.forward, velocity, up);
// If no reliable heading, then stay where we are.
return currentHeading;
}
class HeadingTracker
{
struct Item
{
public Vector3 velocity;
public float weight;
public float time;
};
Item[] mHistory;
int mTop;
int mBottom;
int mCount;
Vector3 mHeadingSum;
float mWeightSum = 0;
float mWeightTime = 0;
Vector3 mLastGoodHeading = Vector3.zero;
public HeadingTracker(int filterSize)
{
mHistory = new Item[filterSize];
float historyHalfLife = filterSize / 5f; // somewhat arbitrarily
mDecayExponent = -Mathf.Log(2f) / historyHalfLife;
ClearHistory();
}
public int FilterSize { get { return mHistory.Length; } }
void ClearHistory()
{
mTop = mBottom = mCount = 0;
mWeightSum = 0;
mHeadingSum = Vector3.zero;
}
static float mDecayExponent;
static float Decay(float time) { return Mathf.Exp(time * mDecayExponent); }
public void Add(Vector3 velocity)
{
if (FilterSize == 0)
{
mLastGoodHeading = velocity;
return;
}
float weight = velocity.magnitude;
if (weight > UnityVectorExtensions.Epsilon)
{
Item item = new Item();
item.velocity = velocity;
item.weight = weight;
item.time = Time.time;
if (mCount == FilterSize)
PopBottom();
++mCount;
mHistory[mTop] = item;
if (++mTop == FilterSize)
mTop = 0;
mWeightSum *= Decay(item.time - mWeightTime);
mWeightTime = item.time;
mWeightSum += weight;
mHeadingSum += item.velocity;
}
}
void PopBottom()
{
if (mCount > 0)
{
float time = Time.time;
Item item = mHistory[mBottom];
if (++mBottom == FilterSize)
mBottom = 0;
--mCount;
float decay = Decay(time - item.time);
mWeightSum -= item.weight * decay;
mHeadingSum -= item.velocity * decay;
if (mWeightSum <= UnityVectorExtensions.Epsilon || mCount == 0)
ClearHistory();
}
}
public void DecayHistory()
{
float time = Time.time;
float decay = Decay(time - mWeightTime);
mWeightSum *= decay;
mWeightTime = time;
if (mWeightSum < UnityVectorExtensions.Epsilon)
ClearHistory();
else
mHeadingSum = mHeadingSum * decay;
}
public Vector3 GetReliableHeading()
{
// Update Last Good Heading
if (mWeightSum > UnityVectorExtensions.Epsilon
&& (mCount == mHistory.Length || mLastGoodHeading.AlmostZero()))
{
Vector3 h = mHeadingSum / mWeightSum;
if (!h.AlmostZero())
mLastGoodHeading = h.normalized;
}
return mLastGoodHeading;
}
}
}
}