using System; using System.Collections.Generic; using System.Linq; using UnityEngine.Playables; namespace UnityEngine.Timeline { /// /// Playable Asset that generates playables for controlling time-related elements on a GameObject. /// [Serializable] [NotKeyable] public class ControlPlayableAsset : PlayableAsset, IPropertyPreview, ITimelineClipAsset { const int k_MaxRandInt = 10000; static readonly List k_EmptyDirectorsList = new List(0); static readonly List k_EmptyParticlesList = new List(0); static readonly HashSet s_SubEmitterCollector = new HashSet(); /// /// GameObject in the scene to control, or the parent of the instantiated prefab. /// [SerializeField] public ExposedReference sourceGameObject; /// /// Prefab object that will be instantiated. /// [SerializeField] public GameObject prefabGameObject; /// /// Indicates whether Particle Systems will be controlled. /// [SerializeField] public bool updateParticle = true; /// /// Random seed to supply particle systems that are set to use autoRandomSeed /// /// /// This is used to maintain determinism when playing back in timeline. Sub emitters will be assigned incrementing random seeds to maintain determinism and distinction. /// [SerializeField] public uint particleRandomSeed; /// /// Indicates whether playableDirectors are controlled. /// [SerializeField] public bool updateDirector = true; /// /// Indicates whether Monobehaviours implementing ITimeControl will be controlled. /// [SerializeField] public bool updateITimeControl = true; /// /// Indicates whether to search the entire hierarchy for controllable components. /// [SerializeField] public bool searchHierarchy = false; /// /// Indicate whether GameObject activation is controlled /// [SerializeField] public bool active = true; /// /// Indicates the active state of the GameObject when Timeline is stopped. /// [SerializeField] public ActivationControlPlayable.PostPlaybackState postPlayback = ActivationControlPlayable.PostPlaybackState.Revert; PlayableAsset m_ControlDirectorAsset; double m_Duration = PlayableBinding.DefaultDuration; bool m_SupportLoop; private static HashSet s_ProcessedDirectors = new HashSet(); private static HashSet s_CreatedPrefabs = new HashSet(); // does the last instance created control directors and/or particles internal bool controllingDirectors { get; private set; } internal bool controllingParticles { get; private set; } /// /// This function is called when the object is loaded. /// public void OnEnable() { // can't be set in a constructor if (particleRandomSeed == 0) particleRandomSeed = (uint)Random.Range(1, k_MaxRandInt); } /// /// Returns the duration in seconds needed to play the underlying director or particle system exactly once. /// public override double duration { get { return m_Duration; } } /// /// Returns the capabilities of TimelineClips that contain a ControlPlayableAsset /// public ClipCaps clipCaps { get { return ClipCaps.ClipIn | ClipCaps.SpeedMultiplier | (m_SupportLoop ? ClipCaps.Looping : ClipCaps.None); } } /// /// Creates the root of a Playable subgraph to control the contents of the game object. /// /// PlayableGraph that will own the playable /// The GameObject that triggered the graph build /// The root playable of the subgraph public override Playable CreatePlayable(PlayableGraph graph, GameObject go) { // case 989856 if (prefabGameObject != null) { if (s_CreatedPrefabs.Contains(prefabGameObject)) { Debug.LogWarningFormat("Control Track Clip ({0}) is causing a prefab to instantiate itself recursively. Aborting further instances.", name); return Playable.Create(graph); } s_CreatedPrefabs.Add(prefabGameObject); } Playable root = Playable.Null; var playables = new List(); GameObject sourceObject = sourceGameObject.Resolve(graph.GetResolver()); if (prefabGameObject != null) { Transform parenTransform = sourceObject != null ? sourceObject.transform : null; var controlPlayable = PrefabControlPlayable.Create(graph, prefabGameObject, parenTransform); sourceObject = controlPlayable.GetBehaviour().prefabInstance; playables.Add(controlPlayable); } m_Duration = PlayableBinding.DefaultDuration; m_SupportLoop = false; controllingParticles = false; controllingDirectors = false; if (sourceObject != null) { var directors = updateDirector ? GetComponent(sourceObject) : k_EmptyDirectorsList; var particleSystems = updateParticle ? GetControllableParticleSystems(sourceObject) : k_EmptyParticlesList; // update the duration and loop values (used for UI purposes) here // so they are tied to the latest gameObject bound UpdateDurationAndLoopFlag(directors, particleSystems); var director = go.GetComponent(); if (director != null) m_ControlDirectorAsset = director.playableAsset; if (go == sourceObject && prefabGameObject == null) { Debug.LogWarningFormat("Control Playable ({0}) is referencing the same PlayableDirector component than the one in which it is playing.", name); active = false; if (!searchHierarchy) updateDirector = false; } if (active) CreateActivationPlayable(sourceObject, graph, playables); if (updateDirector) SearchHierarchyAndConnectDirector(directors, graph, playables, prefabGameObject != null); if (updateParticle) SearchHierarchyAndConnectParticleSystem(particleSystems, graph, playables); if (updateITimeControl) SearchHierarchyAndConnectControlableScripts(GetControlableScripts(sourceObject), graph, playables); // Connect Playables to Generic to Mixer root = ConnectPlayablesToMixer(graph, playables); } if (prefabGameObject != null) s_CreatedPrefabs.Remove(prefabGameObject); if (!root.IsValid()) root = Playable.Create(graph); return root; } static Playable ConnectPlayablesToMixer(PlayableGraph graph, List playables) { var mixer = Playable.Create(graph, playables.Count); for (int i = 0; i != playables.Count; ++i) { ConnectMixerAndPlayable(graph, mixer, playables[i], i); } mixer.SetPropagateSetTime(true); return mixer; } void CreateActivationPlayable(GameObject root, PlayableGraph graph, List outplayables) { var activation = ActivationControlPlayable.Create(graph, root, postPlayback); if (activation.IsValid()) outplayables.Add(activation); } void SearchHierarchyAndConnectParticleSystem(IEnumerable particleSystems, PlayableGraph graph, List outplayables) { foreach (var particleSystem in particleSystems) { if (particleSystem != null) { controllingParticles = true; outplayables.Add(ParticleControlPlayable.Create(graph, particleSystem, particleRandomSeed)); } } } void SearchHierarchyAndConnectDirector(IEnumerable directors, PlayableGraph graph, List outplayables, bool disableSelfReferences) { foreach (var director in directors) { if (director != null) { if (director.playableAsset != m_ControlDirectorAsset) { outplayables.Add(DirectorControlPlayable.Create(graph, director)); controllingDirectors = true; } // if this self references, disable the director. else if (disableSelfReferences) { director.enabled = false; } } } } static void SearchHierarchyAndConnectControlableScripts(IEnumerable controlableScripts, PlayableGraph graph, List outplayables) { foreach (var script in controlableScripts) { outplayables.Add(TimeControlPlayable.Create(graph, (ITimeControl)script)); } } static void ConnectMixerAndPlayable(PlayableGraph graph, Playable mixer, Playable playable, int portIndex) { graph.Connect(playable, 0, mixer, portIndex); mixer.SetInputWeight(playable, 1.0f); } internal IList GetComponent(GameObject gameObject) { var components = new List(); if (gameObject != null) { if (searchHierarchy) { gameObject.GetComponentsInChildren(true, components); } else { gameObject.GetComponents(components); } } return components; } internal static IEnumerable GetControlableScripts(GameObject root) { if (root == null) yield break; foreach (var script in root.GetComponentsInChildren()) { if (script is ITimeControl) yield return script; } } internal void UpdateDurationAndLoopFlag(IList directors, IList particleSystems) { if (directors.Count == 0 && particleSystems.Count == 0) return; const double invalidDuration = double.NegativeInfinity; var maxDuration = invalidDuration; var supportsLoop = false; foreach (var director in directors) { if (director.playableAsset != null) { var assetDuration = director.playableAsset.duration; if (director.playableAsset is TimelineAsset && assetDuration > 0.0) // Timeline assets report being one tick shorter than they actually are, unless they are empty assetDuration = (double)((DiscreteTime)assetDuration).OneTickAfter(); maxDuration = Math.Max(maxDuration, assetDuration); supportsLoop = supportsLoop || director.extrapolationMode == DirectorWrapMode.Loop; } } foreach (var particleSystem in particleSystems) { maxDuration = Math.Max(maxDuration, particleSystem.main.duration); supportsLoop = supportsLoop || particleSystem.main.loop; } m_Duration = double.IsNegativeInfinity(maxDuration) ? PlayableBinding.DefaultDuration : maxDuration; m_SupportLoop = supportsLoop; } IList GetControllableParticleSystems(GameObject go) { var roots = new List(); // searchHierarchy will look for particle systems on child objects. // once a particle system is found, all child particle systems are controlled with playables // unless they are subemitters if (searchHierarchy || go.GetComponent() != null) { GetControllableParticleSystems(go.transform, roots, s_SubEmitterCollector); s_SubEmitterCollector.Clear(); } return roots; } static void GetControllableParticleSystems(Transform t, ICollection roots, HashSet subEmitters) { var ps = t.GetComponent(); if (ps != null) { if (!subEmitters.Contains(ps)) { roots.Add(ps); CacheSubEmitters(ps, subEmitters); } } for (int i = 0; i < t.childCount; ++i) { GetControllableParticleSystems(t.GetChild(i), roots, subEmitters); } } static void CacheSubEmitters(ParticleSystem ps, HashSet subEmitters) { if (ps == null) return; for (int i = 0; i < ps.subEmitters.subEmittersCount; i++) { subEmitters.Add(ps.subEmitters.GetSubEmitterSystem(i)); // don't call this recursively. subEmitters are only simulated one level deep. } } /// public void GatherProperties(PlayableDirector director, IPropertyCollector driver) { // This method is no longer called by Control Tracks. if (director == null) return; // prevent infinite recursion if (s_ProcessedDirectors.Contains(director)) return; s_ProcessedDirectors.Add(director); var gameObject = sourceGameObject.Resolve(director); if (gameObject != null) { if (updateParticle)// case 1076850 -- drive all emitters, not just roots. PreviewParticles(driver, gameObject.GetComponentsInChildren(true)); if (active) PreviewActivation(driver, new[] { gameObject }); if (updateITimeControl) PreviewTimeControl(driver, director, GetControlableScripts(gameObject)); if (updateDirector) PreviewDirectors(driver, GetComponent(gameObject)); } s_ProcessedDirectors.Remove(director); } internal static void PreviewParticles(IPropertyCollector driver, IEnumerable particles) { foreach (var ps in particles) { driver.AddFromName(ps.gameObject, "randomSeed"); driver.AddFromName(ps.gameObject, "autoRandomSeed"); } } internal static void PreviewActivation(IPropertyCollector driver, IEnumerable objects) { foreach (var gameObject in objects) driver.AddFromName(gameObject, "m_IsActive"); } internal static void PreviewTimeControl(IPropertyCollector driver, PlayableDirector director, IEnumerable scripts) { foreach (var script in scripts) { var propertyPreview = script as IPropertyPreview; if (propertyPreview != null) propertyPreview.GatherProperties(director, driver); else driver.AddFromComponent(script.gameObject, script); } } internal static void PreviewDirectors(IPropertyCollector driver, IEnumerable directors) { foreach (var childDirector in directors) { if (childDirector == null) continue; var timeline = childDirector.playableAsset as TimelineAsset; if (timeline == null) continue; timeline.GatherProperties(childDirector, driver); } } } }