using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using UnityEngine; using UnityEngine.Timeline; using UnityEngine.Playables; namespace UnityEditor.Timeline { // Handles Undo animated properties on Monobehaviours to create track clips static partial class TimelineRecording { static readonly List s_TempPropertyModifications = new List(6); internal static UndoPropertyModification[] ProcessUndoModification(UndoPropertyModification[] modifications, WindowState state) { if (HasAnyPlayableAssetModifications(modifications)) return ProcessPlayableAssetModification(modifications, state, false); return ProcessMonoBehaviourModification(modifications, state); } static UnityEngine.Object GetTarget(UndoPropertyModification undo) { if (undo.currentValue != null) return undo.currentValue.target; if (undo.previousValue != null) return undo.previousValue.target; return null; } // Gets the appropriate track for a given game object static TrackAsset GetTrackForGameObject(GameObject gameObject, WindowState state) { if (gameObject == null) return null; var director = state.editSequence.director; if (director == null) return null; var level = int.MaxValue; TrackAsset result = null; // search the output tracks var outputTracks = state.editSequence.asset.flattenedTracks; foreach (var track in outputTracks) { if (track.GetType() != typeof(AnimationTrack)) continue; if (!state.IsTrackRecordable(track)) continue; var obj = TimelineUtility.GetSceneGameObject(director, track); if (obj != null) { // checks if the effected gameobject is our child var childLevel = GetChildLevel(obj, gameObject); if (childLevel != -1 && childLevel < level) { result = track; level = childLevel; } } } // the resulting track is not armed. checking here avoids accidentally recording objects with their own // tracks if (result && !state.IsTrackRecordable(result)) { result = null; } return result; } // Gets the track this property would record to. // Returns null if there is a track, but it's not currently active for recording public static TrackAsset GetRecordingTrack(SerializedProperty property, WindowState state) { var serializedObject = property.serializedObject; var component = serializedObject.targetObject as Component; if (component == null) return null; var gameObject = component.gameObject; return GetTrackForGameObject(gameObject, state); } // Given a serialized property, gathers all animatable properties static void GatherModifications(SerializedProperty property, List modifications) { // handles child properties (Vector3 is 3 recordable properties) if (property.hasChildren) { var iter = property.Copy(); var end = property.GetEndProperty(false); // recurse over all children properties while (iter.Next(true) && !SerializedProperty.EqualContents(iter, end)) { GatherModifications(iter, modifications); } } var isObject = property.propertyType == SerializedPropertyType.ObjectReference; var isFloat = property.propertyType == SerializedPropertyType.Float || property.propertyType == SerializedPropertyType.Boolean || property.propertyType == SerializedPropertyType.Integer; if (isObject || isFloat) { var serializedObject = property.serializedObject; var modification = new PropertyModification(); modification.target = serializedObject.targetObject; modification.propertyPath = property.propertyPath; if (isObject) { modification.value = string.Empty; modification.objectReference = property.objectReferenceValue; } else { modification.value = TimelineUtility.PropertyToString(property); } // Path for monobehaviour based - better to grab the component to get the curvebinding to allow validation if (serializedObject.targetObject is Component) { EditorCurveBinding temp; var go = ((Component)serializedObject.targetObject).gameObject; if (AnimationUtility.PropertyModificationToEditorCurveBinding(modification, go, out temp) != null) { modifications.Add(modification); } } else { modifications.Add(modification); } } } public static void AddKey(SerializedProperty prop, WindowState state) { s_TempPropertyModifications.Clear(); GatherModifications(prop, s_TempPropertyModifications); if (s_TempPropertyModifications.Any()) { AddKey(s_TempPropertyModifications, state); } } public static void AddKey(IEnumerable modifications, WindowState state) { var undos = modifications.Select(PropertyModificationToUndoPropertyModification).ToArray(); if (HasAnyPlayableAssetModifications(undos)) ProcessPlayableAssetModification(undos, state, true); ProcessMonoBehaviourModification(undos, state); } static UndoPropertyModification PropertyModificationToUndoPropertyModification(PropertyModification prop) { return new UndoPropertyModification { previousValue = prop, currentValue = new PropertyModification { objectReference = prop.objectReference, propertyPath = prop.propertyPath, target = prop.target, value = prop.value }, keepPrefabOverride = true }; } // Given an animation track, return the clip that we are currently recording to static AnimationClip GetRecordingClip(TrackAsset asset, WindowState state, out double startTime, out double timeScale) { startTime = 0; timeScale = 1; TimelineClip displayBackground = null; asset.FindRecordingClipAtTime(state.editSequence.time, out displayBackground); var animClip = asset.FindRecordingAnimationClipAtTime(state.editSequence.time); if (displayBackground != null) { startTime = displayBackground.start; timeScale = displayBackground.timeScale; } return animClip; } // Helper that finds the animation clip we are recording and the relative time to that clip static bool GetClipAndRelativeTime(UnityEngine.Object target, WindowState state, out AnimationClip outClip, out double keyTime, out bool keyInRange) { const float floatToDoubleError = 0.00001f; outClip = null; keyTime = 0; keyInRange = false; double startTime = 0; double timeScale = 1; AnimationClip clip = null; IPlayableAsset playableAsset = target as IPlayableAsset; Component component = target as Component; // Handle recordable playable assets if (playableAsset != null) { var curvesOwner = AnimatedParameterUtility.ToCurvesOwner(playableAsset, state.editSequence.asset); if (curvesOwner != null) { if (curvesOwner.curves == null) curvesOwner.CreateCurves(curvesOwner.GetUniqueRecordedClipName()); clip = curvesOwner.curves; var timelineClip = curvesOwner as TimelineClip; if (timelineClip != null) { startTime = timelineClip.start; timeScale = timelineClip.timeScale; } } } // Handle recording components, including infinite clip else if (component != null) { var asset = GetTrackForGameObject(component.gameObject, state); if (asset != null) { clip = GetRecordingClip(asset, state, out startTime, out timeScale); } } if (clip == null) return false; keyTime = (state.editSequence.time - startTime) * timeScale; outClip = clip; keyInRange = keyTime >= 0 && keyTime <= (clip.length * timeScale + floatToDoubleError); return true; } public static bool HasCurve(IList modifications, UnityEngine.Object target, WindowState state) { return GetKeyTimes(modifications, state).Any(); } public static bool HasKey(IList modifications, WindowState state) { AnimationClip clip; double keyTime; bool inRange; if (!GetClipAndRelativeTime(modifications[0].target, state, out clip, out keyTime, out inRange)) return false; return GetKeyTimes(modifications, state).Any(t => (CurveEditUtility.KeyCompare((float)state.editSequence.time, (float)t, clip.frameRate) == 0)); } // Checks if a key already exists for this property static bool HasBinding(UnityEngine.Object target, PropertyModification modification, AnimationClip clip, out EditorCurveBinding binding) { var component = target as Component; var playableAsset = target as IPlayableAsset; if (component != null) { var type = AnimationUtility.PropertyModificationToEditorCurveBinding(modification, component.gameObject, out binding); binding = RotationCurveInterpolation.RemapAnimationBindingForRotationCurves(binding, clip); return type != null; } if (playableAsset != null) { binding = EditorCurveBinding.FloatCurve(string.Empty, target.GetType(), AnimatedParameterUtility.GetAnimatedParameterBindingName(target, modification.propertyPath)); } else { binding = new EditorCurveBinding(); return false; } return true; } public static void RemoveKey(UnityEngine.Object target, IEnumerable modifications, WindowState state) { AnimationClip clip; double keyTime; bool inRange; if (!GetClipAndRelativeTime(target, state, out clip, out keyTime, out inRange) || !inRange) return; var refreshPreview = false; TimelineUndo.PushUndo(clip, L10n.Tr("Remove Key")); foreach (var mod in modifications) { EditorCurveBinding temp; if (HasBinding(target, mod, clip, out temp)) { if (temp.isPPtrCurve) { CurveEditUtility.RemoveObjectKey(clip, temp, keyTime); if (CurveEditUtility.GetObjectKeyCount(clip, temp) == 0) { refreshPreview = true; } } else { AnimationCurve curve = AnimationUtility.GetEditorCurve(clip, temp); if (curve != null) { CurveEditUtility.RemoveKeyFrameFromCurve(curve, (float)keyTime, clip.frameRate); AnimationUtility.SetEditorCurve(clip, temp, curve); if (curve.length == 0) { AnimationUtility.SetEditorCurve(clip, temp, null); refreshPreview = true; } } } } } if (refreshPreview) { state.ResetPreviewMode(); } } static HashSet GetKeyTimes(IList modifications, WindowState state) { var keyTimes = new HashSet(); AnimationClip animationClip; double keyTime; bool inRange; var component = modifications[0].target as Component; var target = modifications[0].target; if (component != null) { var track = GetTrackForGameObject(component.gameObject, state); var go = TimelineUtility.GetSceneGameObject(TimelineEditor.inspectedDirector, track); if (go != null) { target = go.transform; } } GetClipAndRelativeTime(target, state, out animationClip, out keyTime, out inRange); if (animationClip == null) return keyTimes; var playableAsset = target as IPlayableAsset; var info = AnimationClipCurveCache.Instance.GetCurveInfo(animationClip); TimelineClip clip = null; if (component != null) { GetTrackForGameObject(component.gameObject, state).FindRecordingClipAtTime(state.editSequence.time, out clip); } else if (playableAsset != null) { clip = FindClipWithAsset(state.editSequence.asset, playableAsset); } foreach (var mod in modifications) { EditorCurveBinding temp; if (HasBinding(target, mod, animationClip, out temp)) { IEnumerable keys = new HashSet(); if (temp.isPPtrCurve) { var curve = info.GetObjectCurveForBinding(temp); if (curve != null) { keys = curve.Select(x => (double)x.time); } } else { var curve = info.GetCurveForBinding(temp); if (curve != null) { keys = curve.keys.Select(x => (double)x.time); } } // Transform the times in to 'global' space using the clip if (clip != null) { foreach (var k in keys) { var time = clip.FromLocalTimeUnbound(k); const double eps = 1e-5; if (time >= clip.start - eps && time <= clip.end + eps) { keyTimes.Add(time); } } } // infinite clip mode, global == local space else { keyTimes.UnionWith(keys); } } } return keyTimes; } public static void NextKey(UnityEngine.Object target, IList modifications, WindowState state) { const double eps = 1e-5; var keyTimes = GetKeyTimes(modifications, state); if (keyTimes.Count == 0) return; var nextKeys = keyTimes.Where(x => x > state.editSequence.time + eps); if (nextKeys.Any()) { state.editSequence.time = nextKeys.Min(); } } public static void PrevKey(UnityEngine.Object target, IList modifications, WindowState state) { const double eps = 1e-5; var keyTimes = GetKeyTimes(modifications, state); if (keyTimes.Count == 0) return; var prevKeys = keyTimes.Where(x => x < state.editSequence.time - eps); if (prevKeys.Any()) { state.editSequence.time = prevKeys.Max(); } } public static void RemoveCurve(UnityEngine.Object target, IEnumerable modifications, WindowState state) { AnimationClip clip = null; double keyTime = 0; var inRange = false; // not used for curves if (!GetClipAndRelativeTime(target, state, out clip, out keyTime, out inRange)) return; TimelineUndo.PushUndo(clip, L10n.Tr("Remove Curve")); foreach (var mod in modifications) { EditorCurveBinding temp; if (HasBinding(target, mod, clip, out temp)) { if (temp.isPPtrCurve) AnimationUtility.SetObjectReferenceCurve(clip, temp, null); else AnimationUtility.SetEditorCurve(clip, temp, null); } } state.ResetPreviewMode(); } public static void KeyAllProperties(Component target, WindowState state) { var go = target is Component component ? component.gameObject : null; GetClipAndRelativeTime(target, state, out var animationClip, out _, out _); var info = AnimationClipCurveCache.Instance.GetCurveInfo(animationClip); if (animationClip != null && info.curves.Length > 0) { KeyProperties(go, state, info.bindings); } } public static void KeyProperties(GameObject go, WindowState state, IList bindings) { var allKeyedProperties = new List(); var rotationPaths = new HashSet(); for (var i = 0; i < bindings.Count; ++i) { // Skip the euler and key quaternion+hint if (CurveEditUtility.IsRotationKey(bindings[i])) { rotationPaths.Add(bindings[i].path); continue; } AnimationUtility.GetFloatValue(go, bindings[i], out var val); var compo = GetTargetFromEditorBinding(go, bindings[i]); allKeyedProperties.Add(new PropertyModification { target = compo, value = val.ToString(EditorGUI.kFloatFieldFormatString), propertyPath = bindings[i].propertyName }); } foreach (var path in rotationPaths) { foreach (var binding in GetRotationBindings(path)) { var compo = GetTargetFromEditorBinding(go, binding); var readBinding = binding; switch (binding.propertyName) { case kLocalEulerHint + ".x": readBinding = EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalRotation + ".x"); break; case kLocalEulerHint + ".y": readBinding = EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalRotation + ".y"); break; case kLocalEulerHint + ".z": readBinding = EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalRotation + ".z"); break; } AnimationUtility.GetFloatValue(go, readBinding, out var val); allKeyedProperties.Add(new PropertyModification { target = compo, value = val.ToString(EditorGUI.kFloatFieldFormatString), propertyPath = binding.propertyName }); } } AddKey(allKeyedProperties, state); state.Refresh(); } static IEnumerable GetRotationBindings(string path) { return new[] { EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalRotation + ".x"), EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalRotation + ".y"), EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalRotation + ".z"), EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalRotation + ".w"), EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalEulerHint + ".x"), EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalEulerHint + ".y"), EditorCurveBinding.FloatCurve(path, typeof(Transform), kLocalEulerHint + ".z"), }; } static Component GetTargetFromEditorBinding(GameObject root, EditorCurveBinding binding) { GameObject go = null; if (string.IsNullOrEmpty(binding.path)) { go = root; } var childTransform = root.transform.Find(binding.path); if (childTransform != null) { go = childTransform.gameObject; } if (go != null) { return go.GetComponent(binding.type); } return null; } public static IEnumerable GetRecordableGameObjects(WindowState state) { if (state == null || state.editSequence.asset == null || state.editSequence.director == null) yield break; var outputTracks = state.editSequence.asset.GetOutputTracks(); foreach (var track in outputTracks) { if (track.GetType() != typeof(AnimationTrack)) continue; if (!state.IsTrackRecordable(track) && !track.GetChildTracks().Any(state.IsTrackRecordable)) continue; var obj = TimelineUtility.GetSceneGameObject(state.editSequence.director, track); if (obj != null) { yield return obj; } } } } }