#if CINEMACHINE_UNITY_ANIMATION
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
namespace Unity.Cinemachine
{
///
/// 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.
///
[DisallowMultipleComponent]
[ExecuteAlways]
[ExcludeFromPreset]
[AddComponentMenu("Cinemachine/Cinemachine State Driven Camera")]
[HelpURL(Documentation.BaseURL + "manual/CinemachineStateDrivenCamera.html")]
public class CinemachineStateDrivenCamera : CinemachineCameraManagerBase
{
/// 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]
[FormerlySerializedAs("m_AnimatedTarget")]
public Animator AnimatedTarget;
/// Which layer in the target FSM to observe
[Tooltip("Which layer in the target state machine to observe")]
[NoSaveDuringPlay]
[FormerlySerializedAs("m_LayerIndex")]
public int LayerIndex;
/// This represents a single instruction 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")]
[FormerlySerializedAs("m_FullHash")]
public int FullHash;
/// The virtual camera to activate when the animation state becomes active
[Tooltip("The virtual camera to activate when the animation state becomes active")]
[FormerlySerializedAs("m_VirtualCamera")]
public CinemachineVirtualCameraBase Camera;
/// How long to wait (in seconds) before activating the camera.
/// This filters out very short state durations
[Tooltip("How long to wait (in seconds) before activating the camera. "
+ "This filters out very short state durations")]
[FormerlySerializedAs("m_ActivateAfter")]
public float ActivateAfter;
/// The minimum length of time (in seconds) to keep a camera active
[Tooltip("The minimum length of time (in seconds) to keep a camera active")]
[FormerlySerializedAs("m_MinDuration")]
public float 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 cameras with states. "
+ "These instructions are used to choose the live child at any given moment")]
[FormerlySerializedAs("m_Instructions")]
public Instruction[] Instructions;
/// Internal API for the Inspector editor. This implements nested states.
[Serializable]
internal struct ParentHash
{
/// Internal API for the Inspector editor
public int Hash;
/// Internal API for the Inspector editor
public int HashOfParent;
}
/// Internal API for the Inspector editor
[HideInInspector][SerializeField] internal ParentHash[] HashOfParent = null;
[SerializeField, HideInInspector, FormerlySerializedAs("m_LookAt")] Transform m_LegacyLookAt;
[SerializeField, HideInInspector, FormerlySerializedAs("m_Follow")] Transform m_LegacyFollow;
float m_ActivationTime = 0;
int m_ActiveInstructionIndex;
float m_PendingActivationTime = 0;
int m_PendingInstructionIndex;
Dictionary> m_InstructionDictionary;
Dictionary m_StateParentLookup;
readonly List m_ClipInfoList = new ();
///
protected override void Reset()
{
base.Reset();
Instructions = null;
AnimatedTarget = null;
LayerIndex = 0;
Instructions = null;
DefaultBlend = new (CinemachineBlendDefinition.Styles.EaseInOut, 0.5f);
CustomBlends = null;
}
protected internal override void PerformLegacyUpgrade(int streamedVersion)
{
base.PerformLegacyUpgrade(streamedVersion);
if (streamedVersion < 20220721)
{
if (m_LegacyLookAt != null || m_LegacyFollow != null)
{
DefaultTarget = new DefaultTargetSettings
{
Enabled = true,
Target = new CameraTarget
{
LookAtTarget = m_LegacyLookAt,
TrackingTarget = m_LegacyFollow,
CustomLookAtTarget = m_LegacyLookAt != m_LegacyFollow
}
};
m_LegacyLookAt = m_LegacyFollow = null;
}
}
}
/// 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
internal 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> m_HashCache;
int LookupFakeHash(int parentHash, AnimationClip clip)
{
m_HashCache ??= new Dictionary>();
if (!m_HashCache.TryGetValue(clip, out var list))
{
list = new List();
m_HashCache[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 });
m_StateParentLookup[newHash] = parentHash;
return newHash;
}
/// Internal API for the inspector editor.
internal void ValidateInstructions()
{
Instructions ??= Array.Empty();
m_InstructionDictionary = new Dictionary>();
for (int i = 0; i < Instructions.Length; ++i)
{
if (Instructions[i].Camera != null
&& Instructions[i].Camera.transform.parent != transform)
{
Instructions[i].Camera = null;
}
if (!m_InstructionDictionary.TryGetValue(Instructions[i].FullHash, out var list))
{
list = new List();
m_InstructionDictionary[Instructions[i].FullHash] = list;
}
list.Add(i);
}
// Create the parent lookup
m_StateParentLookup = new Dictionary();
for (int i = 0; HashOfParent != null && i < HashOfParent.Length; ++i)
m_StateParentLookup[HashOfParent[i].Hash] = HashOfParent[i].HashOfParent;
// Zap the cached current instructions
m_HashCache = null;
m_ActivationTime = m_PendingActivationTime = 0;
ResetLiveChild();
}
///
protected override CinemachineVirtualCameraBase ChooseCurrentCamera(Vector3 worldUp, float deltaTime)
{
if (!PreviousStateIsValid)
ValidateInstructions();
var children = ChildCameras;
if (children == null || children.Count == 0)
{
m_ActivationTime = 0;
return null;
}
var fallbackCam = children[0];
if (AnimatedTarget == null || !AnimatedTarget.gameObject.activeSelf
|| AnimatedTarget.runtimeAnimatorController == null
|| LayerIndex < 0 || !AnimatedTarget.hasBoundPlayables
|| LayerIndex >= AnimatedTarget.layerCount)
{
m_ActivationTime = 0;
return fallbackCam;
}
// quick sanity check, in case number of instructions changed
if (m_ActiveInstructionIndex < 0 || m_ActiveInstructionIndex >= Instructions.Length)
{
m_ActiveInstructionIndex = 0;
m_ActivationTime = 0;
}
if (!PreviousStateIsValid || m_PendingInstructionIndex < 0 || m_PendingInstructionIndex >= Instructions.Length)
{
m_PendingInstructionIndex = 0;
m_PendingActivationTime = 0;
}
// Get the current animation state
int hash;
if (AnimatedTarget.IsInTransition(LayerIndex))
{
// Force "current" state to be the state we're transitioning to
var info = AnimatedTarget.GetNextAnimatorStateInfo(LayerIndex);
AnimatedTarget.GetNextAnimatorClipInfo(LayerIndex, m_ClipInfoList);
hash = GetClipHash(info.fullPathHash, m_ClipInfoList);
}
else
{
var info = AnimatedTarget.GetCurrentAnimatorStateInfo(LayerIndex);
AnimatedTarget.GetCurrentAnimatorClipInfo(LayerIndex, m_ClipInfoList);
hash = GetClipHash(info.fullPathHash, m_ClipInfoList);
}
// If we don't have an instruction for this state, find a suitable default state
while (hash != 0 && !m_InstructionDictionary.ContainsKey(hash))
hash = m_StateParentLookup.ContainsKey(hash) ? m_StateParentLookup[hash] : 0;
// Get the highest-priority active camera from the instruction list
int newInstrIndex = -1;
if (m_InstructionDictionary.ContainsKey(hash))
{
var instrList = m_InstructionDictionary[hash];
// Find the instruction whose camera is active and has the highest priority
int bestPriority = int.MinValue;
for (int i = 0; i < instrList.Count; ++i)
{
var index = instrList[i];
var cam = Instructions[index].Camera;
if (cam != null && cam.isActiveAndEnabled && cam.Priority.Value > bestPriority)
{
newInstrIndex = index;
bestPriority = cam.Priority.Value;
}
}
}
// Process it. If no new camera is desired, we just ignore this state
var now = CinemachineCore.CurrentTime;
if (newInstrIndex >= 0)
{
// If it's neither active nor pending, we must take action
if (m_ActivationTime == 0)
{
// No current camera, actibvate immediately
m_ActiveInstructionIndex = newInstrIndex;
m_ActivationTime = now;
m_PendingActivationTime = 0;
}
else if (m_ActiveInstructionIndex != newInstrIndex
&& (m_PendingActivationTime == 0 || m_PendingInstructionIndex != newInstrIndex))
{
// Make it pending
m_PendingInstructionIndex = newInstrIndex;
m_PendingActivationTime = now;
}
}
// Process the pending instruction
if (m_PendingActivationTime != 0)
{
// Has it been pending long enough, and are we allowed to switch away
// from the active action?
if ((now - m_PendingActivationTime) > Instructions[m_PendingInstructionIndex].ActivateAfter
&& (now - m_ActivationTime) > Instructions[m_ActiveInstructionIndex].MinDuration)
{
// Yes, activate it now
m_ActiveInstructionIndex = m_PendingInstructionIndex;
m_ActivationTime = now;
m_PendingActivationTime = 0;
}
}
if (m_ActivationTime != 0)
return Instructions[m_ActiveInstructionIndex].Camera;
return fallbackCam;
}
int GetClipHash(int hash, List clips)
{
// Find the strongest-weighted animation clip sub-state
var bestClip = -1;
for (var 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;
}
///
/// Call this to cancel the current wait time for the pending instruction and activate
/// the pending instruction immediately.
///
public void CancelWait()
{
if (m_PendingActivationTime != 0 && m_PendingInstructionIndex >= 0 && m_PendingInstructionIndex < Instructions.Length)
{
m_ActiveInstructionIndex = m_PendingInstructionIndex;
m_ActivationTime = CinemachineCore.CurrentTime;
m_PendingActivationTime = 0;
}
}
}
}
#endif