// Cinema Suite using CinemaDirector.Helpers; using System; using System.Collections; using System.Collections.Generic; using UnityEngine; namespace CinemaDirector { /// /// The Cutscene is the main Behaviour of Cinema Director. /// [ExecuteInEditMode, Serializable] public class Cutscene : MonoBehaviour, IOptimizable { #region Fields [SerializeField] private float duration = 30f; // Duration of cutscene in seconds. [SerializeField] private float runningTime = 0f; // Running time of the cutscene in seconds. [SerializeField] private float playbackSpeed = 1f; // Multiplier for playback speed. [SerializeField] private CutsceneState state = CutsceneState.Inactive; [SerializeField] private bool isSkippable = true; [SerializeField] private bool isLooping = false; [SerializeField] private bool canOptimize = true; // Keeps track of the previous time an update was made. private float previousTime; // Has the Cutscene been optimized yet. private bool hasBeenOptimized = false; // Has the Cutscene been initialized yet. private bool hasBeenInitialized = false; // The cache of Track Groups that this Cutscene contains. private TrackGroup[] trackGroupCache; // A list of all the Tracks and Items revert info, to revert states on Cutscene entering and exiting play mode. private List revertCache = new List(); #endregion // Event fired when Cutscene's runtime reaches it's duration. public event CutsceneHandler CutsceneFinished; // Event fired when Cutscene has been paused. public event CutsceneHandler CutscenePaused; /// /// Optimizes the Cutscene by preparing all tracks and timeline items into a cache. /// Call this on scene load in most cases. (Avoid calling in edit mode). /// public void Optimize() { if (canOptimize) { trackGroupCache = GetTrackGroups(); } foreach (TrackGroup tg in GetTrackGroups()) { tg.Optimize(); } hasBeenOptimized = true; } /// /// Plays/Resumes the cutscene from inactive/paused states using a Coroutine. /// public void Play() { if (state == CutsceneState.Inactive) { state = CutsceneState.Playing; if (!hasBeenOptimized) { Optimize(); } if (!hasBeenInitialized) { initialize(); } StartCoroutine("updateCoroutine"); } else if (state == CutsceneState.Paused) { state = CutsceneState.Playing; StartCoroutine("updateCoroutine"); } } /// /// Pause the playback of this cutscene. /// public void Pause() { if (state == CutsceneState.Playing) { StopCoroutine("updateCoroutine"); } if (state == CutsceneState.PreviewPlaying || state == CutsceneState.Playing || state == CutsceneState.Scrubbing) { foreach (TrackGroup trackGroup in GetTrackGroups()) { trackGroup.Pause(); } } state = CutsceneState.Paused; if (CutscenePaused != null) { CutscenePaused(this, new CutsceneEventArgs()); } } /// /// Skip the cutscene to the end and stop it /// public void Skip() { if (isSkippable) { SetRunningTime(this.Duration); state = CutsceneState.Inactive; Stop(); } } /// /// Stops the cutscene. /// public void Stop() { this.RunningTime = 0f; revert(); foreach (TrackGroup trackGroup in GetTrackGroups()) { trackGroup.Stop(); } if (state == CutsceneState.Playing) { StopCoroutine("updateCoroutine"); if (state == CutsceneState.Playing && isLooping) { state = CutsceneState.Inactive; Play(); } else { state = CutsceneState.Inactive; } } else { state = CutsceneState.Inactive; } if (state == CutsceneState.Inactive) { if (CutsceneFinished != null) { CutsceneFinished(this, new CutsceneEventArgs()); } } } /// /// Updates the cutscene by the amount of time passed. /// /// The delta in time between the last update call and this one. public void UpdateCutscene(float deltaTime) { this.RunningTime += (deltaTime * playbackSpeed); foreach (TrackGroup group in GetTrackGroups()) { group.UpdateTrackGroup(RunningTime, deltaTime * playbackSpeed); } if (state != CutsceneState.Scrubbing) { if (runningTime >= duration || runningTime < 0f) { Stop(); } } } /// /// Preview play readies the cutscene to be played in edit mode. Never use for runtime. /// This is necessary for playing the cutscene in edit mode. /// public void PreviewPlay() { if (state == CutsceneState.Inactive) { EnterPreviewMode(); } else if (state == CutsceneState.Paused) { resume(); } if (Application.isPlaying) { state = CutsceneState.Playing; } else { state = CutsceneState.PreviewPlaying; } } /// /// Play the cutscene from it's given running time to a new time /// /// The new time to make up for public void ScrubToTime(float newTime) { float deltaTime = Mathf.Clamp(newTime, 0, Duration) - this.RunningTime; state = CutsceneState.Scrubbing; if (deltaTime != 0) { if (deltaTime > (1 / 30f)) { float prevTime = RunningTime; foreach (float milestone in getMilestones(RunningTime + deltaTime)) { float delta = milestone - prevTime; UpdateCutscene(delta); prevTime = milestone; } } else { UpdateCutscene(deltaTime); } } else { Pause(); } } /// /// Set the cutscene to the state of a given new running time and do not proceed to play the cutscene /// /// The new running time to be set. public void SetRunningTime(float time) { foreach (float milestone in getMilestones(time)) { foreach (TrackGroup group in this.TrackGroups) { group.SetRunningTime(milestone); } } this.RunningTime = time; } /// /// Set the cutscene into an active state. /// public void EnterPreviewMode() { if (state == CutsceneState.Inactive) { initialize(); bake(); SetRunningTime(RunningTime); state = CutsceneState.Paused; } } /// /// Set the cutscene into an inactive state. /// public void ExitPreviewMode() { Stop(); } /// /// Cutscene has been destroyed. Revert everything if necessary. /// protected void OnDestroy() { if (!Application.isPlaying) { Stop(); } } /// /// Exit and Re-enter preview mode at the current running time. /// Important for Mecanim Previewing. /// public void Refresh() { if (state != CutsceneState.Inactive) { float tempTime = runningTime; Stop(); EnterPreviewMode(); SetRunningTime(tempTime); } } /// /// Bake all Track Groups who are Bakeable. /// This is necessary for things like mecanim previewing. /// private void bake() { if (Application.isEditor) { foreach (TrackGroup group in this.TrackGroups) { if (group is IBakeable) { (group as IBakeable).Bake(); } } } } /// /// The duration of this cutscene in seconds. /// public float Duration { get { return this.duration; } set { this.duration = value; if (this.duration <= 0f) { this.duration = 0.1f; } } } /// /// Returns true if this cutscene is currently playing. /// public CutsceneState State { get { return this.state; } } /// /// The current running time of this cutscene in seconds. Values are restricted between 0 and duration. /// public float RunningTime { get { return this.runningTime; } set { runningTime = Mathf.Clamp(value, 0, duration); } } /// /// Get all Track Groups in this Cutscene. Will return cache if possible. /// /// public TrackGroup[] GetTrackGroups() { // Return the cache if possible if (hasBeenOptimized) { return trackGroupCache; } return TrackGroups; } /// /// Get all track groups in this cutscene. /// public TrackGroup[] TrackGroups { get { return base.GetComponentsInChildren(); } } /// /// Get the director group of this cutscene. /// public DirectorGroup DirectorGroup { get { return base.GetComponentInChildren(); } } /// /// Cutscene state is used to determine if the cutscene is currently Playing, Previewing (In edit mode), paused, scrubbing or inactive. /// public enum CutsceneState { Inactive, Playing, PreviewPlaying, Scrubbing, Paused } /// /// Enable this if the Cutscene does not have TrackGroups added/removed during running. /// public bool CanOptimize { get { return canOptimize; } set { canOptimize = value; } } /// /// True if Cutscene can be skipped. /// public bool IsSkippable { get { return isSkippable; } set { isSkippable = value; } } /// /// Will the Cutscene loop upon completion. /// public bool IsLooping { get { return isLooping; } set { isLooping = value; } } /// /// An important call to make before sampling the cutscene, to initialize all track groups /// and save states of all actors/game objects. /// private void initialize() { saveRevertData(); // Initialize each track group. foreach (TrackGroup trackGroup in this.TrackGroups) { trackGroup.Initialize(); } } /// /// Cache all the revert data. /// private void saveRevertData() { revertCache.Clear(); // Build the cache of revert info. foreach (MonoBehaviour mb in this.GetComponentsInChildren()) { IRevertable revertable = mb as IRevertable; if (revertable != null) { revertCache.AddRange(revertable.CacheState()); } } } /// /// Revert all data that has been manipulated by the Cutscene. /// private void revert() { foreach (RevertInfo revertable in revertCache) { if (revertable != null) { if ((revertable.EditorRevert == RevertMode.Revert && !Application.isPlaying) || (revertable.RuntimeRevert == RevertMode.Revert && Application.isPlaying)) { revertable.Revert(); } } } } /// /// Get the milestones between the current running time and the given time. /// /// The time to progress towards /// A list of times that state changes are made in the Cutscene. private List getMilestones(float time) { // Create a list of ordered milestone times. List milestoneTimes = new List(); milestoneTimes.Add(time); foreach (TrackGroup group in this.TrackGroups) { List times = group.GetMilestones(RunningTime, time); foreach (float f in times) { if (!milestoneTimes.Contains(f)) milestoneTimes.Add(f); } } milestoneTimes.Sort(); if (time < RunningTime) { milestoneTimes.Reverse(); } return milestoneTimes; } /// /// Couroutine to call UpdateCutscene while the cutscene is in the playing state. /// /// private IEnumerator updateCoroutine() { while (state == CutsceneState.Playing) { UpdateCutscene(Time.deltaTime); yield return null; } } /// /// Prepare all track groups for resuming from pause. /// private void resume() { foreach (TrackGroup group in this.TrackGroups) { group.Resume(); } } } // Delegate for handling Cutscene Events public delegate void CutsceneHandler(object sender, CutsceneEventArgs e); /// /// Cutscene event arguments. Blank for now. /// public class CutsceneEventArgs : EventArgs { public CutsceneEventArgs() { } } }