using System;
using System.Collections.Generic;
using UnityEngine.Playables;
using UnityEngine.Serialization;
namespace UnityEngine.Timeline
{
///
/// Implement this interface to support advanced features of timeline clips.
///
public interface ITimelineClipAsset
{
///
/// Returns a description of the features supported by clips with PlayableAssets implementing this interface.
///
ClipCaps clipCaps { get; }
}
///
/// Represents a clip on the timeline.
///
[Serializable]
public partial class TimelineClip : ICurvesOwner, ISerializationCallbackReceiver
{
///
/// The default capabilities for a clip
///
public static readonly ClipCaps kDefaultClipCaps = ClipCaps.Blending;
///
/// The default length of a clip in seconds.
///
public static readonly float kDefaultClipDurationInSeconds = 5;
///
/// The minimum timescale allowed on a clip
///
public static readonly double kTimeScaleMin = 1.0 / 1000;
///
/// The maximum timescale allowed on a clip
///
public static readonly double kTimeScaleMax = 1000;
internal static readonly string kDefaultCurvesName = "Clip Parameters";
internal static readonly double kMinDuration = 1 / 60.0;
// constant representing the longest possible sequence duration
internal static readonly double kMaxTimeValue = 1000000; // more than a week's time, and within numerical precision boundaries
///
/// How the clip handles time outside its start and end range.
///
public enum ClipExtrapolation
{
///
/// No extrapolation is applied.
///
None,
///
/// Hold the time at the end value of the clip.
///
Hold,
///
/// Repeat time values outside the start/end range.
///
Loop,
///
/// Repeat time values outside the start/end range, reversing direction at each loop
///
PingPong,
///
/// Time values are passed in without modification, extending beyond the clips range
///
Continue
};
///
/// How blend curves are treated in an overlap
///
public enum BlendCurveMode
{
///
/// The curve is normalized against the opposing clip
///
Auto,
///
/// The blend curve is fixed.
///
Manual
};
internal TimelineClip(TrackAsset parent)
{
// parent clip into track
SetParentTrack_Internal(parent);
}
[SerializeField] double m_Start;
[SerializeField] double m_ClipIn;
[SerializeField] Object m_Asset;
[SerializeField][FormerlySerializedAs("m_HackDuration")] double m_Duration;
[SerializeField] double m_TimeScale = 1.0;
[SerializeField] TrackAsset m_ParentTrack;
// for mixing out scripts - default is no mix out (i.e. flat)
[SerializeField] double m_EaseInDuration;
[SerializeField] double m_EaseOutDuration;
// the blend durations override ease in / out durations
[SerializeField] double m_BlendInDuration = -1.0f;
[SerializeField] double m_BlendOutDuration = -1.0f;
// doubles as ease in/out and blend in/out curves
[SerializeField] AnimationCurve m_MixInCurve;
[SerializeField] AnimationCurve m_MixOutCurve;
[SerializeField] BlendCurveMode m_BlendInCurveMode = BlendCurveMode.Auto;
[SerializeField] BlendCurveMode m_BlendOutCurveMode = BlendCurveMode.Auto;
[SerializeField] List m_ExposedParameterNames;
[SerializeField] AnimationClip m_AnimationCurves;
[SerializeField] bool m_Recordable;
// extrapolation
[SerializeField] ClipExtrapolation m_PostExtrapolationMode;
[SerializeField] ClipExtrapolation m_PreExtrapolationMode;
[SerializeField] double m_PostExtrapolationTime;
[SerializeField] double m_PreExtrapolationTime;
[SerializeField] string m_DisplayName;
///
/// Is the clip being extrapolated before its start time?
///
public bool hasPreExtrapolation
{
get { return m_PreExtrapolationMode != ClipExtrapolation.None && m_PreExtrapolationTime > 0; }
}
///
/// Is the clip being extrapolated past its end time?
///
public bool hasPostExtrapolation
{
get { return m_PostExtrapolationMode != ClipExtrapolation.None && m_PostExtrapolationTime > 0; }
}
///
/// A speed multiplier for the clip;
///
public double timeScale
{
get { return clipCaps.HasAny(ClipCaps.SpeedMultiplier) ? Math.Max(kTimeScaleMin, Math.Min(m_TimeScale, kTimeScaleMax)) : 1.0; }
set
{
UpdateDirty(m_TimeScale, value);
m_TimeScale = clipCaps.HasAny(ClipCaps.SpeedMultiplier) ? Math.Max(kTimeScaleMin, Math.Min(value, kTimeScaleMax)) : 1.0;
}
}
///
/// The start time, in seconds, of the clip
///
public double start
{
get { return m_Start; }
set
{
UpdateDirty(value, m_Start);
var newValue = Math.Max(SanitizeTimeValue(value, m_Start), 0);
if (m_ParentTrack != null && m_Start != newValue)
{
m_ParentTrack.OnClipMove();
}
m_Start = newValue;
}
}
///
/// The length, in seconds, of the clip
///
public double duration
{
get { return m_Duration; }
set
{
UpdateDirty(m_Duration, value);
m_Duration = Math.Max(SanitizeTimeValue(value, m_Duration), double.Epsilon);
}
}
///
/// The end time, in seconds of the clip
///
public double end
{
get { return m_Start + m_Duration; }
}
///
/// Local offset time of the clip.
///
public double clipIn
{
get { return clipCaps.HasAny(ClipCaps.ClipIn) ? m_ClipIn : 0; }
set
{
UpdateDirty(m_ClipIn, value);
m_ClipIn = clipCaps.HasAny(ClipCaps.ClipIn) ? Math.Max(Math.Min(SanitizeTimeValue(value, m_ClipIn), kMaxTimeValue), 0.0) : 0;
}
}
///
/// The name displayed on the clip
///
public string displayName
{
get { return m_DisplayName; }
set { m_DisplayName = value; }
}
///
/// The length, in seconds, of the PlayableAsset attached to the clip.
///
public double clipAssetDuration
{
get
{
var playableAsset = m_Asset as IPlayableAsset;
return playableAsset != null ? playableAsset.duration : double.MaxValue;
}
}
///
/// An animation clip containing animated properties of the attached PlayableAsset
///
///
/// This is where animated clip properties are stored.
///
public AnimationClip curves
{
get { return m_AnimationCurves; }
internal set { m_AnimationCurves = value; }
}
string ICurvesOwner.defaultCurvesName
{
get { return kDefaultCurvesName; }
}
///
/// Whether this clip contains animated properties for the attached PlayableAsset.
///
///
/// This property is false if the curves property is null or if it contains no information.
///
public bool hasCurves
{
get { return m_AnimationCurves != null && !m_AnimationCurves.empty; }
}
///
/// The PlayableAsset attached to the clip.
///
public Object asset
{
get { return m_Asset; }
set { m_Asset = value; }
}
Object ICurvesOwner.assetOwner
{
get { return GetParentTrack(); }
}
TrackAsset ICurvesOwner.targetTrack
{
get { return GetParentTrack(); }
}
///
/// underlyingAsset property is obsolete. Use asset property instead
///
[Obsolete("underlyingAsset property is obsolete. Use asset property instead", true)]
public Object underlyingAsset
{
get { return null; }
set {}
}
///
/// Returns the TrackAsset to which this clip is attached.
///
[Obsolete("parentTrack is deprecated and will be removed in a future release. Use " + nameof(GetParentTrack) + "() and " + nameof(TimelineClipExtensions) + "::" + nameof(TimelineClipExtensions.MoveToTrack) + "() or " + nameof(TimelineClipExtensions) + "::" + nameof(TimelineClipExtensions.TryMoveToTrack) + "() instead.", false)]
public TrackAsset parentTrack
{
get { return m_ParentTrack; }
set { SetParentTrack_Internal(value);}
}
///
/// Get the TrackAsset to which this clip is attached.
///
/// the parent TrackAsset
public TrackAsset GetParentTrack()
{
return m_ParentTrack;
}
///
/// Sets the parent track without performing any validation. To ensure a valid change use TimelineClipExtensions.TrySetParentTrack(TrackAsset) instead.
///
///
internal void SetParentTrack_Internal(TrackAsset newParentTrack)
{
if (m_ParentTrack == newParentTrack)
return;
if (m_ParentTrack != null)
m_ParentTrack.RemoveClip(this);
m_ParentTrack = newParentTrack;
if (m_ParentTrack != null)
m_ParentTrack.AddClip(this);
}
///
/// The ease in duration of the timeline clip in seconds. This only applies if the start of the clip is not overlapping.
///
public double easeInDuration
{
get
{
var availableDuration = hasBlendOut ? duration - m_BlendOutDuration : duration;
return clipCaps.HasAny(ClipCaps.Blending) ? Math.Min(Math.Max(m_EaseInDuration, 0), availableDuration) : 0;
}
set
{
var availableDuration = hasBlendOut ? duration - m_BlendOutDuration : duration;
m_EaseInDuration = clipCaps.HasAny(ClipCaps.Blending) ? Math.Max(0, Math.Min(SanitizeTimeValue(value, m_EaseInDuration), availableDuration)) : 0;
}
}
///
/// The ease out duration of the timeline clip in seconds. This only applies if the end of the clip is not overlapping.
///
public double easeOutDuration
{
get
{
var availableDuration = hasBlendIn ? duration - m_BlendInDuration : duration;
return clipCaps.HasAny(ClipCaps.Blending) ? Math.Min(Math.Max(m_EaseOutDuration, 0), availableDuration) : 0;
}
set
{
var availableDuration = hasBlendIn ? duration - m_BlendInDuration : duration;
m_EaseOutDuration = clipCaps.HasAny(ClipCaps.Blending) ? Math.Max(0, Math.Min(SanitizeTimeValue(value, m_EaseOutDuration), availableDuration)) : 0;
}
}
///
/// eastOutTime property is obsolete use easeOutTime property instead
///
[Obsolete("Use easeOutTime instead (UnityUpgradable) -> easeOutTime", true)]
public double eastOutTime
{
get { return duration - easeOutDuration + m_Start; }
}
///
/// The time in seconds that the ease out begins
///
public double easeOutTime
{
get { return duration - easeOutDuration + m_Start; }
}
///
/// The amount of overlap in seconds on the start of a clip.
///
public double blendInDuration
{
get { return clipCaps.HasAny(ClipCaps.Blending) ? m_BlendInDuration : 0; }
set { m_BlendInDuration = clipCaps.HasAny(ClipCaps.Blending) ? SanitizeTimeValue(value, m_BlendInDuration) : 0; }
}
///
/// The amount of overlap in seconds at the end of a clip.
///
public double blendOutDuration
{
get { return clipCaps.HasAny(ClipCaps.Blending) ? m_BlendOutDuration : 0; }
set { m_BlendOutDuration = clipCaps.HasAny(ClipCaps.Blending) ? SanitizeTimeValue(value, m_BlendOutDuration) : 0; }
}
///
/// The mode for calculating the blend curve of the overlap at the start of the clip
///
public BlendCurveMode blendInCurveMode
{
get { return m_BlendInCurveMode; }
set { m_BlendInCurveMode = value; }
}
///
/// The mode for calculating the blend curve of the overlap at the end of the clip
///
public BlendCurveMode blendOutCurveMode
{
get { return m_BlendOutCurveMode; }
set { m_BlendOutCurveMode = value; }
}
///
/// Returns whether the clip is blending in
///
public bool hasBlendIn { get { return clipCaps.HasAny(ClipCaps.Blending) && m_BlendInDuration > 0; } }
///
/// Returns whether the clip is blending out
///
public bool hasBlendOut { get { return clipCaps.HasAny(ClipCaps.Blending) && m_BlendOutDuration > 0; } }
///
/// The animation curve used for calculating weights during an ease in or a blend in.
///
public AnimationCurve mixInCurve
{
get
{
// auto fix broken curves
if (m_MixInCurve == null || m_MixInCurve.length < 2)
m_MixInCurve = GetDefaultMixInCurve();
return m_MixInCurve;
}
set { m_MixInCurve = value; }
}
///
/// The amount of the clip being used for ease or blend in as a percentage
///
public float mixInPercentage
{
get { return (float)(mixInDuration / duration); }
}
///
/// The amount of the clip blending or easing in, in seconds
///
public double mixInDuration
{
get { return hasBlendIn ? blendInDuration : easeInDuration; }
}
///
/// The animation curve used for calculating weights during an ease out or a blend out.
///
public AnimationCurve mixOutCurve
{
get
{
if (m_MixOutCurve == null || m_MixOutCurve.length < 2)
m_MixOutCurve = GetDefaultMixOutCurve();
return m_MixOutCurve;
}
set { m_MixOutCurve = value; }
}
///
/// The time in seconds that an ease out or blend out starts
///
public double mixOutTime
{
get { return duration - mixOutDuration + m_Start; }
}
///
/// The amount of the clip blending or easing out, in seconds
///
public double mixOutDuration
{
get { return hasBlendOut ? blendOutDuration : easeOutDuration; }
}
///
/// The amount of the clip being used for ease or blend out as a percentage
///
public float mixOutPercentage
{
get { return (float)(mixOutDuration / duration); }
}
///
/// Returns whether this clip is recordable in editor
///
public bool recordable
{
get { return m_Recordable; }
internal set { m_Recordable = value; }
}
///
/// exposedParameter is deprecated and will be removed in a future release
///
[Obsolete("exposedParameter is deprecated and will be removed in a future release", true)]
public List exposedParameters
{
get { return m_ExposedParameterNames ?? (m_ExposedParameterNames = new List()); }
}
///
/// Returns the capabilities supported by this clip.
///
public ClipCaps clipCaps
{
get
{
var clipAsset = asset as ITimelineClipAsset;
return (clipAsset != null) ? clipAsset.clipCaps : kDefaultClipCaps;
}
}
internal int Hash()
{
return HashUtility.CombineHash(m_Start.GetHashCode(),
m_Duration.GetHashCode(),
m_TimeScale.GetHashCode(),
m_ClipIn.GetHashCode(),
((int)m_PreExtrapolationMode).GetHashCode(),
((int)m_PostExtrapolationMode).GetHashCode());
}
///
/// Given a time, returns the weight from the mix out
///
/// Time (relative to the timeline)
///
public float EvaluateMixOut(double time)
{
if (!clipCaps.HasAny(ClipCaps.Blending))
return 1.0f;
if (mixOutDuration > Mathf.Epsilon)
{
var perc = (float)(time - mixOutTime) / (float)mixOutDuration;
perc = Mathf.Clamp01(mixOutCurve.Evaluate(perc));
return perc;
}
return 1.0f;
}
///
/// Given a time, returns the weight from the mix in
///
/// Time (relative to the timeline)
///
public float EvaluateMixIn(double time)
{
if (!clipCaps.HasAny(ClipCaps.Blending))
return 1.0f;
if (mixInDuration > Mathf.Epsilon)
{
var perc = (float)(time - m_Start) / (float)mixInDuration;
perc = Mathf.Clamp01(mixInCurve.Evaluate(perc));
return perc;
}
return 1.0f;
}
static AnimationCurve GetDefaultMixInCurve()
{
return AnimationCurve.EaseInOut(0, 0, 1, 1);
}
static AnimationCurve GetDefaultMixOutCurve()
{
return AnimationCurve.EaseInOut(0, 1, 1, 0);
}
///
/// Converts from global time to a clips local time.
///
/// time relative to the timeline
///
/// The local time with extrapolation applied
///
public double ToLocalTime(double time)
{
if (time < 0)
return time;
// handle Extrapolation
if (IsPreExtrapolatedTime(time))
time = GetExtrapolatedTime(time - m_Start, m_PreExtrapolationMode, m_Duration);
else if (IsPostExtrapolatedTime(time))
time = GetExtrapolatedTime(time - m_Start, m_PostExtrapolationMode, m_Duration);
else
time -= m_Start;
// handle looping and time scale within the clip
time *= timeScale;
time += clipIn;
return time;
}
///
/// Converts from global time to local time of the clip
///
/// The time relative to the timeline
/// The local time, ignoring any extrapolation or bounds
public double ToLocalTimeUnbound(double time)
{
return (time - m_Start) * timeScale + clipIn;
}
///
/// Converts from local time of the clip to global time
///
/// Time relative to the clip
/// The time relative to the timeline
internal double FromLocalTimeUnbound(double time)
{
return (time - clipIn) / timeScale + m_Start;
}
///
/// If this contains an animation asset, returns the animation clip attached. Otherwise returns null.
///
public AnimationClip animationClip
{
get
{
if (m_Asset == null)
return null;
var playableAsset = m_Asset as AnimationPlayableAsset;
return playableAsset != null ? playableAsset.clip : null;
}
}
static double SanitizeTimeValue(double value, double defaultValue)
{
if (double.IsInfinity(value) || double.IsNaN(value))
{
Debug.LogError("Invalid time value assigned");
return defaultValue;
}
return Math.Max(-kMaxTimeValue, Math.Min(kMaxTimeValue, value));
}
///
/// Returns whether the clip is being extrapolated past the end time.
///
public ClipExtrapolation postExtrapolationMode
{
get { return clipCaps.HasAny(ClipCaps.Extrapolation) ? m_PostExtrapolationMode : ClipExtrapolation.None; }
internal set { m_PostExtrapolationMode = clipCaps.HasAny(ClipCaps.Extrapolation) ? value : ClipExtrapolation.None; }
}
///
/// Returns whether the clip is being extrapolated before the start time.
///
public ClipExtrapolation preExtrapolationMode
{
get { return clipCaps.HasAny(ClipCaps.Extrapolation) ? m_PreExtrapolationMode : ClipExtrapolation.None; }
internal set { m_PreExtrapolationMode = clipCaps.HasAny(ClipCaps.Extrapolation) ? value : ClipExtrapolation.None; }
}
internal void SetPostExtrapolationTime(double time)
{
m_PostExtrapolationTime = time;
}
internal void SetPreExtrapolationTime(double time)
{
m_PreExtrapolationTime = time;
}
///
/// Given a time, returns whether it falls within the clip's extrapolation
///
/// The time relative to the timeline
/// True if is within the clip extrapolation
public bool IsExtrapolatedTime(double sequenceTime)
{
return IsPreExtrapolatedTime(sequenceTime) || IsPostExtrapolatedTime(sequenceTime);
}
///
/// Given a time, returns whether it falls within the clip's pre-extrapolation
///
/// The time relative to the timeline
/// True if is within the clip pre-extrapolation
public bool IsPreExtrapolatedTime(double sequenceTime)
{
return preExtrapolationMode != ClipExtrapolation.None &&
sequenceTime < m_Start && sequenceTime >= m_Start - m_PreExtrapolationTime;
}
///
/// Given a time, returns whether it falls within the clip's post-extrapolation
///
/// The time relative to the timeline
/// True if is within the clip post-extrapolation
public bool IsPostExtrapolatedTime(double sequenceTime)
{
return postExtrapolationMode != ClipExtrapolation.None &&
(sequenceTime > end) && (sequenceTime - end < m_PostExtrapolationTime);
}
///
/// The start time of the clip, accounting for pre-extrapolation
///
public double extrapolatedStart
{
get
{
if (m_PreExtrapolationMode != ClipExtrapolation.None)
return m_Start - m_PreExtrapolationTime;
return m_Start;
}
}
///
/// The length of the clip in seconds, including extrapolation.
///
public double extrapolatedDuration
{
get
{
double length = m_Duration;
if (m_PostExtrapolationMode != ClipExtrapolation.None)
length += Math.Min(m_PostExtrapolationTime, kMaxTimeValue);
if (m_PreExtrapolationMode != ClipExtrapolation.None)
length += m_PreExtrapolationTime;
return length;
}
}
static double GetExtrapolatedTime(double time, ClipExtrapolation mode, double duration)
{
if (duration == 0)
return 0;
switch (mode)
{
case ClipExtrapolation.None:
break;
case ClipExtrapolation.Loop:
if (time < 0)
time = duration - (-time % duration);
else if (time > duration)
time %= duration;
break;
case ClipExtrapolation.Hold:
if (time < 0)
return 0;
if (time > duration)
return duration;
break;
case ClipExtrapolation.PingPong:
if (time < 0)
{
time = duration * 2 - (-time % (duration * 2));
time = duration - Math.Abs(time - duration);
}
else
{
time = time % (duration * 2.0);
time = duration - Math.Abs(time - duration);
}
break;
case ClipExtrapolation.Continue:
break;
}
return time;
}
///
/// Creates an AnimationClip to store animated properties for the attached PlayableAsset.
///
///
/// If curves already exists for this clip, this method produces no result regardless of the
/// value specified for curvesClipName.
///
///
/// When used from the editor, this method attempts to save the created curves clip to the TimelineAsset.
/// The TimelineAsset must already exist in the AssetDatabase to save the curves clip. If the TimelineAsset
/// does not exist, the curves clip is still created but it is not saved.
///
///
/// The name of the AnimationClip to create.
/// This method does not ensure unique names. If you want a unique clip name, you must provide one.
/// See ObjectNames.GetUniqueName for information on a method that creates unique names.
///
public void CreateCurves(string curvesClipName)
{
if (m_AnimationCurves != null)
return;
m_AnimationCurves = TimelineCreateUtilities.CreateAnimationClipForTrack(string.IsNullOrEmpty(curvesClipName) ? kDefaultCurvesName : curvesClipName, GetParentTrack(), true);
}
///
/// Called before Unity serializes this object.
///
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
m_Version = k_LatestVersion;
}
///
/// Called after Unity deserializes this object.
///
void ISerializationCallbackReceiver.OnAfterDeserialize()
{
if (m_Version < k_LatestVersion)
{
UpgradeToLatestVersion();
}
}
///
/// Outputs a more readable representation of the timeline clip as a string
///
///
public override string ToString()
{
return UnityString.Format("{0} ({1:F2}, {2:F2}):{3:F2} | {4}", displayName, start, end, clipIn, GetParentTrack());
}
///
/// Use this method to adjust ease in and ease out values to avoid overlapping.
///
///
/// Ease values will be adjusted to respect the ratio between ease in and ease out.
///
public void ConformEaseValues()
{
if (m_EaseInDuration + m_EaseOutDuration > duration)
{
var ratio = CalculateEasingRatio(m_EaseInDuration, m_EaseOutDuration);
m_EaseInDuration = duration * ratio;
m_EaseOutDuration = duration * (1.0 - ratio);
}
}
static double CalculateEasingRatio(double easeIn, double easeOut)
{
if (Math.Abs(easeIn - easeOut) < TimeUtility.kTimeEpsilon)
return 0.5;
if (easeIn == 0.0)
return 0.0;
if (easeOut == 0.0)
return 1.0;
return easeIn / (easeIn + easeOut);
}
#if UNITY_EDITOR
internal int DirtyIndex { get; private set; }
internal void MarkDirty()
{
DirtyIndex++;
}
void UpdateDirty(double oldValue, double newValue)
{
if (oldValue != newValue)
DirtyIndex++;
}
#else
void UpdateDirty(double oldValue, double newValue) {}
#endif
};
}