using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.Playables; using UnityEngine.Timeline; namespace UnityEditor.Timeline { class SequenceHierarchy : ScriptableObject { readonly List m_Sequences = new List(); WindowState m_WindowState; [SerializeField] SequencePath m_SerializedPath; public ISequenceState masterSequence { get { return m_Sequences.FirstOrDefault(); } } public ISequenceState editSequence { get { return m_Sequences.LastOrDefault(); } } public int count { get { return m_Sequences.Count; } } public IEnumerable allSequences { get { return m_Sequences; } } public static SequenceHierarchy CreateInstance() { var hierarchy = ScriptableObject.CreateInstance(); hierarchy.hideFlags = HideFlags.HideAndDontSave; return hierarchy; } public void Init(WindowState owner) { m_WindowState = owner; } // This is called when performing Undo operations. // It needs to be called here since some operations are not // allowed (EditorUtility.InstanceIDToObject, for example) // during the ISerializationCallbackReceiver methods. void OnValidate() { if (m_SerializedPath == null || m_WindowState == null || m_WindowState.GetWindow() == null) return; bool hasDifferentRoot = m_WindowState.GetCurrentSequencePath().selectionRoot != m_SerializedPath.selectionRoot; if (m_WindowState.GetWindow().locked && hasDifferentRoot) return; EditorApplication.delayCall += () => { m_WindowState.SetCurrentSequencePath(m_SerializedPath, true); }; } public void Add(TimelineAsset asset, PlayableDirector director, TimelineClip hostClip) { if (hostClip == null) AddToCurrentUndoGroup(this); // Merge with selection undo else TimelineUndo.PushUndo(this, L10n.Tr("Edit Sub-Timeline")); Add_Internal(asset, director, hostClip); UpdateSerializedPath(); } public void Remove() { if (m_Sequences.Count == 0) return; TimelineUndo.PushUndo(this, L10n.Tr("Go to Sub-Timeline")); Remove_Internal(); UpdateSerializedPath(); } public ISequenceState GetStateAtIndex(int index) { return m_Sequences[index]; } public void RemoveUntilCount(int expectedCount) { if (expectedCount < 0 || m_Sequences.Count <= expectedCount) return; TimelineUndo.PushUndo(this, L10n.Tr("Go to Sub-Timeline")); RemoveUntilCount_Internal(expectedCount); UpdateSerializedPath(); } public void Clear() { if (m_Sequences.Count == 0) return; AddToCurrentUndoGroup(this); Clear_Internal(); UpdateSerializedPath(); } public SequencePath ToSequencePath() { var path = new SequencePath(); if (m_Sequences.Count == 0) return path; var rootSequence = m_Sequences[0]; var root = 0; if (rootSequence.director != null && rootSequence.director.gameObject != null) root = rootSequence.director.gameObject.GetInstanceID(); else if (rootSequence.asset != null) root = rootSequence.asset.GetInstanceID(); path.SetSelectionRoot(root); var resolver = rootSequence.director; if (m_Sequences.Count > 1) { for (int i = 1, n = m_Sequences.Count; i < n; ++i) { path.AddSubSequence(m_Sequences[i], resolver); resolver = m_Sequences[i].director; } } return path; } public bool NeedsUpdate(SequencePath path, bool forceRebuild) { return forceRebuild || !SequencePath.AreEqual(m_SerializedPath, path); } public void FromSequencePath(SequencePath path, bool forceRebuild) { if (!NeedsUpdate(path, forceRebuild)) return; Clear_Internal(); var rootObject = EditorUtility.InstanceIDToObject(path.selectionRoot); if (rootObject == null) { UpdateSerializedPath(); return; } var candidateAsset = rootObject as TimelineAsset; if (candidateAsset != null) { Add_Internal(candidateAsset, null, null); UpdateSerializedPath(); return; } var candidateGameObject = rootObject as GameObject; if (candidateGameObject == null) { UpdateSerializedPath(); return; } var director = TimelineUtility.GetDirectorComponentForGameObject(candidateGameObject); var asset = TimelineUtility.GetTimelineAssetForDirectorComponent(director); Add_Internal(asset, director, null); if (!path.subElements.Any()) { UpdateSerializedPath(); return; } List buildingBlocks; if (ValidateSubElements(path.subElements, director, out buildingBlocks)) { foreach (var buildingBlock in buildingBlocks) Add_Internal(buildingBlock.asset, buildingBlock.director, buildingBlock.hostClip); } UpdateSerializedPath(); } void Add_Internal(TimelineAsset asset, PlayableDirector director, TimelineClip hostClip) { if (hostClip == null) Clear_Internal(); var parent = m_Sequences.Count > 0 ? editSequence : null; m_Sequences.Add(new SequenceState(m_WindowState, asset, director, hostClip, (SequenceState)parent)); } void Remove_Internal() { m_Sequences.Last().Dispose(); m_Sequences.RemoveAt(m_Sequences.Count - 1); } void RemoveUntilCount_Internal(int expectedCount) { while (m_Sequences.Count > expectedCount) { Remove_Internal(); } } void Clear_Internal() { RemoveUntilCount_Internal(0); } void UpdateSerializedPath() { m_SerializedPath = ToSequencePath(); } static bool ValidateSubElements(List subElements, PlayableDirector director, out List buildingBlocks) { buildingBlocks = new List(subElements.Count); var currentDirector = director; foreach (var element in subElements) { var timeline = currentDirector.playableAsset as TimelineAsset; if (timeline == null) return false; if (timeline.trackObjects == null) return false; var track = timeline.GetOutputTracks().FirstOrDefault(t => t.GetInstanceID() == element.trackInstanceID); if (track == null) return false; if (track.Hash() != element.trackHash) return false; if (track.clips == null) return false; if (track.clips.Length <= element.clipIndex) return false; var clip = track.clips[element.clipIndex]; if (clip == null) return false; if (clip.Hash() != element.clipHash) return false; var candidateDirectors = TimelineUtility.GetSubTimelines(clip, director); if (element.subDirectorIndex < 0 || element.subDirectorIndex >= candidateDirectors.Count) return false; var candidateDirector = candidateDirectors[element.subDirectorIndex]; if (candidateDirector == null || !(candidateDirector.playableAsset is TimelineAsset)) return false; currentDirector = candidateDirector; buildingBlocks.Add( new SequenceBuildingBlock { asset = currentDirector.playableAsset as TimelineAsset, director = currentDirector, hostClip = clip }); } return true; } struct SequenceBuildingBlock { public TimelineAsset asset; public PlayableDirector director; public TimelineClip hostClip; } static void AddToCurrentUndoGroup(Object target) { if (target == null) return; var group = Undo.GetCurrentGroup(); var groupName = Undo.GetCurrentGroupName(); EditorUtility.SetDirty(target); Undo.RegisterCompleteObjectUndo(target, groupName); Undo.CollapseUndoOperations(group); } } }