using UnityEngine;
using System;
using UnityEngine.Splines;
using UnityEngine.Serialization;
namespace Unity.Cinemachine
{
///
/// A Cinemachine Camera Body component that constrains camera motion to a Spline.
/// The camera can move along the spline.
///
/// This behaviour can operate in two modes: manual positioning, and Auto-Dolly positioning.
/// In Manual mode, the camera's position is specified by animating the Spline Position field.
/// In Auto-Dolly mode, the Spline Position field is animated automatically every frame by finding
/// the position on the spline that's closest to the camera's tracking target.
///
[AddComponentMenu("Cinemachine/Procedural/Position Control/Cinemachine Spline Dolly")]
[SaveDuringPlay]
[DisallowMultipleComponent]
[CameraPipeline(CinemachineCore.Stage.Body)]
[HelpURL(Documentation.BaseURL + "manual/CinemachineSplineDolly.html")]
public class CinemachineSplineDolly : CinemachineComponentBase
{
///
/// Holds the Spline container, the spline position, and the position unit type
///
public SplineSettings SplineSettings = new () { Units = PathIndexUnit.Normalized };
/// Where to put the camera relative to the spline position. X is perpendicular
/// to the spline, Y is up, and Z is parallel to the spline.
[Tooltip("Where to put the camera relative to the spline position. X is perpendicular "
+ "to the spline, Y is up, and Z is parallel to the spline.")]
public Vector3 SplineOffset = Vector3.zero;
/// How to set the camera's rotation and Up. This will affect the screen composition.
[Tooltip("How to set the camera's rotation and Up. This will affect the screen composition, because "
+ "the camera Aim behaviours will always try to respect the Up direction.")]
[FormerlySerializedAs("CameraUp")]
public RotationMode CameraRotation = RotationMode.Default;
/// Different ways to set the camera's up vector
public enum RotationMode
{
/// Leave the camera's up vector alone. It will be set according to the Brain's WorldUp.
Default,
/// Take the up vector from the spline's up vector at the current point
Spline,
/// Take the up vector from the spline's up vector at the current point, but with the roll zeroed out
SplineNoRoll,
/// Take the up vector from the Follow target's up vector
FollowTarget,
/// Take the up vector from the Follow target's up vector, but with the roll zeroed out
FollowTargetNoRoll,
};
/// Settings for controlling damping
[Serializable]
public struct DampingSettings
{
/// Enables damping, which causes the camera to move gradually towards
/// the desired spline position.
[Tooltip("Enables damping, which causes the camera to move gradually towards the desired spline position")]
public bool Enabled;
/// How aggressively the camera tries to maintain the offset along
/// the x, y, or z directions in spline local space.
/// Meaning:
/// - x represents the axis that is perpendicular to the spline. Use this to smooth out
/// imperfections in the path. This may move the camera off the spline.
/// - y represents the axis that is defined by the spline-local up direction. Use this to smooth out
/// imperfections in the path. This may move the camera off the spline.
/// - z represents the axis that is parallel to the spline. This won't move the camera off the spline.
/// Smaller numbers are more responsive. Larger numbers give a heavier more slowly responding camera.
/// Using different settings per axis can yield a wide range of camera behaviors.
[Tooltip("How aggressively the camera tries to maintain the offset along the "
+ "x, y, or z directions in spline local space. \n"
+ "- x represents the axis that is perpendicular to the spline. Use this to smooth out "
+ "imperfections in the path. This may move the camera off the spline.\n"
+ "- y represents the axis that is defined by the spline-local up direction. Use this to smooth out "
+ "imperfections in the path. This may move the camera off the spline.\n"
+ "- z represents the axis that is parallel to the spline. This won't move the camera off the spline.\n\n"
+ "Smaller numbers are more responsive, larger numbers give a heavier more slowly responding camera. "
+ "Using different settings per axis can yield a wide range of camera behaviors.")]
public Vector3 Position;
/// How aggressively the camera tries to maintain the desired rotation.
/// This is only used if Camera Rotation is not Default.
[Range(0f, 20f)]
[Tooltip("How aggressively the camera tries to maintain the desired rotation. "
+ "This is only used if Camera Rotation is not Default.")]
public float Angular;
}
/// Settings for controlling damping, which causes the camera to
/// move gradually towards the desired spline position
[FoldoutWithEnabledButton]
[Tooltip("Settings for controlling damping, which causes the camera to "
+ "move gradually towards the desired spline position")]
public DampingSettings Damping;
/// Controls how automatic dolly occurs
[NoSaveDuringPlay]
[FoldoutWithEnabledButton]
[Tooltip("Controls how automatic dolly occurs. A tracking target may be necessary to use this feature.")]
public SplineAutoDolly AutomaticDolly;
// State info for damping
float m_PreviousSplinePosition;
Quaternion m_PreviousRotation;
Vector3 m_PreviousPosition;
CinemachineSplineRoll m_RollCache; // don't use this directly - use SplineRoll
// In-editor only: CM 3.0.x Legacy support =================================
[SerializeField, HideInInspector, FormerlySerializedAs("CameraPosition")] private float m_LegacyPosition = -1;
[SerializeField, HideInInspector, FormerlySerializedAs("PositionUnits")] private PathIndexUnit m_LegacyUnits;
[SerializeField, HideInInspector, FormerlySerializedAs("Spline")] private SplineContainer m_LegacySpline;
void PerformLegacyUpgrade()
{
if (m_LegacyPosition != -1)
{
SplineSettings.Position = m_LegacyPosition;
SplineSettings.Units = m_LegacyUnits;
m_LegacyPosition = -1;
m_LegacyUnits = 0;
}
if (m_LegacySpline != null)
{
SplineSettings.Spline = m_LegacySpline;
m_LegacySpline = null;
}
}
// =================================
/// The Spline container to which the camera will be constrained.
public SplineContainer Spline
{
get => SplineSettings.Spline;
set => SplineSettings.Spline = value;
}
/// The position along the spline at which the camera will be placed. This can be animated directly,
/// or set automatically by the Auto-Dolly feature to get as close as possible to the Follow target.
/// The value is interpreted according to the Position Units setting.
public float CameraPosition
{
get => SplineSettings.Position;
set => SplineSettings.Position = value;
}
/// How to interpret the Spline Position:
/// - Distance: Values range from 0 (start of Spline) to Length of the Spline (end of Spline).
/// - Normalized: Values range from 0 (start of Spline) to 1 (end of Spline).
/// - Knot: Values are defined by knot indices and a fractional value representing the normalized
/// interpolation between the specific knot index and the next knot."
public PathIndexUnit PositionUnits
{
get => SplineSettings.Units;
set => SplineSettings.ChangeUnitPreservePosition(value);
}
void OnValidate()
{
PerformLegacyUpgrade(); // only called in-editor
Damping.Position.x = Mathf.Clamp(Damping.Position.x, 0, 20);
Damping.Position.y = Mathf.Clamp(Damping.Position.y, 0, 20);
Damping.Position.z = Mathf.Clamp(Damping.Position.z, 0, 20);
Damping.Angular = Mathf.Clamp(Damping.Angular, 0, 20);
AutomaticDolly.Method?.Validate();
}
void Reset()
{
SplineSettings = new SplineSettings { Units = PathIndexUnit.Normalized };
SplineOffset = Vector3.zero;
CameraRotation = RotationMode.Default;
Damping = default;
AutomaticDolly.Method = null;
}
/// Called when the behaviour is enabled.
protected override void OnEnable()
{
base.OnEnable();
RefreshRollCache();
AutomaticDolly.Method?.Reset();
}
/// True if component is enabled and has a spline
public override bool IsValid => enabled && Spline != null;
/// Get the Cinemachine Pipeline stage that this component implements.
/// Always returns the Body stage
public override CinemachineCore.Stage Stage => CinemachineCore.Stage.Body;
///
/// Report maximum damping time needed for this component.
///
/// Highest damping setting in this component
public override float GetMaxDampTime() => !Damping.Enabled ? 0 :
Mathf.Max(Mathf.Max(Damping.Position.x, Mathf.Max(Damping.Position.y, Damping.Position.z)), Damping.Angular);
/// Positions the virtual camera according to the transposer rules.
/// The current camera state
/// Used for damping. If less that 0, no damping is done.
public override void MutateCameraState(ref CameraState curState, float deltaTime)
{
if (!IsValid)
return;
var splinePath = Spline.Spline;
if (splinePath == null || splinePath.Count == 0)
return;
var pathLength = splinePath.GetLength();
var splinePos = splinePath.StandardizePosition(CameraPosition, PositionUnits, pathLength);
// Init previous frame state info
if (deltaTime < 0 || !VirtualCamera.PreviousStateIsValid)
{
m_PreviousSplinePosition = splinePos;
m_PreviousPosition = curState.RawPosition;
m_PreviousRotation = curState.RawOrientation;
RefreshRollCache();
}
// Invoke AutoDolly algorithm to get new desired spline position
if (AutomaticDolly.Enabled && AutomaticDolly.Method != null)
splinePos = AutomaticDolly.Method.GetSplinePosition(
this, FollowTarget, Spline, splinePos, PositionUnits, deltaTime);
// Apply damping in the spline direction
if (Damping.Enabled && deltaTime >= 0 && VirtualCamera.PreviousStateIsValid)
{
// If spline is closed, we choose shortest path for damping
var max = splinePath.GetMaxPosition(PositionUnits, pathLength);
var prev = m_PreviousSplinePosition;
if (splinePath.Closed && Mathf.Abs(splinePos - prev) > max * 0.5f)
prev += (splinePos > prev) ? max : -max;
// Do the damping
splinePos = prev + Damper.Damp(splinePos - prev, Damping.Position.z, deltaTime);
}
m_PreviousSplinePosition = CameraPosition = splinePos;
Spline.EvaluateSplineWithRoll(
SplineRoll, m_PreviousRotation,
splinePath.ConvertIndexUnit(splinePos, PositionUnits, PathIndexUnit.Normalized),
out var newPos, out var newSplineRotation);
// Apply the offset to get the new camera position
var offsetX = newSplineRotation * Vector3.right;
var offsetY = newSplineRotation * Vector3.up;
var offsetZ = newSplineRotation * Vector3.forward;
newPos += SplineOffset.x * offsetX;
newPos += SplineOffset.y * offsetY;
newPos += SplineOffset.z * offsetZ;
// Apply damping to the remaining directions
if (Damping.Enabled && deltaTime >= 0 && VirtualCamera.PreviousStateIsValid)
{
var currentCameraPos = m_PreviousPosition;
var delta = (currentCameraPos - newPos);
var delta1 = Vector3.Dot(delta, offsetY) * offsetY;
var delta0 = delta - delta1;
delta0 = Damper.Damp(delta0, Damping.Position.x, deltaTime);
delta1 = Damper.Damp(delta1, Damping.Position.y, deltaTime);
newPos = currentCameraPos - (delta0 + delta1);
}
curState.RawPosition = m_PreviousPosition = newPos;
// Set the orientation and up
var newRot = GetCameraRotationAtSplinePoint(newSplineRotation, curState.ReferenceUp);
if (Damping.Enabled && deltaTime >= 0 && VirtualCamera.PreviousStateIsValid)
{
float t = VirtualCamera.DetachedFollowTargetDamp(1, Damping.Angular, deltaTime);
newRot = Quaternion.Slerp(m_PreviousRotation, newRot, t);
}
m_PreviousRotation = newRot;
curState.RawOrientation = newRot;
if (CameraRotation != RotationMode.Default)
curState.ReferenceUp = curState.RawOrientation * Vector3.up;
}
Quaternion GetCameraRotationAtSplinePoint(Quaternion splineOrientation, Vector3 up)
{
switch (CameraRotation)
{
default:
case RotationMode.Default: break;
case RotationMode.Spline: return splineOrientation;
case RotationMode.SplineNoRoll:
return Quaternion.LookRotation(splineOrientation * Vector3.forward, up);
case RotationMode.FollowTarget:
if (FollowTarget != null)
return FollowTargetRotation;
break;
case RotationMode.FollowTargetNoRoll:
if (FollowTarget != null)
return Quaternion.LookRotation(FollowTargetRotation * Vector3.forward, up);
break;
}
return Quaternion.LookRotation(VirtualCamera.transform.rotation * Vector3.forward, up);
}
CinemachineSplineRoll SplineRoll
{
get
{
#if UNITY_EDITOR
if (!Application.isPlaying)
RefreshRollCache();
#endif
return m_RollCache;
}
}
void RefreshRollCache()
{
// check if we have CinemachineSplineRoll
TryGetComponent(out m_RollCache);
#if UNITY_EDITOR
// need to tell CinemachineSplineRoll about its spline for gizmo drawing purposes
if (m_RollCache != null)
m_RollCache.Container = Spline;
#endif
// check if our spline has CinemachineSplineRoll
if (Spline != null && m_RollCache == null)
Spline.TryGetComponent(out m_RollCache);
}
}
}