#if !UNITY_2019_3_OR_NEWER
#define CINEMACHINE_UNITY_ANIMATION
#endif
using Cinemachine.Utility;
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Cinemachine
{
#if CINEMACHINE_UNITY_ANIMATION
///
/// This is a virtual camera "manager" that owns and manages a collection
/// of child Virtual Cameras. These child vcams are mapped to individual states in
/// an animation state machine, allowing you to associate specific vcams to specific
/// animation states. When that state is active in the state machine, then the
/// associated camera will be activated.
///
/// You can define custom blends and transitions between child cameras.
///
/// In order to use this behaviour, you must have an animated target (i.e. an object
/// animated with a state machine) to drive the behaviour.
///
[DocumentationSorting(DocumentationSortingAttribute.Level.UserRef)]
[DisallowMultipleComponent]
[ExecuteAlways]
[ExcludeFromPreset]
[AddComponentMenu("Cinemachine/CinemachineStateDrivenCamera")]
[HelpURL(Documentation.BaseURL + "manual/CinemachineStateDrivenCamera.html")]
public class CinemachineStateDrivenCamera : CinemachineVirtualCameraBase
{
/// Default object for the camera children to look at (the aim target),
/// if not specified in a child rig. May be empty
[Tooltip("Default object for the camera children to look at (the aim target), "
+ "if not specified in a child camera. May be empty if all of the children "
+ "define targets of their own.")]
[NoSaveDuringPlay]
[VcamTargetProperty]
public Transform m_LookAt = null;
/// Default object for the camera children wants to move with (the body target),
/// if not specified in a child rig. May be empty
[Tooltip("Default object for the camera children wants to move with (the body target), "
+ "if not specified in a child camera. May be empty if all of the children "
+ "define targets of their own.")]
[NoSaveDuringPlay]
[VcamTargetProperty]
public Transform m_Follow = null;
/// The state machine whose state changes will drive this camera's choice of active child
[Space]
[Tooltip("The state machine whose state changes will drive this camera's choice of active child")]
[NoSaveDuringPlay]
public Animator m_AnimatedTarget;
/// Which layer in the target FSM to observe
[Tooltip("Which layer in the target state machine to observe")]
[NoSaveDuringPlay]
public int m_LayerIndex;
/// When enabled, the current camera and blend will be indicated in
/// the game window, for debugging
[Tooltip("When enabled, the current child camera and blend will be indicated in "
+ "the game window, for debugging")]
public bool m_ShowDebugText = false;
/// Internal API for the editor. Do not use this field
[SerializeField][HideInInspector][NoSaveDuringPlay]
internal CinemachineVirtualCameraBase[] m_ChildCameras = null;
/// This represents a single instrunction to the StateDrivenCamera. It associates
/// an state from the state machine with a child Virtual Camera, and also holds
/// activation tuning parameters.
[Serializable]
public struct Instruction
{
/// The full hash of the animation state
[Tooltip("The full hash of the animation state")]
public int m_FullHash;
/// The virtual camera to activate when the animation state becomes active
[Tooltip("The virtual camera to activate when the animation state becomes active")]
public CinemachineVirtualCameraBase m_VirtualCamera;
/// How long to wait (in seconds) before activating the virtual camera.
/// This filters out very short state durations
[Tooltip("How long to wait (in seconds) before activating the virtual camera. "
+ "This filters out very short state durations")]
public float m_ActivateAfter;
/// The minimum length of time (in seconds) to keep a virtual camera active
[Tooltip("The minimum length of time (in seconds) to keep a virtual camera active")]
public float m_MinDuration;
};
/// The set of instructions associating virtual cameras with states.
/// These instructions are used to choose the live child at any given moment
[Tooltip("The set of instructions associating virtual cameras with states. "
+ "These instructions are used to choose the live child at any given moment")]
public Instruction[] m_Instructions;
///
/// The blend which is used if you don't explicitly define a blend between two Virtual Camera children.
///
[CinemachineBlendDefinitionProperty]
[Tooltip("The blend which is used if you don't explicitly define a blend between two Virtual Camera children")]
public CinemachineBlendDefinition m_DefaultBlend
= new CinemachineBlendDefinition(CinemachineBlendDefinition.Style.EaseInOut, 0.5f);
///
/// This is the asset which contains custom settings for specific child blends.
///
[Tooltip("This is the asset which contains custom settings for specific child blends")]
public CinemachineBlenderSettings m_CustomBlends = null;
/// Internal API for the Inspector editor. This implements nested states.
[Serializable]
[DocumentationSorting(DocumentationSortingAttribute.Level.Undoc)]
internal struct ParentHash
{
/// Internal API for the Inspector editor
public int m_Hash;
/// Internal API for the Inspector editor
public int m_ParentHash;
/// Internal API for the Inspector editor
public ParentHash(int h, int p) { m_Hash = h; m_ParentHash = p; }
}
/// Internal API for the Inspector editor
[HideInInspector][SerializeField] internal ParentHash[] m_ParentHash = null;
/// Gets a brief debug description of this virtual camera, for use when displayiong debug info
public override string Description
{
get
{
// Show the active camera and blend
if (mActiveBlend != null)
return mActiveBlend.Description;
ICinemachineCamera vcam = LiveChild;
if (vcam == null)
return "(none)";
var sb = CinemachineDebug.SBFromPool();
sb.Append("["); sb.Append(vcam.Name); sb.Append("]");
string text = sb.ToString();
CinemachineDebug.ReturnToPool(sb);
return text;
}
}
/// Get the current "best" child virtual camera, that would be chosen
/// if the State Driven Camera were active.
public ICinemachineCamera LiveChild { get; set; }
/// Check whether the vcam a live child of this camera.
/// The Virtual Camera to check
/// If true, will only return true if this vcam is the dominat live child
/// True if the vcam is currently actively influencing the state of this vcam
public override bool IsLiveChild(ICinemachineCamera vcam, bool dominantChildOnly = false)
{
return vcam == LiveChild || (mActiveBlend != null && mActiveBlend.Uses(vcam));
}
/// The State of the current live child
public override CameraState State { get { return m_State; } }
/// Get the current LookAt target. Returns parent's LookAt if parent
/// is non-null and no specific LookAt defined for this camera
override public Transform LookAt
{
get { return ResolveLookAt(m_LookAt); }
set { m_LookAt = value; }
}
/// Get the current Follow target. Returns parent's Follow if parent
/// is non-null and no specific Follow defined for this camera
override public Transform Follow
{
get { return ResolveFollow(m_Follow); }
set { m_Follow = value; }
}
/// This is called to notify the vcam that a target got warped,
/// so that the vcam can update its internal state to make the camera
/// also warp seamlessly.
/// The object that was warped
/// The amount the target's position changed
public override void OnTargetObjectWarped(Transform target, Vector3 positionDelta)
{
UpdateListOfChildren();
foreach (var vcam in m_ChildCameras)
vcam.OnTargetObjectWarped(target, positionDelta);
base.OnTargetObjectWarped(target, positionDelta);
}
///
/// Force the virtual camera to assume a given position and orientation
///
/// Worldspace pposition to take
/// Worldspace orientation to take
public override void ForceCameraPosition(Vector3 pos, Quaternion rot)
{
UpdateListOfChildren();
foreach (var vcam in m_ChildCameras)
vcam.ForceCameraPosition(pos, rot);
base.ForceCameraPosition(pos, rot);
}
/// Notification that this virtual camera is going live.
/// The camera being deactivated. May be null.
/// Default world Up, set by the CinemachineBrain
/// Delta time for time-based effects (ignore if less than or equal to 0)
public override void OnTransitionFromCamera(
ICinemachineCamera fromCam, Vector3 worldUp, float deltaTime)
{
base.OnTransitionFromCamera(fromCam, worldUp, deltaTime);
InvokeOnTransitionInExtensions(fromCam, worldUp, deltaTime);
m_TransitioningFrom = fromCam;
InternalUpdateCameraState(worldUp, deltaTime);
}
ICinemachineCamera m_TransitioningFrom;
/// Internal use only. Do not call this method.
/// Called by CinemachineCore at designated update time
/// so the vcam can position itself and track its targets. This implementation
/// updates all the children, chooses the best one, and implements any required blending.
/// Default world Up, set by the CinemachineBrain
/// Delta time for time-based effects (ignore if less than or equal to 0)
public override void InternalUpdateCameraState(Vector3 worldUp, float deltaTime)
{
UpdateListOfChildren();
CinemachineVirtualCameraBase best = ChooseCurrentCamera();
if (best != null && !best.gameObject.activeInHierarchy)
{
best.gameObject.SetActive(true);
best.UpdateCameraState(worldUp, deltaTime);
}
ICinemachineCamera previousCam = LiveChild;
LiveChild = best;
// Are we transitioning cameras?
if (previousCam != LiveChild && LiveChild != null)
{
// Notify incoming camera of transition
LiveChild.OnTransitionFromCamera(previousCam, worldUp, deltaTime);
// Generate Camera Activation event in the brain if live
CinemachineCore.Instance.GenerateCameraActivationEvent(LiveChild, previousCam);
if (previousCam != null)
{
// Create a blend (will be null if a cut)
mActiveBlend = CreateBlend(
previousCam, LiveChild,
LookupBlend(previousCam, LiveChild), mActiveBlend);
// If cutting, generate a camera cut event if live
if (mActiveBlend == null || !mActiveBlend.Uses(previousCam))
CinemachineCore.Instance.GenerateCameraCutEvent(LiveChild);
}
}
// Advance the current blend (if any)
if (mActiveBlend != null)
{
mActiveBlend.TimeInBlend += (deltaTime >= 0)
? deltaTime : mActiveBlend.Duration;
if (mActiveBlend.IsComplete)
mActiveBlend = null;
}
if (mActiveBlend != null)
{
mActiveBlend.UpdateCameraState(worldUp, deltaTime);
m_State = mActiveBlend.State;
}
else if (LiveChild != null)
{
if (m_TransitioningFrom != null)
LiveChild.OnTransitionFromCamera(m_TransitioningFrom , worldUp, deltaTime);
m_State = LiveChild.State;
}
m_TransitioningFrom = null;
InvokePostPipelineStageCallback(this, CinemachineCore.Stage.Finalize, ref m_State, deltaTime);
PreviousStateIsValid = true;
}
/// Makes sure the internal child cache is up to date
protected override void OnEnable()
{
base.OnEnable();
InvalidateListOfChildren();
mActiveBlend = null;
CinemachineDebug.OnGUIHandlers -= OnGuiHandler;
CinemachineDebug.OnGUIHandlers += OnGuiHandler;
}
///
/// Uninstall the GUI handler
///
protected override void OnDisable()
{
base.OnDisable();
CinemachineDebug.OnGUIHandlers -= OnGuiHandler;
}
/// Makes sure the internal child cache is up to date
public void OnTransformChildrenChanged()
{
InvalidateListOfChildren();
}
/// Will only be called if Unity Editor - never in build
private void OnGuiHandler()
{
if (!m_ShowDebugText)
CinemachineDebug.ReleaseScreenPos(this);
else
{
var sb = CinemachineDebug.SBFromPool();
sb.Append(Name); sb.Append(": "); sb.Append(Description);
string text = sb.ToString();
Rect r = CinemachineDebug.GetScreenPos(this, text, GUI.skin.box);
GUI.Label(r, text, GUI.skin.box);
CinemachineDebug.ReturnToPool(sb);
}
}
CameraState m_State = CameraState.Default;
/// The list of child cameras. These are just the immediate children in the hierarchy.
public CinemachineVirtualCameraBase[] ChildCameras { get { UpdateListOfChildren(); return m_ChildCameras; }}
/// Is there a blend in progress?
public bool IsBlending => mActiveBlend != null;
///
/// Get the current active blend in progress. Will return null if no blend is in progress.
///
public CinemachineBlend ActiveBlend => mActiveBlend;
/// API for the inspector editor. Animation module does not have hashes
/// for state parents, so we have to invent them in order to implement nested state
/// handling
/// Parent state's hash
/// The clip to create the fake hash for
/// The fake hash
public static int CreateFakeHash(int parentHash, AnimationClip clip)
{
return Animator.StringToHash(parentHash.ToString() + "_" + clip.name);
}
// Avoid garbage string manipulations at runtime
struct HashPair { public int parentHash; public int hash; }
Dictionary> mHashCache;
int LookupFakeHash(int parentHash, AnimationClip clip)
{
if (mHashCache == null)
mHashCache = new Dictionary>();
List list = null;
if (!mHashCache.TryGetValue(clip, out list))
{
list = new List();
mHashCache[clip] = list;
}
for (int i = 0; i < list.Count; ++i)
if (list[i].parentHash == parentHash)
return list[i].hash;
int newHash = CreateFakeHash(parentHash, clip);
list.Add(new HashPair() { parentHash = parentHash, hash = newHash });
mStateParentLookup[newHash] = parentHash;
return newHash;
}
float mActivationTime = 0;
Instruction mActiveInstruction;
float mPendingActivationTime = 0;
Instruction mPendingInstruction;
private CinemachineBlend mActiveBlend = null;
void InvalidateListOfChildren() { m_ChildCameras = null; LiveChild = null; }
void UpdateListOfChildren()
{
if (m_ChildCameras != null && mInstructionDictionary != null && mStateParentLookup != null)
return;
List list = new List();
CinemachineVirtualCameraBase[] kids = GetComponentsInChildren(true);
foreach (CinemachineVirtualCameraBase k in kids)
if (k.transform.parent == transform)
list.Add(k);
m_ChildCameras = list.ToArray();
ValidateInstructions();
}
private Dictionary mInstructionDictionary;
private Dictionary mStateParentLookup;
/// Internal API for the inspector editor.
internal void ValidateInstructions()
{
if (m_Instructions == null)
m_Instructions = Array.Empty();
mInstructionDictionary = new Dictionary();
for (int i = 0; i < m_Instructions.Length; ++i)
{
if (m_Instructions[i].m_VirtualCamera != null
&& m_Instructions[i].m_VirtualCamera.transform.parent != transform)
{
m_Instructions[i].m_VirtualCamera = null;
}
mInstructionDictionary[m_Instructions[i].m_FullHash] = i;
}
// Create the parent lookup
mStateParentLookup = new Dictionary();
if (m_ParentHash != null)
foreach (var i in m_ParentHash)
mStateParentLookup[i.m_Hash] = i.m_ParentHash;
// Zap the cached current instructions
mHashCache = null;
mActivationTime = mPendingActivationTime = 0;
mActiveBlend = null;
}
List m_clipInfoList = new List();
private CinemachineVirtualCameraBase ChooseCurrentCamera()
{
if (m_ChildCameras == null || m_ChildCameras.Length == 0)
{
mActivationTime = 0;
return null;
}
CinemachineVirtualCameraBase defaultCam = m_ChildCameras[0];
if (m_AnimatedTarget == null || !m_AnimatedTarget.gameObject.activeSelf
|| m_AnimatedTarget.runtimeAnimatorController == null
|| m_LayerIndex < 0 || !m_AnimatedTarget.hasBoundPlayables
|| m_LayerIndex >= m_AnimatedTarget.layerCount)
{
mActivationTime = 0;
return defaultCam;
}
// Get the current state
int hash;
if (m_AnimatedTarget.IsInTransition(m_LayerIndex))
{
// Force "current" state to be the state we're transitioning to
AnimatorStateInfo info = m_AnimatedTarget.GetNextAnimatorStateInfo(m_LayerIndex);
m_AnimatedTarget.GetNextAnimatorClipInfo(m_LayerIndex, m_clipInfoList);
hash = GetClipHash(info.fullPathHash, m_clipInfoList);
}
else
{
AnimatorStateInfo info = m_AnimatedTarget.GetCurrentAnimatorStateInfo(m_LayerIndex);
m_AnimatedTarget.GetCurrentAnimatorClipInfo(m_LayerIndex, m_clipInfoList);
hash = GetClipHash(info.fullPathHash, m_clipInfoList);
}
// If we don't have an instruction for this state, find a suitable default
while (hash != 0 && !mInstructionDictionary.ContainsKey(hash))
hash = mStateParentLookup.ContainsKey(hash) ? mStateParentLookup[hash] : 0;
float now = CinemachineCore.CurrentTime;
if (mActivationTime != 0)
{
// Is it active now?
if (mActiveInstruction.m_FullHash == hash)
{
// Yes, cancel any pending
mPendingActivationTime = 0;
return mActiveInstruction.m_VirtualCamera;
}
// Is it pending?
if (PreviousStateIsValid)
{
if (mPendingActivationTime != 0 && mPendingInstruction.m_FullHash == hash)
{
// Has it been pending long enough, and are we allowed to switch away
// from the active action?
if ((now - mPendingActivationTime) > mPendingInstruction.m_ActivateAfter
&& ((now - mActivationTime) > mActiveInstruction.m_MinDuration
|| mPendingInstruction.m_VirtualCamera.Priority
> mActiveInstruction.m_VirtualCamera.Priority))
{
// Yes, activate it now
mActiveInstruction = mPendingInstruction;
mActivationTime = now;
mPendingActivationTime = 0;
}
return mActiveInstruction.m_VirtualCamera;
}
}
}
// Neither active nor pending.
mPendingActivationTime = 0; // cancel the pending, if any
if (!mInstructionDictionary.ContainsKey(hash))
{
// No defaults set, we just ignore this state
if (mActivationTime != 0)
return mActiveInstruction.m_VirtualCamera;
return defaultCam;
}
// Can we activate it now?
Instruction newInstr = m_Instructions[mInstructionDictionary[hash]];
if (newInstr.m_VirtualCamera == null)
newInstr.m_VirtualCamera = defaultCam;
if (PreviousStateIsValid && mActivationTime > 0)
{
if (newInstr.m_ActivateAfter > 0
|| ((now - mActivationTime) < mActiveInstruction.m_MinDuration
&& newInstr.m_VirtualCamera.Priority
<= mActiveInstruction.m_VirtualCamera.Priority))
{
// Too early - make it pending
mPendingInstruction = newInstr;
mPendingActivationTime = now;
if (mActivationTime != 0)
return mActiveInstruction.m_VirtualCamera;
return defaultCam;
}
}
// Activate now
mActiveInstruction = newInstr;
mActivationTime = now;
return mActiveInstruction.m_VirtualCamera;
}
int GetClipHash(int hash, List clips)
{
// Find the strongest-weighted animation clip substate
int bestClip = -1;
for (int i = 0; i < clips.Count; ++i)
if (bestClip < 0 || clips[i].weight > clips[bestClip].weight)
bestClip = i;
// Use its hash
if (bestClip >= 0 && clips[bestClip].weight > 0)
hash = LookupFakeHash(hash, clips[bestClip].clip);
return hash;
}
private CinemachineBlendDefinition LookupBlend(
ICinemachineCamera fromKey, ICinemachineCamera toKey)
{
// Get the blend curve that's most appropriate for these cameras
CinemachineBlendDefinition blend = m_DefaultBlend;
if (m_CustomBlends != null)
{
string fromCameraName = (fromKey != null) ? fromKey.Name : string.Empty;
string toCameraName = (toKey != null) ? toKey.Name : string.Empty;
blend = m_CustomBlends.GetBlendForVirtualCameras(
fromCameraName, toCameraName, blend);
}
if (CinemachineCore.GetBlendOverride != null)
blend = CinemachineCore.GetBlendOverride(fromKey, toKey, blend, this);
return blend;
}
}
#endif
}