using UnityEngine; using Cinemachine.Utility; using UnityEngine.Serialization; using System; namespace Cinemachine { /// /// A Cinemachine Camera geared towards a 3rd person camera experience. /// The camera orbits around its subject with three separate camera rigs defining /// rings around the target. Each rig has its own radius, height offset, composer, /// and lens settings. /// Depending on the camera's position along the spline connecting these three rigs, /// these settings are interpolated to give the final camera position and state. /// [DocumentationSorting(11, DocumentationSortingAttribute.Level.UserRef)] [ExecuteInEditMode, DisallowMultipleComponent] [AddComponentMenu("Cinemachine/CinemachineFreeLook")] public class CinemachineFreeLook : CinemachineVirtualCameraBase { /// Object for the camera children to look at (the aim target) [Tooltip("Object for the camera children to look at (the aim target).")] [NoSaveDuringPlay] public Transform m_LookAt = null; /// Object for the camera children wants to move with (the body target) [Tooltip("Object for the camera children wants to move with (the body target).")] [NoSaveDuringPlay] public Transform m_Follow = null; /// If enabled, this lens setting will apply to all three child rigs, otherwise the child rig lens settings will be used [Tooltip("If enabled, this lens setting will apply to all three child rigs, otherwise the child rig lens settings will be used")] [FormerlySerializedAs("m_UseCommonLensSetting")] public bool m_CommonLens = true; /// Specifies the lens properties of this Virtual Camera. /// This generally mirrors the Unity Camera's lens settings, and will be used to drive /// the Unity camera when the vcam is active [FormerlySerializedAs("m_LensAttributes")] [Tooltip("Specifies the lens properties of this Virtual Camera. This generally mirrors the Unity Camera's lens settings, and will be used to drive the Unity camera when the vcam is active")] [LensSettingsProperty] public LensSettings m_Lens = LensSettings.Default; /// The Vertical axis. Value is 0..1. Chooses how to blend the child rigs [Header("Axis Control")] [Tooltip("The Vertical axis. Value is 0..1. Chooses how to blend the child rigs")] public AxisState m_YAxis = new AxisState(2f, 0.2f, 0.1f, 0.5f, "Mouse Y", false); /// The Horizontal axis. Value is 0..359. This is passed on to the rigs' OrbitalTransposer component [Tooltip("The Horizontal axis. Value is 0..359. This is passed on to the rigs' OrbitalTransposer component")] public AxisState m_XAxis = new AxisState(300f, 0.1f, 0.1f, 0f, "Mouse X", true); /// The definition of Forward. Camera will follow behind [Tooltip("The definition of Forward. Camera will follow behind.")] public CinemachineOrbitalTransposer.Heading m_Heading = new CinemachineOrbitalTransposer.Heading( CinemachineOrbitalTransposer.Heading.HeadingDefinition.TargetForward, 4, 0); /// Controls how automatic recentering of the X axis is accomplished [Tooltip("Controls how automatic recentering of the X axis is accomplished")] public CinemachineOrbitalTransposer.Recentering m_RecenterToTargetHeading = new CinemachineOrbitalTransposer.Recentering(false, 1, 2); /// The coordinate space to use when interpreting the offset from the target [Header("Orbits")] [Tooltip("The coordinate space to use when interpreting the offset from the target. This is also used to set the camera's Up vector, which will be maintained when aiming the camera.")] public CinemachineOrbitalTransposer.BindingMode m_BindingMode = CinemachineOrbitalTransposer.BindingMode.SimpleFollowWithWorldUp; /// [Tooltip("Controls how taut is the line that connects the rigs' orbits, which determines final placement on the Y axis")] [Range(0f, 1f)] [FormerlySerializedAs("m_SplineTension")] public float m_SplineCurvature = 0.2f; /// Defines the height and radius of the Rig orbit [Serializable] public struct Orbit { /// Height relative to target public float m_Height; /// Radius of orbit public float m_Radius; /// Constructor with specific values public Orbit(float h, float r) { m_Height = h; m_Radius = r; } } /// The radius and height of the three orbiting rigs [Tooltip("The radius and height of the three orbiting rigs.")] public Orbit[] m_Orbits = new Orbit[3] { // These are the default orbits new Orbit(4.5f, 1.75f), new Orbit(2.5f, 3f), new Orbit(0.4f, 1.3f) }; // Legacy support [SerializeField] [HideInInspector] [FormerlySerializedAs("m_HeadingBias")] private float m_LegacyHeadingBias = float.MaxValue; bool mUseLegacyRigDefinitions = false; /// Enforce bounds for fields, when changed in inspector. protected override void OnValidate() { base.OnValidate(); // Upgrade after a legacy deserialize if (m_LegacyHeadingBias != float.MaxValue) { m_Heading.m_HeadingBias = m_LegacyHeadingBias; m_LegacyHeadingBias = float.MaxValue; m_RecenterToTargetHeading.LegacyUpgrade( ref m_Heading.m_HeadingDefinition, ref m_Heading.m_VelocityFilterStrength); mUseLegacyRigDefinitions = true; } m_YAxis.Validate(); m_XAxis.Validate(); m_RecenterToTargetHeading.Validate(); m_Lens.Validate(); InvalidateRigCache(); } /// Get a child rig /// Rig index. Can be 0, 1, or 2 /// The rig, or null if index is bad. public CinemachineVirtualCamera GetRig(int i) { UpdateRigCache(); return (i < 0 || i > 2) ? null : m_Rigs[i]; } /// Names of the 3 child rigs public static string[] RigNames { get { return new string[] { "TopRig", "MiddleRig", "BottomRig" }; } } bool mIsDestroyed = false; /// Updates the child rig cache protected override void OnEnable() { mIsDestroyed = false; base.OnEnable(); InvalidateRigCache(); } /// Makes sure that the child rigs get destroyed in an undo-firndly manner. /// Invalidates the rig cache. protected override void OnDestroy() { // Make the rigs visible instead of destroying - this is to keep Undo happy if (m_Rigs != null) foreach (var rig in m_Rigs) if (rig != null && rig.gameObject != null) rig.gameObject.hideFlags &= ~(HideFlags.HideInHierarchy | HideFlags.HideInInspector); mIsDestroyed = true; base.OnDestroy(); } /// Invalidates the rig cache void OnTransformChildrenChanged() { InvalidateRigCache(); } void Reset() { DestroyRigs(); } /// The cacmera state, which will be a blend of the child rig states override public 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; } } /// Returns the rig with the greatest weight public override ICinemachineCamera LiveChildOrSelf { get { // Do not update the rig cache here or there will be infinite loop at creation time if (m_Rigs == null || m_Rigs.Length != 3) return this; if (m_YAxis.Value < 0.33f) return m_Rigs[2]; if (m_YAxis.Value > 0.66f) return m_Rigs[0]; return m_Rigs[1]; } } /// Check whether the vcam a live child of this camera. /// Returns true if the child is currently contributing actively to the camera state. /// The Virtual Camera to check /// True if the vcam is currently actively influencing the state of this vcam public override bool IsLiveChild(ICinemachineCamera vcam) { // Do not update the rig cache here or there will be infinite loop at creation time if (m_Rigs == null || m_Rigs.Length != 3) return false; if (m_YAxis.Value < 0.33f) return vcam == (ICinemachineCamera)m_Rigs[2]; if (m_YAxis.Value > 0.66f) return vcam == (ICinemachineCamera)m_Rigs[0]; return vcam == (ICinemachineCamera)m_Rigs[1]; } /// Remove a Pipeline stage hook callback. /// Make sure it is removed from all the children. /// The delegate to remove. public override void RemovePostPipelineStageHook(OnPostPipelineStageDelegate d) { base.RemovePostPipelineStageHook(d); UpdateRigCache(); if (m_Rigs != null) foreach (var vcam in m_Rigs) if (vcam != null) vcam.RemovePostPipelineStageHook(d); } /// Called by CinemachineCore at designated update time /// so the vcam can position itself and track its targets. All 3 child rigs are updated, /// and a blend calculated, depending on the value of the Y axis. /// Default world Up, set by the CinemachineBrain /// Delta time for time-based effects (ignore if less than 0) override public void UpdateCameraState(Vector3 worldUp, float deltaTime) { //UnityEngine.Profiling.Profiler.BeginSample("CinemachineFreeLook.UpdateCameraState"); if (!PreviousStateIsValid) deltaTime = -1; UpdateRigCache(); // Reset the base camera state, in case the game object got moved in the editor if (deltaTime < 0) m_State = PullStateFromVirtualCamera(worldUp); // Not in gameplay // Update the current state by invoking the component pipeline m_State = CalculateNewState(worldUp, deltaTime); // Push the raw position back to the game object's transform, so it // moves along with the camera. Leave the orientation alone, because it // screws up camera dragging when there is a LookAt behaviour. if (Follow != null) { Vector3 delta = State.RawPosition - transform.position; transform.position = State.RawPosition; m_Rigs[0].transform.position -= delta; m_Rigs[1].transform.position -= delta; m_Rigs[2].transform.position -= delta; } PreviousStateIsValid = true; // Set up for next frame bool activeCam = (deltaTime >= 0) || CinemachineCore.Instance.IsLive(this); if (activeCam) m_YAxis.Update(deltaTime); PushSettingsToRigs(); //UnityEngine.Profiling.Profiler.EndSample(); } /// If we are transitioning from another FreeLook, grab the axis values from it. /// 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); if ((fromCam != null) && (fromCam is CinemachineFreeLook)) { CinemachineFreeLook freeLookFrom = fromCam as CinemachineFreeLook; if (freeLookFrom.Follow == Follow) { m_XAxis.Value = freeLookFrom.m_XAxis.Value; m_YAxis.Value = freeLookFrom.m_YAxis.Value; UpdateCameraState(worldUp, deltaTime); } } } CameraState m_State = CameraState.Default; // Current state this frame /// Serialized in order to support copy/paste [SerializeField][HideInInspector][NoSaveDuringPlay] private CinemachineVirtualCamera[] m_Rigs = new CinemachineVirtualCamera[3]; void InvalidateRigCache() { mOrbitals = null; } CinemachineOrbitalTransposer[] mOrbitals = null; CinemachineBlend mBlendA; CinemachineBlend mBlendB; /// /// Override component pipeline creation. /// This needs to be done by the editor to support Undo. /// The override must do exactly the same thing as the CreatePipeline method in this class. /// public static CreateRigDelegate CreateRigOverride; /// /// Override component pipeline creation. /// This needs to be done by the editor to support Undo. /// The override must do exactly the same thing as the CreatePipeline method in this class. /// public delegate CinemachineVirtualCamera CreateRigDelegate( CinemachineFreeLook vcam, string name, CinemachineVirtualCamera copyFrom); /// /// Override component pipeline destruction. /// This needs to be done by the editor to support Undo. /// public static DestroyRigDelegate DestroyRigOverride; /// /// Override component pipeline destruction. /// This needs to be done by the editor to support Undo. /// public delegate void DestroyRigDelegate(GameObject rig); private void DestroyRigs() { CinemachineVirtualCamera[] oldRigs = new CinemachineVirtualCamera[RigNames.Length]; for (int i = 0; i < RigNames.Length; ++i) { foreach (Transform child in transform) if (child.gameObject.name == RigNames[i]) oldRigs[i] = child.GetComponent(); } for (int i = 0; i < oldRigs.Length; ++i) { if (oldRigs[i] != null) { if (DestroyRigOverride != null) DestroyRigOverride(oldRigs[i].gameObject); else Destroy(oldRigs[i].gameObject); } } m_Rigs = null; mOrbitals = null; } private CinemachineVirtualCamera[] CreateRigs(CinemachineVirtualCamera[] copyFrom) { // Invalidate the cache mOrbitals = null; float[] softCenterDefaultsV = new float[] { 0.5f, 0.55f, 0.6f }; CinemachineVirtualCamera[] newRigs = new CinemachineVirtualCamera[3]; for (int i = 0; i < RigNames.Length; ++i) { CinemachineVirtualCamera src = null; if (copyFrom != null && copyFrom.Length > i) src = copyFrom[i]; if (CreateRigOverride != null) newRigs[i] = CreateRigOverride(this, RigNames[i], src); else { // Create a new rig with default components GameObject go = new GameObject(RigNames[i]); go.transform.parent = transform; newRigs[i] = go.AddComponent(); if (src != null) ReflectionHelpers.CopyFields(src, newRigs[i]); else { go = newRigs[i].GetComponentOwner().gameObject; go.AddComponent(); go.AddComponent(); } } // Set up the defaults newRigs[i].InvalidateComponentPipeline(); CinemachineOrbitalTransposer orbital = newRigs[i].GetCinemachineComponent(); if (orbital == null) orbital = newRigs[i].AddCinemachineComponent(); // should not happen if (src == null) { // Only set defaults if not copying orbital.m_YawDamping = 0; CinemachineComposer composer = newRigs[i].GetCinemachineComponent(); if (composer != null) { composer.m_HorizontalDamping = composer.m_VerticalDamping = 0; composer.m_ScreenX = 0.5f; composer.m_ScreenY = softCenterDefaultsV[i]; composer.m_DeadZoneWidth = composer.m_DeadZoneHeight = 0.1f; composer.m_SoftZoneWidth = composer.m_SoftZoneHeight = 0.8f; composer.m_BiasX = composer.m_BiasY = 0; } } } return newRigs; } private void UpdateRigCache() { //UnityEngine.Profiling.Profiler.BeginSample("CinemachineFreeLook.UpdateRigCache"); if (mIsDestroyed) { //UnityEngine.Profiling.Profiler.EndSample(); return; } // Special condition: Did we just get copy/pasted? if (m_Rigs != null && m_Rigs.Length == 3 && m_Rigs[0] != null && m_Rigs[0].transform.parent != transform) { DestroyRigs(); m_Rigs = CreateRigs(m_Rigs); } // Early out if we're up to date if (mOrbitals != null && mOrbitals.Length == 3) { //UnityEngine.Profiling.Profiler.EndSample(); return; } // Locate existing rigs, and recreate them if any are missing if (LocateExistingRigs(RigNames, false) != 3) { DestroyRigs(); m_Rigs = CreateRigs(null); LocateExistingRigs(RigNames, true); } foreach (var rig in m_Rigs) { // Configure the UI rig.m_ExcludedPropertiesInInspector = m_CommonLens ? new string[] { "m_Script", "Header", "Extensions", "m_Priority", "m_Follow", "m_Lens" } : new string[] { "m_Script", "Header", "Extensions", "m_Priority", "m_Follow" }; rig.m_LockStageInInspector = new CinemachineCore.Stage[] { CinemachineCore.Stage.Body }; } // Create the blend objects mBlendA = new CinemachineBlend(m_Rigs[1], m_Rigs[0], AnimationCurve.Linear(0, 0, 1, 1), 1, 0); mBlendB = new CinemachineBlend(m_Rigs[2], m_Rigs[1], AnimationCurve.Linear(0, 0, 1, 1), 1, 0); // Horizontal rotation clamped to [0,360] (with wraparound) m_XAxis.SetThresholds(0f, 360f, true); // Vertical rotation cleamped to [0,1] as it is a t-value for the // catmull-rom spline going through the 3 points on the rig m_YAxis.SetThresholds(0f, 1f, false); //UnityEngine.Profiling.Profiler.EndSample(); } private int LocateExistingRigs(string[] rigNames, bool forceOrbital) { mOrbitals = new CinemachineOrbitalTransposer[rigNames.Length]; m_Rigs = new CinemachineVirtualCamera[rigNames.Length]; int rigsFound = 0; foreach (Transform child in transform) { CinemachineVirtualCamera vcam = child.GetComponent(); if (vcam != null) { GameObject go = child.gameObject; for (int i = 0; i < rigNames.Length; ++i) { if (mOrbitals[i] == null && go.name == rigNames[i]) { // Must have an orbital transposer or it's no good mOrbitals[i] = vcam.GetCinemachineComponent(); if (mOrbitals[i] == null && forceOrbital) mOrbitals[i] = vcam.AddCinemachineComponent(); if (mOrbitals[i] != null) { mOrbitals[i].m_HeadingIsSlave = true; if (i == 0) mOrbitals[i].HeadingUpdater = (CinemachineOrbitalTransposer orbital, float deltaTime, Vector3 up) => { return orbital.UpdateHeading(deltaTime, up, ref m_XAxis); }; m_Rigs[i] = vcam; ++rigsFound; } } } } } return rigsFound; } void PushSettingsToRigs() { //UnityEngine.Profiling.Profiler.BeginSample("CinemachineFreeLook.PushSettingsToRigs"); UpdateRigCache(); for (int i = 0; i < m_Rigs.Length; ++i) { //UnityEngine.Profiling.Profiler.BeginSample("CinemachineFreeLook.PushSettingsToRigs.m_Rigs[i] == null"); if (m_Rigs[i] == null) { //UnityEngine.Profiling.Profiler.EndSample(); continue; } //UnityEngine.Profiling.Profiler.EndSample(); //UnityEngine.Profiling.Profiler.BeginSample("CinemachineFreeLook.PushSettingsToRigs.m_CommonLens"); if (m_CommonLens) m_Rigs[i].m_Lens = m_Lens; //UnityEngine.Profiling.Profiler.EndSample(); // If we just deserialized from a legacy version, // pull the orbits and targets from the rigs //UnityEngine.Profiling.Profiler.BeginSample("CinemachineFreeLook.PushSettingsToRigs.mUseLegacyRigDefinitions"); if (mUseLegacyRigDefinitions) { mUseLegacyRigDefinitions = false; m_Orbits[i].m_Height = mOrbitals[i].m_FollowOffset.y; m_Orbits[i].m_Radius = -mOrbitals[i].m_FollowOffset.z; if (m_Rigs[i].Follow != null) Follow = m_Rigs[i].Follow; } m_Rigs[i].Follow = null; //UnityEngine.Profiling.Profiler.EndSample(); // Hide the rigs from prying eyes //UnityEngine.Profiling.Profiler.BeginSample("CinemachineFreeLook.PushSettingsToRigs.Hide the rigs"); if (CinemachineCore.sShowHiddenObjects) m_Rigs[i].gameObject.hideFlags &= ~(HideFlags.HideInHierarchy | HideFlags.HideInInspector); else m_Rigs[i].gameObject.hideFlags |= (HideFlags.HideInHierarchy | HideFlags.HideInInspector); //UnityEngine.Profiling.Profiler.EndSample(); //UnityEngine.Profiling.Profiler.BeginSample("CinemachineFreeLook.PushSettingsToRigs.Push"); mOrbitals[i].m_FollowOffset = GetLocalPositionForCameraFromInput(m_YAxis.Value); mOrbitals[i].m_BindingMode = m_BindingMode; mOrbitals[i].m_Heading = m_Heading; mOrbitals[i].m_XAxis = m_XAxis; mOrbitals[i].m_RecenterToTargetHeading = m_RecenterToTargetHeading; if (i > 0) mOrbitals[i].m_RecenterToTargetHeading.m_enabled = false; // Hack to get SimpleFollow with heterogeneous dampings to work if (m_BindingMode == CinemachineTransposer.BindingMode.SimpleFollowWithWorldUp) m_Rigs[i].SetStateRawPosition(State.RawPosition); //UnityEngine.Profiling.Profiler.EndSample(); } //UnityEngine.Profiling.Profiler.EndSample(); } private CameraState CalculateNewState(Vector3 worldUp, float deltaTime) { //UnityEngine.Profiling.Profiler.BeginSample("CinemachineFreeLook.CalculateNewState"); CameraState state = PullStateFromVirtualCamera(worldUp); // Blend from the appropriate rigs float t = m_YAxis.Value; if (t > 0.5f) { if (mBlendA != null) { mBlendA.TimeInBlend = (t - 0.5f) * 2f; mBlendA.UpdateCameraState(worldUp, deltaTime); state = mBlendA.State; } } else { if (mBlendB != null) { mBlendB.TimeInBlend = t * 2f; mBlendB.UpdateCameraState(worldUp, deltaTime); state = mBlendB.State; } } //UnityEngine.Profiling.Profiler.EndSample(); return state; } private CameraState PullStateFromVirtualCamera(Vector3 worldUp) { CameraState state = CameraState.Default; state.RawPosition = transform.position; state.RawOrientation = transform.rotation; state.ReferenceUp = worldUp; CinemachineBrain brain = CinemachineCore.Instance.FindPotentialTargetBrain(this); m_Lens.Aspect = brain != null ? brain.OutputCamera.aspect : 1; m_Lens.Orthographic = brain != null ? brain.OutputCamera.orthographic : false; state.Lens = m_Lens; return state; } /// /// Returns the local position of the camera along the spline used to connect the /// three camera rigs. Does not take into account the current heading of the /// camera (or its target) /// /// The t-value for the camera on its spline. Internally clamped to /// the value [0,1] /// The local offset (back + up) of the camera WRT its target based on the /// supplied t-value public Vector3 GetLocalPositionForCameraFromInput(float t) { //UnityEngine.Profiling.Profiler.BeginSample("CinemachineFreeLook.GetLocalPositionForCameraFromInput"); if (mOrbitals == null) { //UnityEngine.Profiling.Profiler.EndSample(); return Vector3.zero; } UpdateCachedSpline(); int n = 1; if (t > 0.5f) { t -= 0.5f; n = 2; } //UnityEngine.Profiling.Profiler.EndSample(); return SplineHelpers.Bezier3( t * 2f, m_CachedKnots[n], m_CachedCtrl1[n], m_CachedCtrl2[n], m_CachedKnots[n+1]); } Orbit[] m_CachedOrbits; float m_CachedTension; Vector4[] m_CachedKnots; Vector4[] m_CachedCtrl1; Vector4[] m_CachedCtrl2; void UpdateCachedSpline() { //UnityEngine.Profiling.Profiler.BeginSample("CinemachineFreeLook.UpdateCachedSpline"); bool cacheIsValid = (m_CachedOrbits != null && m_CachedTension == m_SplineCurvature); for (int i = 0; i < 3 && cacheIsValid; ++i) cacheIsValid = (m_CachedOrbits[i].m_Height == m_Orbits[i].m_Height && m_CachedOrbits[i].m_Radius == m_Orbits[i].m_Radius); if (!cacheIsValid) { float t = m_SplineCurvature; m_CachedKnots = new Vector4[5]; m_CachedCtrl1 = new Vector4[5]; m_CachedCtrl2 = new Vector4[5]; m_CachedKnots[1] = new Vector4(0, m_Orbits[2].m_Height, -m_Orbits[2].m_Radius, 0); m_CachedKnots[2] = new Vector4(0, m_Orbits[1].m_Height, -m_Orbits[1].m_Radius, 0); m_CachedKnots[3] = new Vector4(0, m_Orbits[0].m_Height, -m_Orbits[0].m_Radius, 0); m_CachedKnots[0] = Vector4.Lerp(m_CachedKnots[1], Vector4.zero, t); m_CachedKnots[4] = Vector4.Lerp(m_CachedKnots[3], Vector4.zero, t); SplineHelpers.ComputeSmoothControlPoints( ref m_CachedKnots, ref m_CachedCtrl1, ref m_CachedCtrl2); m_CachedOrbits = new Orbit[3]; for (int i = 0; i < 3; ++i) m_CachedOrbits[i] = m_Orbits[i]; m_CachedTension = m_SplineCurvature; } //UnityEngine.Profiling.Profiler.EndSample(); } } }