476 lines
21 KiB
C#
476 lines
21 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace Unity.Cinemachine
|
|
{
|
|
/// <summary>
|
|
/// This interface provides a way to override camera selection logic.
|
|
/// The cinemachine timeline track drives its target via this interface.
|
|
/// </summary>
|
|
public interface ICameraOverrideStack
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="overrideId">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.</param>
|
|
/// <param name="priority">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.</param>
|
|
/// <param name="camA">The camera to set, corresponding to weight=0.</param>
|
|
/// <param name="camB">The camera to set, corresponding to weight=1.</param>
|
|
/// <param name="weightB">The blend weight. 0=camA, 1=camB.</param>
|
|
/// <param name="deltaTime">Override for deltaTime. Should be Time.FixedDelta for
|
|
/// time-based calculations to be included, -1 otherwise.</param>
|
|
/// <returns>The override ID. Don't forget to call ReleaseCameraOverride
|
|
/// after all overriding is finished, to free the OverrideStack resources.</returns>
|
|
int SetCameraOverride(
|
|
int overrideId,
|
|
int priority,
|
|
ICinemachineCamera camA, ICinemachineCamera camB,
|
|
float weightB, float deltaTime);
|
|
|
|
/// <summary>
|
|
/// See SetCameraOverride. Call ReleaseCameraOverride after all overriding
|
|
/// is finished, to free the OverrideStack resources.
|
|
/// </summary>
|
|
/// <param name="overrideId">The ID to released. This is the value that
|
|
/// was returned by SetCameraOverride</param>
|
|
void ReleaseCameraOverride(int overrideId);
|
|
|
|
// GML todo: delete this
|
|
/// <summary>
|
|
/// Get the current definition of Up. May be different from Vector3.up.
|
|
/// </summary>
|
|
Vector3 DefaultWorldUp { get; }
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Implements an overridable stack of blend states.
|
|
/// </summary>
|
|
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;
|
|
|
|
// If reversing a blend-in-progress, this will indicate how much of the blend was skipped
|
|
public float MidBlendNormalizedStartPoint;
|
|
|
|
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<StackFrame> m_FrameStack = new ();
|
|
int m_NextFrameId = 0;
|
|
|
|
// To avoid GC memory alloc every frame
|
|
static readonly AnimationCurve s_DefaultLinearAnimationCurve = AnimationCurve.Linear(0, 0, 1, 1);
|
|
|
|
// GML todo: delete this
|
|
/// <summary>Get the default world up for the virtual cameras.</summary>
|
|
public Vector3 DefaultWorldUp => Vector3.up;
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Call this when object is enabled</summary>
|
|
public virtual void OnEnable()
|
|
{
|
|
// Make sure there is a first stack frame
|
|
m_FrameStack.Clear();
|
|
m_FrameStack.Add(new StackFrame());
|
|
}
|
|
|
|
/// <summary>Call this when object is disabled</summary>
|
|
public virtual void OnDisable()
|
|
{
|
|
m_FrameStack.Clear();
|
|
m_NextFrameId = 0;
|
|
}
|
|
|
|
/// <summary>Has OnEnable been called?</summary>
|
|
public bool IsInitialized => m_FrameStack.Count > 0;
|
|
|
|
/// <summary>This holds the function that performs a blend lookup.
|
|
/// This is used to find a blend definition, when a blend is being created.</summary>
|
|
public CinemachineBlendDefinition.LookupBlendDelegate LookupBlendDelegate { get; set; }
|
|
|
|
/// <summary>Clear the state of the root frame: no current camera, no blend.</summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Call this every frame with the current active camera of the root frame.
|
|
/// </summary>
|
|
/// <param name="context">The mixer context in which this blend stack exists</param>
|
|
/// <param name="activeCamera">Current active camera (pre-override)</param>
|
|
/// <param name="up">Current world up</param>
|
|
/// <param name="deltaTime">How much time has elapsed, for computing blends</param>
|
|
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 normalizedBlendPosition = 0;
|
|
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;
|
|
if (backingOutOfBlend && frame.Blend.Duration > kEpsilon)
|
|
normalizedBlendPosition = frame.MidBlendNormalizedStartPoint
|
|
+ (1 - frame.MidBlendNormalizedStartPoint) * frame.Blend.TimeInBlend / frame.Blend.Duration;
|
|
|
|
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
|
|
if (!snapshot && frame.Blend.CamA is NestedBlendSource nbs && 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 = duration * normalizedBlendPosition; // skip first part of blend
|
|
normalizedBlendPosition = 1 - normalizedBlendPosition;
|
|
}
|
|
|
|
// 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 actual 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;
|
|
frame.MidBlendNormalizedStartPoint = normalizedBlendPosition;
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compute the current blend, taking into account
|
|
/// the in-game camera and all the active overrides. Caller may optionally
|
|
/// exclude n topmost overrides.
|
|
/// </summary>
|
|
/// <param name="outputBlend">Receives the nested blend</param>
|
|
/// <param name="numTopLayersToExclude">Optionally exclude the last number
|
|
/// of overrides from the blend</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set the current root blend. Can be used to modify the root blend state.
|
|
/// <param name="blend">The new blend. CamA and CamB will be ignored, but the other fields
|
|
/// will be taken. If null, current blend will be force-completed.</param>
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the current active deltaTime override, defined in the topmost override frame.
|
|
/// </summary>
|
|
/// <returns>The active deltaTime override, or -1 for none</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|