File size: 9,891 Bytes
18a519f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
using System;
using UnityEngine;
using UnityEngine.Playables;

namespace UnityEngine.Timeline
{
    /// <summary>
    /// Playable Behaviour used to control a PlayableDirector.
    /// </summary>
    /// <remarks>
    /// This playable is used to control other PlayableDirector components from a Timeline sequence.
    /// </remarks>
    public class DirectorControlPlayable : PlayableBehaviour
    {
        /// <summary>
        /// The PlayableDirector being controlled by this PlayableBehaviour
        /// </summary>
        public PlayableDirector director;

        private bool m_SyncTime = false;

        private double m_AssetDuration = double.MaxValue;

        /// <summary>
        /// Creates a Playable with a DirectorControlPlayable attached
        /// </summary>
        /// <param name="graph">The graph to inject the playable into</param>
        /// <param name="director">The director to control</param>
        /// <returns>Returns a Playable with a DirectorControlPlayable attached</returns>
        public static ScriptPlayable<DirectorControlPlayable> Create(PlayableGraph graph, PlayableDirector director)
        {
            if (director == null)
                return ScriptPlayable<DirectorControlPlayable>.Null;

            var handle = ScriptPlayable<DirectorControlPlayable>.Create(graph);
            handle.GetBehaviour().director = director;

#if UNITY_EDITOR
            if (!Application.isPlaying && UnityEditor.PrefabUtility.IsPartOfPrefabInstance(director))
                UnityEditor.PrefabUtility.prefabInstanceUpdated += handle.GetBehaviour().OnPrefabUpdated;
#endif

            return handle;
        }

        /// <summary>
        /// This function is called when this PlayableBehaviour is destroyed.
        /// </summary>
        /// <param name="playable">The Playable that owns the current PlayableBehaviour.</param>
        public override void OnPlayableDestroy(Playable playable)
        {
#if UNITY_EDITOR
            if (!Application.isPlaying)
                UnityEditor.PrefabUtility.prefabInstanceUpdated -= OnPrefabUpdated;
#endif
            if (director != null && director.playableAsset != null)
                director.Stop();
        }

        /// <summary>
        /// This function is called during the PrepareFrame phase of the PlayableGraph.
        /// </summary>
        /// <param name="playable">The Playable that owns the current PlayableBehaviour.</param>
        /// <param name="info">A FrameData structure that contains information about the current frame context.</param>
        public override void PrepareFrame(Playable playable, FrameData info)
        {
            if (director == null || !director.isActiveAndEnabled || director.playableAsset == null)
                return;

            // resync the time on an evaluate or a time jump (caused by loops, or some setTime calls)
            m_SyncTime |= (info.evaluationType == FrameData.EvaluationType.Evaluate) ||
                DetectDiscontinuity(playable, info);

            SyncSpeed(info.effectiveSpeed);
            SyncStart(playable.GetGraph(), playable.GetTime());
#if !UNITY_2021_2_OR_NEWER
            SyncStop(playable.GetGraph(), playable.GetTime());
#endif
        }

        /// <summary>
        /// This function is called when the Playable play state is changed to Playables.PlayState.Playing.
        /// </summary>
        /// <param name="playable">The Playable that owns the current PlayableBehaviour.</param>
        /// <param name="info">A FrameData structure that contains information about the current frame context.</param>
        public override void OnBehaviourPlay(Playable playable, FrameData info)
        {
            m_SyncTime = true;

            if (director != null && director.playableAsset != null)
                m_AssetDuration = director.playableAsset.duration;
        }

        /// <summary>
        /// This function is called when the Playable play state is changed to PlayState.Paused.
        /// </summary>
        /// <param name="playable">The playable this behaviour is attached to.</param>
        /// <param name="info">A FrameData structure that contains information about the current frame context.</param>
        public override void OnBehaviourPause(Playable playable, FrameData info)
        {
            if (director != null && director.playableAsset != null)
            {
                if (info.effectivePlayState == PlayState.Playing) // graph was paused
                    director.Pause();
                else
                    director.Stop();
            }
        }

        /// <summary>
        /// This function is called during the ProcessFrame phase of the PlayableGraph.
        /// </summary>
        /// <param name="playable">The playable this behaviour is attached to.</param>
        /// <param name="info">A FrameData structure that contains information about the current frame context.</param>
        /// <param name="playerData">unused</param>
        public override void ProcessFrame(Playable playable, FrameData info, object playerData)
        {
            if (director == null || !director.isActiveAndEnabled || director.playableAsset == null)
                return;

            if (m_SyncTime || DetectOutOfSync(playable))
            {
                UpdateTime(playable);
                if (director.playableGraph.IsValid())
                {
                    director.playableGraph.Evaluate();
#if TIMELINE_FRAMEACCURATE
                    director.playableGraph.SynchronizeEvaluation(playable.GetGraph());
#endif
                }
                else
                {
                    director.Evaluate();
                }
            }

            m_SyncTime = false;
#if UNITY_2021_2_OR_NEWER
            SyncStop(playable.GetGraph(), playable.GetTime());
#endif
        }

#if UNITY_EDITOR
        void OnPrefabUpdated(GameObject go)
        {
            // When the prefab asset is updated, we rebuild the graph to reflect the changes in editor
            if (UnityEditor.PrefabUtility.GetRootGameObject(director) == go)
                director.RebuildGraph();
        }

#endif

        void SyncSpeed(double speed)
        {
            if (director.playableGraph.IsValid())
            {
                int roots = director.playableGraph.GetRootPlayableCount();
                for (int i = 0; i < roots; i++)
                {
                    var rootPlayable = director.playableGraph.GetRootPlayable(i);
                    if (rootPlayable.IsValid())
                    {
                        rootPlayable.SetSpeed(speed);
                    }
                }
            }
        }

        void SyncStart(PlayableGraph graph, double time)
        {
            if (director.state == PlayState.Playing
                || !graph.IsPlaying()
                || (director.extrapolationMode == DirectorWrapMode.None && time > m_AssetDuration))
                return;
#if TIMELINE_FRAMEACCURATE
            if (graph.IsMatchFrameRateEnabled())
                director.Play(graph.GetFrameRate());
            else
                director.Play();
#else
            director.Play();
#endif
        }

        void SyncStop(PlayableGraph graph, double time)
        {
            if (director.state == PlayState.Paused)
                return;

            bool expectedFinished = director.extrapolationMode == DirectorWrapMode.None && time > m_AssetDuration;
            if (expectedFinished || !graph.IsPlaying())
                director.Pause();
        }

        bool DetectDiscontinuity(Playable playable, FrameData info)
        {
            return Math.Abs(playable.GetTime() - playable.GetPreviousTime() - info.m_DeltaTime * info.m_EffectiveSpeed) > DiscreteTime.tickValue;
        }

        bool DetectOutOfSync(Playable playable)
        {
            double expectedTime = playable.GetTime();
            if (playable.GetTime() >= m_AssetDuration)
            {
                switch (director.extrapolationMode)
                {
                    case DirectorWrapMode.None:
                        expectedTime = m_AssetDuration;
                        break;
                    case DirectorWrapMode.Hold:
                        expectedTime = m_AssetDuration;
                        break;
                    case DirectorWrapMode.Loop:
                        expectedTime %= m_AssetDuration;
                        break;
                }
            }

            if (!Mathf.Approximately((float)expectedTime, (float)director.time))
            {
#if UNITY_EDITOR
                double lastDelta = playable.GetTime() - playable.GetPreviousTime();
                if (UnityEditor.Unsupported.IsDeveloperBuild())
                    Debug.LogWarningFormat("Internal Warning - Control track desync detected on {2} ({0:F10} vs {1:F10} with delta {3:F10}). Time will be resynchronized. Known to happen with nested control tracks", playable.GetTime(), director.time, director.name, lastDelta);
#endif
                return true;
            }
            return false;
        }

        // We need to handle loop modes explicitly since we are setting the time directly
        void UpdateTime(Playable playable)
        {
            double duration = Math.Max(0.1, director.playableAsset.duration);
            switch (director.extrapolationMode)
            {
                case DirectorWrapMode.Hold:
                    director.time = Math.Min(duration, Math.Max(0, playable.GetTime()));
                    break;
                case DirectorWrapMode.Loop:
                    director.time = Math.Max(0, playable.GetTime() % duration);
                    break;
                case DirectorWrapMode.None:
                    director.time = Math.Min(duration, Math.Max(0, playable.GetTime()));
                    break;
            }
        }
    }
}