A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using UnityEngine.Playables;
5
6namespace UnityEngine.Timeline
7{
8 /// <summary>
9 /// Playable Asset that generates playables for controlling time-related elements on a GameObject.
10 /// </summary>
11 [Serializable]
12 [NotKeyable]
13 public class ControlPlayableAsset : PlayableAsset, IPropertyPreview, ITimelineClipAsset
14 {
15 const int k_MaxRandInt = 10000;
16 static readonly List<PlayableDirector> k_EmptyDirectorsList = new List<PlayableDirector>(0);
17 static readonly List<ParticleSystem> k_EmptyParticlesList = new List<ParticleSystem>(0);
18 static readonly HashSet<ParticleSystem> s_SubEmitterCollector = new HashSet<ParticleSystem>();
19
20 /// <summary>
21 /// GameObject in the scene to control, or the parent of the instantiated prefab.
22 /// </summary>
23 [SerializeField] public ExposedReference<GameObject> sourceGameObject;
24
25 /// <summary>
26 /// Prefab object that will be instantiated.
27 /// </summary>
28 [SerializeField] public GameObject prefabGameObject;
29
30 /// <summary>
31 /// Indicates whether Particle Systems will be controlled.
32 /// </summary>
33 [SerializeField] public bool updateParticle = true;
34
35 /// <summary>
36 /// Random seed to supply particle systems that are set to use autoRandomSeed
37 /// </summary>
38 /// <remarks>
39 /// This is used to maintain determinism when playing back in timeline. Sub emitters will be assigned incrementing random seeds to maintain determinism and distinction.
40 /// </remarks>
41 [SerializeField] public uint particleRandomSeed;
42
43 /// <summary>
44 /// Indicates whether playableDirectors are controlled.
45 /// </summary>
46 [SerializeField] public bool updateDirector = true;
47
48 /// <summary>
49 /// Indicates whether Monobehaviours implementing ITimeControl will be controlled.
50 /// </summary>
51 [SerializeField] public bool updateITimeControl = true;
52
53 /// <summary>
54 /// Indicates whether to search the entire hierarchy for controllable components.
55 /// </summary>
56 [SerializeField] public bool searchHierarchy;
57
58 /// <summary>
59 /// Indicate whether GameObject activation is controlled
60 /// </summary>
61 [SerializeField] public bool active = true;
62
63 /// <summary>
64 /// Indicates the active state of the GameObject when Timeline is stopped.
65 /// </summary>
66 [SerializeField]
67 public ActivationControlPlayable.PostPlaybackState postPlayback = ActivationControlPlayable.PostPlaybackState.Revert;
68
69 /// <summary>
70 /// Which action to apply to the <see cref="PlayableDirector"/> at the end of the control clip. <seealso cref="DirectorControlPlayable.PauseAction"/>
71 /// </summary>
72 [SerializeField]
73 public DirectorControlPlayable.PauseAction directorOnClipEnd;
74
75 PlayableAsset m_ControlDirectorAsset;
76 double m_Duration = PlayableBinding.DefaultDuration;
77 bool m_SupportLoop;
78
79 private static HashSet<PlayableDirector> s_ProcessedDirectors = new HashSet<PlayableDirector>();
80 private static HashSet<GameObject> s_CreatedPrefabs = new HashSet<GameObject>();
81
82 // does the last instance created control directors and/or particles
83 internal bool controllingDirectors { get; private set; }
84 internal bool controllingParticles { get; private set; }
85
86 /// <summary>
87 /// This function is called when the object is loaded.
88 /// </summary>
89 public void OnEnable()
90 {
91 // can't be set in a constructor
92 if (particleRandomSeed == 0)
93 particleRandomSeed = (uint)Random.Range(1, k_MaxRandInt);
94 }
95
96 /// <summary>
97 /// Returns the duration in seconds needed to play the underlying director or particle system exactly once.
98 /// </summary>
99 public override double duration { get { return m_Duration; } }
100
101 /// <summary>
102 /// Returns the capabilities of TimelineClips that contain a ControlPlayableAsset
103 /// </summary>
104 public ClipCaps clipCaps
105 {
106 get { return ClipCaps.ClipIn | ClipCaps.SpeedMultiplier | (m_SupportLoop ? ClipCaps.Looping : ClipCaps.None); }
107 }
108
109 /// <summary>
110 /// Creates the root of a Playable subgraph to control the contents of the game object.
111 /// </summary>
112 /// <param name="graph">PlayableGraph that will own the playable</param>
113 /// <param name="go">The GameObject that triggered the graph build</param>
114 /// <returns>The root playable of the subgraph</returns>
115 public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
116 {
117 // case 989856
118 if (prefabGameObject != null)
119 {
120 if (s_CreatedPrefabs.Contains(prefabGameObject))
121 {
122 Debug.LogWarningFormat("Control Track Clip ({0}) is causing a prefab to instantiate itself recursively. Aborting further instances.", name);
123 return Playable.Create(graph);
124 }
125 s_CreatedPrefabs.Add(prefabGameObject);
126 }
127
128 Playable root = Playable.Null;
129 var playables = new List<Playable>();
130
131 GameObject sourceObject = sourceGameObject.Resolve(graph.GetResolver());
132 if (prefabGameObject != null)
133 {
134 Transform parenTransform = sourceObject != null ? sourceObject.transform : null;
135 var controlPlayable = PrefabControlPlayable.Create(graph, prefabGameObject, parenTransform);
136
137 sourceObject = controlPlayable.GetBehaviour().prefabInstance;
138 playables.Add(controlPlayable);
139 }
140
141 m_Duration = PlayableBinding.DefaultDuration;
142 m_SupportLoop = false;
143
144 controllingParticles = false;
145 controllingDirectors = false;
146
147 if (sourceObject != null)
148 {
149 var directors = updateDirector ? GetComponent<PlayableDirector>(sourceObject) : k_EmptyDirectorsList;
150 var particleSystems = updateParticle ? GetControllableParticleSystems(sourceObject) : k_EmptyParticlesList;
151
152 // update the duration and loop values (used for UI purposes) here
153 // so they are tied to the latest gameObject bound
154 UpdateDurationAndLoopFlag(directors, particleSystems);
155
156 var director = go.GetComponent<PlayableDirector>();
157 if (director != null)
158 m_ControlDirectorAsset = director.playableAsset;
159
160 if (go == sourceObject && prefabGameObject == null)
161 {
162 Debug.LogWarningFormat("Control Playable ({0}) is referencing the same PlayableDirector component than the one in which it is playing.", name);
163 active = false;
164 if (!searchHierarchy)
165 updateDirector = false;
166 }
167
168 if (active)
169 CreateActivationPlayable(sourceObject, graph, playables);
170
171 if (updateDirector)
172 SearchHierarchyAndConnectDirector(directors, graph, playables, prefabGameObject != null);
173
174 if (updateParticle)
175 SearchHierarchyAndConnectParticleSystem(particleSystems, graph, playables);
176
177 if (updateITimeControl)
178 SearchHierarchyAndConnectControlableScripts(GetControlableScripts(sourceObject), graph, playables);
179
180 // Connect Playables to Generic to Mixer
181 root = ConnectPlayablesToMixer(graph, playables);
182 }
183
184 if (prefabGameObject != null)
185 s_CreatedPrefabs.Remove(prefabGameObject);
186
187 if (!root.IsValid())
188 root = Playable.Create(graph);
189
190 return root;
191 }
192
193 static Playable ConnectPlayablesToMixer(PlayableGraph graph, List<Playable> playables)
194 {
195 var mixer = Playable.Create(graph, playables.Count);
196
197 for (int i = 0; i != playables.Count; ++i)
198 {
199 ConnectMixerAndPlayable(graph, mixer, playables[i], i);
200 }
201
202 mixer.SetPropagateSetTime(true);
203
204 return mixer;
205 }
206
207 void CreateActivationPlayable(GameObject root, PlayableGraph graph,
208 List<Playable> outplayables)
209 {
210 var activation = ActivationControlPlayable.Create(graph, root, postPlayback);
211 if (activation.IsValid())
212 outplayables.Add(activation);
213 }
214
215 void SearchHierarchyAndConnectParticleSystem(IEnumerable<ParticleSystem> particleSystems, PlayableGraph graph,
216 List<Playable> outplayables)
217 {
218 foreach (var particleSystem in particleSystems)
219 {
220 if (particleSystem != null)
221 {
222 controllingParticles = true;
223 outplayables.Add(ParticleControlPlayable.Create(graph, particleSystem, particleRandomSeed));
224 }
225 }
226 }
227
228 void SearchHierarchyAndConnectDirector(IEnumerable<PlayableDirector> directors, PlayableGraph graph,
229 List<Playable> outplayables, bool disableSelfReferences)
230 {
231 foreach (var director in directors)
232 {
233 if (director != null)
234 {
235 if (director.playableAsset != m_ControlDirectorAsset)
236 {
237 ScriptPlayable<DirectorControlPlayable> directorControlPlayable = DirectorControlPlayable.Create(graph, director);
238 directorControlPlayable.GetBehaviour().pauseAction = directorOnClipEnd;
239 outplayables.Add(directorControlPlayable);
240
241 controllingDirectors = true;
242 }
243 // if this self references, disable the director.
244 else if (disableSelfReferences)
245 {
246 director.enabled = false;
247 }
248 }
249 }
250 }
251
252 static void SearchHierarchyAndConnectControlableScripts(IEnumerable<MonoBehaviour> controlableScripts, PlayableGraph graph, List<Playable> outplayables)
253 {
254 foreach (var script in controlableScripts)
255 {
256 outplayables.Add(TimeControlPlayable.Create(graph, (ITimeControl)script));
257 }
258 }
259
260 static void ConnectMixerAndPlayable(PlayableGraph graph, Playable mixer, Playable playable,
261 int portIndex)
262 {
263 graph.Connect(playable, 0, mixer, portIndex);
264 mixer.SetInputWeight(playable, 1.0f);
265 }
266
267 internal IList<T> GetComponent<T>(GameObject gameObject)
268 {
269 var components = new List<T>();
270 if (gameObject != null)
271 {
272 if (searchHierarchy)
273 {
274 gameObject.GetComponentsInChildren<T>(true, components);
275 }
276 else
277 {
278 gameObject.GetComponents<T>(components);
279 }
280 }
281 return components;
282 }
283
284 internal static IEnumerable<MonoBehaviour> GetControlableScripts(GameObject root)
285 {
286 if (root == null)
287 yield break;
288
289 foreach (var script in root.GetComponentsInChildren<MonoBehaviour>())
290 {
291 if (script is ITimeControl)
292 yield return script;
293 }
294 }
295
296 internal void UpdateDurationAndLoopFlag(IList<PlayableDirector> directors, IList<ParticleSystem> particleSystems)
297 {
298 if (directors.Count == 0 && particleSystems.Count == 0)
299 return;
300
301 const double invalidDuration = double.NegativeInfinity;
302
303 var maxDuration = invalidDuration;
304 var supportsLoop = false;
305
306 foreach (var director in directors)
307 {
308 if (director.playableAsset != null)
309 {
310 var assetDuration = director.playableAsset.duration;
311
312 if (director.playableAsset is TimelineAsset && assetDuration > 0.0)
313 // Timeline assets report being one tick shorter than they actually are, unless they are empty
314 assetDuration = (double)((DiscreteTime)assetDuration).OneTickAfter();
315
316 maxDuration = Math.Max(maxDuration, assetDuration);
317 supportsLoop = supportsLoop || director.extrapolationMode == DirectorWrapMode.Loop;
318 }
319 }
320
321 foreach (var particleSystem in particleSystems)
322 {
323 maxDuration = Math.Max(maxDuration, particleSystem.main.duration);
324 supportsLoop = supportsLoop || particleSystem.main.loop;
325 }
326
327 m_Duration = double.IsNegativeInfinity(maxDuration) ? PlayableBinding.DefaultDuration : maxDuration;
328 m_SupportLoop = supportsLoop;
329 }
330
331 IList<ParticleSystem> GetControllableParticleSystems(GameObject go)
332 {
333 var roots = new List<ParticleSystem>();
334
335 // searchHierarchy will look for particle systems on child objects.
336 // once a particle system is found, all child particle systems are controlled with playables
337 // unless they are subemitters
338
339 if (searchHierarchy || go.GetComponent<ParticleSystem>() != null)
340 {
341 GetControllableParticleSystems(go.transform, roots, s_SubEmitterCollector);
342 s_SubEmitterCollector.Clear();
343 }
344
345 return roots;
346 }
347
348 static void GetControllableParticleSystems(Transform t, ICollection<ParticleSystem> roots, HashSet<ParticleSystem> subEmitters)
349 {
350 var ps = t.GetComponent<ParticleSystem>();
351 if (ps != null)
352 {
353 if (!subEmitters.Contains(ps))
354 {
355 roots.Add(ps);
356 CacheSubEmitters(ps, subEmitters);
357 }
358 }
359
360 for (int i = 0; i < t.childCount; ++i)
361 {
362 GetControllableParticleSystems(t.GetChild(i), roots, subEmitters);
363 }
364 }
365
366 static void CacheSubEmitters(ParticleSystem ps, HashSet<ParticleSystem> subEmitters)
367 {
368 if (ps == null)
369 return;
370
371 for (int i = 0; i < ps.subEmitters.subEmittersCount; i++)
372 {
373 subEmitters.Add(ps.subEmitters.GetSubEmitterSystem(i));
374 // don't call this recursively. subEmitters are only simulated one level deep.
375 }
376 }
377
378 /// <inheritdoc/>
379 public void GatherProperties(PlayableDirector director, IPropertyCollector driver)
380 {
381 // This method is no longer called by Control Tracks.
382 if (director == null)
383 return;
384
385 // prevent infinite recursion
386 if (s_ProcessedDirectors.Contains(director))
387 return;
388 s_ProcessedDirectors.Add(director);
389
390 var gameObject = sourceGameObject.Resolve(director);
391 if (gameObject != null)
392 {
393 if (updateParticle)// case 1076850 -- drive all emitters, not just roots.
394 PreviewParticles(driver, gameObject.GetComponentsInChildren<ParticleSystem>(true));
395
396 if (active)
397 PreviewActivation(driver, new[] { gameObject });
398
399 if (updateITimeControl)
400 PreviewTimeControl(driver, director, GetControlableScripts(gameObject));
401
402 if (updateDirector)
403 PreviewDirectors(driver, GetComponent<PlayableDirector>(gameObject));
404 }
405 s_ProcessedDirectors.Remove(director);
406 }
407
408 internal static void PreviewParticles(IPropertyCollector driver, IEnumerable<ParticleSystem> particles)
409 {
410 foreach (var ps in particles)
411 {
412 driver.AddFromName<ParticleSystem>(ps.gameObject, "randomSeed");
413 driver.AddFromName<ParticleSystem>(ps.gameObject, "autoRandomSeed");
414 }
415 }
416
417 internal static void PreviewActivation(IPropertyCollector driver, IEnumerable<GameObject> objects)
418 {
419 foreach (var gameObject in objects)
420 driver.AddFromName(gameObject, "m_IsActive");
421 }
422
423 internal static void PreviewTimeControl(IPropertyCollector driver, PlayableDirector director, IEnumerable<MonoBehaviour> scripts)
424 {
425 foreach (var script in scripts)
426 {
427 var propertyPreview = script as IPropertyPreview;
428 if (propertyPreview != null)
429 propertyPreview.GatherProperties(director, driver);
430 else
431 driver.AddFromComponent(script.gameObject, script);
432 }
433 }
434
435 internal static void PreviewDirectors(IPropertyCollector driver, IEnumerable<PlayableDirector> directors)
436 {
437 foreach (var childDirector in directors)
438 {
439 if (childDirector == null)
440 continue;
441
442 var timeline = childDirector.playableAsset as TimelineAsset;
443 if (timeline == null)
444 continue;
445
446 timeline.GatherProperties(childDirector, driver);
447 }
448 }
449 }
450}