using System.Collections.Generic; using UnityEngine; namespace Unity.Cinemachine { /// /// This interface provides a way to override camera selection logic. /// The cinemachine timeline track drives its target via this interface. /// public interface ICameraOverrideStack { /// /// Override the current camera and current blend. This setting will trump /// any in-game logic that sets virtual camera priorities and Enabled states. /// This is the main API for the timeline. /// /// Id to represent a specific client. An internal /// stack is maintained, with the most recent non-empty override taking precedence. /// This id must be > 0. If you pass -1, a new id will be created, and returned. /// Use that id for subsequent calls. Don't forget to /// call ReleaseCameraOverride after all overriding is finished, to /// free the OverrideStack resources. /// The priority to assign to the override. Higher priorities take /// precedence over lower ones. This is not connected to the Priority field in the /// individual CinemachineCameras, but the function is analogous. /// The camera to set, corresponding to weight=0. /// The camera to set, corresponding to weight=1. /// The blend weight. 0=camA, 1=camB. /// Override for deltaTime. Should be Time.FixedDelta for /// time-based calculations to be included, -1 otherwise. /// The override ID. Don't forget to call ReleaseCameraOverride /// after all overriding is finished, to free the OverrideStack resources. int SetCameraOverride( int overrideId, int priority, ICinemachineCamera camA, ICinemachineCamera camB, float weightB, float deltaTime); /// /// See SetCameraOverride. Call ReleaseCameraOverride after all overriding /// is finished, to free the OverrideStack resources. /// /// The ID to released. This is the value that /// was returned by SetCameraOverride void ReleaseCameraOverride(int overrideId); // GML todo: delete this /// /// Get the current definition of Up. May be different from Vector3.up. /// Vector3 DefaultWorldUp { get; } } /// /// Implements an overridable stack of blend states. /// class CameraBlendStack : ICameraOverrideStack { const float kEpsilon = UnityVectorExtensions.Epsilon; class StackFrame : NestedBlendSource { public int Id; public int Priority; public readonly CinemachineBlend Source = new (); public float DeltaTimeOverride; // If blending in from a snapshot, this holds the source state readonly SnapshotBlendSource m_Snapshot = new (); ICinemachineCamera m_SnapshotSource; float m_SnapshotBlendWeight; public StackFrame() : base(new ()) {} public bool Active => Source.IsValid; // This is a little tricky, because we only want to take a new snapshot at the start // of a blend. In other cases, we reuse the last snapshot taken, until a new blend starts. public ICinemachineCamera GetSnapshotIfAppropriate(ICinemachineCamera cam, float weight) { if (cam == null || (cam.State.BlendHint & CameraState.BlendHints.FreezeWhenBlendingOut) == 0) { // No snapshot required - reset it m_Snapshot.TakeSnapshot(null); m_SnapshotSource = null; m_SnapshotBlendWeight = 0; return cam; } // A snapshot is needed if (m_SnapshotSource != cam || m_SnapshotBlendWeight > weight) { // At this point we're pretty sure this is a new blend, // so we take a new snapshot of the camera state m_Snapshot.TakeSnapshot(cam); m_SnapshotSource = cam; m_SnapshotBlendWeight = weight; } // Use the most recent snapshot return m_Snapshot; } } // Current game state is always frame 0, overrides are subsequent frames readonly List m_FrameStack = new (); int m_NextFrameId = 1; // To avoid GC memory alloc every frame static readonly AnimationCurve s_DefaultLinearAnimationCurve = AnimationCurve.Linear(0, 0, 1, 1); // GML todo: delete this /// Get the default world up for the virtual cameras. public Vector3 DefaultWorldUp => Vector3.up; /// public int SetCameraOverride( int overrideId, int priority, ICinemachineCamera camA, ICinemachineCamera camB, float weightB, float deltaTime) { if (overrideId < 0) overrideId = m_NextFrameId++; if (m_FrameStack.Count == 0) m_FrameStack.Add(new StackFrame()); var frame = m_FrameStack[FindFrame(overrideId, priority)]; frame.DeltaTimeOverride = deltaTime; frame.Source.TimeInBlend = weightB; if (frame.Source.CamA != camA || frame.Source.CamB != camB) { frame.Source.CustomBlender = CinemachineCore.GetCustomBlender?.Invoke(camA, camB); frame.Source.CamA = camA; frame.Source.CamB = camB; // In case vcams are inactive game objects, make sure they get initialized properly if (camA is CinemachineVirtualCameraBase vcamA) vcamA.EnsureStarted(); if (camB is CinemachineVirtualCameraBase vcamB) vcamB.EnsureStarted(); } return overrideId; // local function to get the frame index corresponding to the ID int FindFrame(int withId, int priority) { int count = m_FrameStack.Count; int index; for (index = count - 1; index > 0; --index) if (m_FrameStack[index].Id == withId) return index; // Not found - add it for (index = 1; index < count; ++index) if (m_FrameStack[index].Priority > priority) break; var frame = new StackFrame { Id = withId, Priority = priority }; frame.Source.Duration = 1; frame.Source.BlendCurve = s_DefaultLinearAnimationCurve; m_FrameStack.Insert(index, frame); return index; } } /// public void ReleaseCameraOverride(int overrideId) { for (int i = m_FrameStack.Count - 1; i > 0; --i) { if (m_FrameStack[i].Id == overrideId) { m_FrameStack.RemoveAt(i); return; } } } /// Call this when object is enabled public void OnEnable() { // Make sure there is a first stack frame m_FrameStack.Clear(); m_FrameStack.Add(new StackFrame()); } /// Call this when object is disabled public void OnDisable() { m_FrameStack.Clear(); m_NextFrameId = -1; } /// Has OnEnable been called? public bool IsInitialized => m_FrameStack.Count > 0; /// This holds the function that performs a blend lookup. /// This is used to find a blend definition, when a blend is being created. public CinemachineBlendDefinition.LookupBlendDelegate LookupBlendDelegate { get; set; } /// Clear the state of the root frame: no current camera, no blend. public void ResetRootFrame() { // Make sure there is a first stack frame if (m_FrameStack.Count == 0) m_FrameStack.Add(new StackFrame()); else { var frame = m_FrameStack[0]; frame.Blend.ClearBlend(); frame.Blend.CamB = null; frame.Source.ClearBlend(); frame.Source.CamB = null; } } /// /// Call this every frame with the current active camera of the root frame. /// /// The mixer context in which this blend stack exists /// Current active camera (pre-override) /// Current world up /// How much time has elapsed, for computing blends public void UpdateRootFrame( ICinemachineMixer context, ICinemachineCamera activeCamera, Vector3 up, float deltaTime) { // Make sure there is a first stack frame if (m_FrameStack.Count == 0) m_FrameStack.Add(new StackFrame()); // Update the root frame (frame 0) var frame = m_FrameStack[0]; var outgoingCamera = frame.Source.CamB; if (activeCamera != outgoingCamera) { bool backingOutOfBlend = false; float duration = 0; // Do we need to create a game-play blend? if (LookupBlendDelegate != null && activeCamera != null && activeCamera.IsValid && outgoingCamera != null && outgoingCamera.IsValid && deltaTime >= 0) { // Create a blend (curve will be null if a cut) var blendDef = LookupBlendDelegate(outgoingCamera, activeCamera); if (blendDef.BlendCurve != null && blendDef.BlendTime > kEpsilon) { // Are we backing out of a blend-in-progress? backingOutOfBlend = frame.Source.CamA == activeCamera && frame.Source.CamB == outgoingCamera; frame.Source.CamA = outgoingCamera; frame.Source.BlendCurve = blendDef.BlendCurve; duration = blendDef.BlendTime; } frame.Source.Duration = duration; frame.Source.TimeInBlend = 0; frame.Source.CustomBlender = null; } frame.Source.CamB = activeCamera; // Get custom blender, if any if (duration > 0) frame.Source.CustomBlender = CinemachineCore.GetCustomBlender?.Invoke(outgoingCamera, activeCamera); // Update the working blend: // Check the status of the working blend. If not blending, no problem. // Otherwise, we need to do some work to chain the blends. ICinemachineCamera camA; if (frame.Blend.IsComplete) camA = frame.GetSnapshotIfAppropriate(outgoingCamera, 0); // new blend else { bool snapshot = (frame.Blend.State.BlendHint & CameraState.BlendHints.FreezeWhenBlendingOut) != 0; // Special check here: if incoming is InheritPosition and if it's already live // in the outgoing blend, use a snapshot otherwise there could be a pop if (!snapshot && activeCamera != null && (activeCamera.State.BlendHint & CameraState.BlendHints.InheritPosition) != 0 && frame.Blend.Uses(activeCamera)) snapshot = true; // Avoid nesting too deeply var nbs = frame.Blend.CamA as NestedBlendSource; if (!snapshot && nbs != null && nbs.Blend.CamA is NestedBlendSource nbs2) nbs2.Blend.CamA = new SnapshotBlendSource(nbs2.Blend.CamA); // Special case: if backing out of a blend-in-progress // with the same blend in reverse, adjust the blend time // to cancel out the progress made in the opposite direction if (backingOutOfBlend) { snapshot = true; // always use a snapshot for this to prevent pops duration = frame.Blend.TimeInBlend; if (nbs != null) duration += nbs.Blend.Duration - nbs.Blend.TimeInBlend; else if (frame.Blend.CamA is SnapshotBlendSource sbs) duration += sbs.RemainingTimeInBlend; } // Chain to existing blend if (snapshot) camA = new SnapshotBlendSource(frame, frame.Blend.Duration - frame.Blend.TimeInBlend); else { var blendCopy = new CinemachineBlend(); blendCopy.CopyFrom(frame.Blend); camA = new NestedBlendSource(blendCopy); } } // For the event, we use the raw outgoing camera, not the blend source frame.Blend.CamA = outgoingCamera; frame.Blend.CamB = activeCamera; frame.Blend.BlendCurve = frame.Source.BlendCurve; frame.Blend.Duration = duration; frame.Blend.TimeInBlend = 0; frame.Blend.CustomBlender = frame.Source.CustomBlender; // Allow the client to modify the blend if (duration > 0) CinemachineCore.BlendCreatedEvent.Invoke(new () { Origin = context, Blend = frame.Blend }); // In case the event handler tried to change the cameras, put them back frame.Blend.CamA = camA; frame.Blend.CamB = activeCamera; } // Advance the working blend if (AdvanceBlend(frame.Blend, deltaTime)) frame.Source.ClearBlend(); frame.UpdateCameraState(up, deltaTime); // local function static bool AdvanceBlend(CinemachineBlend blend, float deltaTime) { bool blendCompleted = false; if (blend.CamA != null) { blend.TimeInBlend += (deltaTime >= 0) ? deltaTime : blend.Duration; if (blend.IsComplete) { // No more blend blend.ClearBlend(); blendCompleted = true; } else if (blend.CamA is NestedBlendSource bs) AdvanceBlend(bs.Blend, deltaTime); } return blendCompleted; } } /// /// Compute the current blend, taking into account /// the in-game camera and all the active overrides. Caller may optionally /// exclude n topmost overrides. /// /// Receives the nested blend /// Optionally exclude the last number /// of overrides from the blend public void ProcessOverrideFrames( ref CinemachineBlend outputBlend, int numTopLayersToExclude) { // Make sure there is a first stack frame if (m_FrameStack.Count == 0) m_FrameStack.Add(new StackFrame()); // Resolve the current working frame states in the stack int lastActive = 0; int topLayer = Mathf.Max(0, m_FrameStack.Count - numTopLayersToExclude); for (int i = 1; i < topLayer; ++i) { var frame = m_FrameStack[i]; if (frame.Active) { frame.Blend.CopyFrom(frame.Source); if (frame.Source.CamA != null) frame.Blend.CamA = frame.GetSnapshotIfAppropriate( frame.Source.CamA, frame.Source.TimeInBlend); // Handle cases where we're blending into or out of nothing if (!frame.Blend.IsComplete) { if (frame.Blend.CamA == null) { frame.Blend.CamA = frame.GetSnapshotIfAppropriate( m_FrameStack[lastActive], frame.Blend.TimeInBlend); frame.Blend.CustomBlender = CinemachineCore.GetCustomBlender?.Invoke( frame.Blend.CamA, frame.Blend.CamB); } if (frame.Blend.CamB == null) { frame.Blend.CamB = m_FrameStack[lastActive]; frame.Blend.CustomBlender = CinemachineCore.GetCustomBlender?.Invoke( frame.Blend.CamA, frame.Blend.CamB); } } lastActive = i; } } outputBlend.CopyFrom(m_FrameStack[lastActive].Blend); } /// /// Set the current root blend. Can be used to modify the root blend state. /// The new blend. CamA and CamB will be ignored, but the other fields /// will be taken. If null, current blend will be force-completed. /// public void SetRootBlend(CinemachineBlend blend) { if (IsInitialized) { if (blend == null) m_FrameStack[0].Blend.Duration = 0; else { m_FrameStack[0].Blend.BlendCurve = blend.BlendCurve; m_FrameStack[0].Blend.TimeInBlend = blend.TimeInBlend; m_FrameStack[0].Blend.Duration = blend.Duration; m_FrameStack[0].Blend.CustomBlender = blend.CustomBlender; } } } /// /// Get the current active deltaTime override, defined in the topmost override frame. /// /// The active deltaTime override, or -1 for none public float GetDeltaTimeOverride() { for (int i = m_FrameStack.Count - 1; i > 0; --i) { var frame = m_FrameStack[i]; if (frame.Active) return frame.DeltaTimeOverride; } return -1; } /// /// Static source for blending. It's not really a virtual camera, but takes /// a CameraState and exposes it as a virtual camera for the purposes of blending. /// class SnapshotBlendSource : ICinemachineCamera { CameraState m_State; string m_Name; public float RemainingTimeInBlend { get; set; } public SnapshotBlendSource(ICinemachineCamera source = null, float remainingTimeInBlend = 0) { TakeSnapshot(source); RemainingTimeInBlend = remainingTimeInBlend; } public string Name => m_Name; public string Description => Name; public CameraState State => m_State; public bool IsValid => true; public ICinemachineMixer ParentCamera => null; public void UpdateCameraState(Vector3 worldUp, float deltaTime) {} public void OnCameraActivated(ICinemachineCamera.ActivationEventParams evt) {} public void TakeSnapshot(ICinemachineCamera source) { m_State = source != null ? source.State : CameraState.Default; m_State.BlendHint &= ~CameraState.BlendHints.FreezeWhenBlendingOut; m_Name ??= source == null ? "(null)" : "*" + source.Name; } } } }