#if !UNITY_2019_3_OR_NEWER #define CINEMACHINE_PHYSICS #endif using UnityEngine; using Cinemachine.Utility; namespace Cinemachine { /// /// Third-person follower, with complex pivoting: horizontal about the origin, /// vertical about the shoulder. /// [AddComponentMenu("")] // Don't display in add component menu [SaveDuringPlay] public class Cinemachine3rdPersonFollow : CinemachineComponentBase { /// How responsively the camera tracks the target. Each axis (camera-local) /// can have its own setting. Value is the approximate time it takes the camera /// to catch up to the target's new position. Smaller values give a more rigid /// effect, larger values give a squishier one. [Tooltip("How responsively the camera tracks the target. Each axis (camera-local) " + "can have its own setting. Value is the approximate time it takes the camera " + "to catch up to the target's new position. Smaller values give a more " + "rigid effect, larger values give a squishier one")] public Vector3 Damping; /// Position of the shoulder pivot relative to the Follow target origin. /// This offset is in target-local space. [Header("Rig")] [Tooltip("Position of the shoulder pivot relative to the Follow target origin. " + "This offset is in target-local space")] public Vector3 ShoulderOffset; /// Vertical offset of the hand in relation to the shoulder. /// Arm length will affect the follow target's screen position /// when the camera rotates vertically. [Tooltip("Vertical offset of the hand in relation to the shoulder. " + "Arm length will affect the follow target's screen position when " + "the camera rotates vertically")] public float VerticalArmLength; /// Specifies which shoulder (left, right, or in-between) the camera is on. [Tooltip("Specifies which shoulder (left, right, or in-between) the camera is on")] [Range(0, 1)] public float CameraSide; /// How far baehind the hand the camera will be placed. [Tooltip("How far baehind the hand the camera will be placed")] public float CameraDistance; #if CINEMACHINE_PHYSICS /// Camera will avoid obstacles on these layers. [Header("Obstacles")] [Tooltip("Camera will avoid obstacles on these layers")] public LayerMask CameraCollisionFilter; /// /// Obstacles with this tag will be ignored. It is a good idea /// to set this field to the target's tag /// [TagField] [Tooltip("Obstacles with this tag will be ignored. " + "It is a good idea to set this field to the target's tag")] public string IgnoreTag = string.Empty; /// /// Specifies how close the camera can get to obstacles /// [Tooltip("Specifies how close the camera can get to obstacles")] [Range(0, 1)] public float CameraRadius; #else // Keep here for code simplicity internal float CameraRadius; #endif // State info Vector3 m_PreviousFollowTargetPosition; Vector3 m_DampingCorrection; // this is in local rig space void OnValidate() { CameraSide = Mathf.Clamp(CameraSide, -1.0f, 1.0f); Damping.x = Mathf.Max(0, Damping.x); Damping.y = Mathf.Max(0, Damping.y); Damping.z = Mathf.Max(0, Damping.z); CameraRadius = Mathf.Max(0.001f, CameraRadius); } void Reset() { ShoulderOffset = new Vector3(0.5f, -0.4f, 0.0f); VerticalArmLength = 0.4f; CameraSide = 1.0f; CameraDistance = 2.0f; Damping = new Vector3(0.1f, 0.5f, 0.3f); #if CINEMACHINE_PHYSICS CameraCollisionFilter = 0; CameraRadius = 0.2f; #else CameraRadius = 0.02f; #endif } #if CINEMACHINE_PHYSICS void OnDestroy() { RuntimeUtility.DestroyScratchCollider(); } #endif /// True if component is enabled and has a Follow target defined public override bool IsValid => enabled && FollowTarget != null; /// Get the Cinemachine Pipeline stage that this component implements. /// Always returns the Aim stage public override CinemachineCore.Stage Stage { get { return CinemachineCore.Stage.Body; } } /// /// Report maximum damping time needed for this component. /// /// Highest damping setting in this component public override float GetMaxDampTime() { return Mathf.Max(Damping.x, Mathf.Max(Damping.y, Damping.z)); } /// Orients the camera to match the Follow target's orientation /// The current camera state /// Elapsed time since last frame, for damping calculations. /// If negative, previous state is reset. public override void MutateCameraState(ref CameraState curState, float deltaTime) { if (IsValid) { if (!VirtualCamera.PreviousStateIsValid) deltaTime = -1; PositionCamera(ref curState, deltaTime); } } void PositionCamera(ref CameraState curState, float deltaTime) { var up = curState.ReferenceUp; var targetPos = FollowTargetPosition; var targetRot = FollowTargetRotation; var targetForward = targetRot * Vector3.forward; var heading = GetHeading(targetForward, up); if (deltaTime < 0) { // No damping - reset damping state info m_DampingCorrection = Vector3.zero; } else { // Damping correction is applied to the shoulder offset - stretching the rig m_DampingCorrection += Quaternion.Inverse(heading) * (m_PreviousFollowTargetPosition - targetPos); m_DampingCorrection -= VirtualCamera.DetachedFollowTargetDamp(m_DampingCorrection, Damping, deltaTime); } m_PreviousFollowTargetPosition = targetPos; var root = targetPos; GetRawRigPositions(root, targetRot, heading, out _, out Vector3 hand); // Check if hand is colliding with something, if yes, then move the hand // closer to the player. The radius is slightly enlarged, to avoid problems // next to walls var collidedHand = ResolveCollisions(root, hand, CameraRadius * 1.05f); // Place the camera at the correct distance from the hand Vector3 camPos = hand - (targetForward * (CameraDistance - m_DampingCorrection.z)); camPos = ResolveCollisions(collidedHand, camPos, CameraRadius); // Set state curState.RawPosition = camPos; curState.RawOrientation = targetRot; // not necessary, but left in to avoid breaking scenes that depend on this curState.ReferenceUp = up; } /// /// Internal use only. Public for the inspector gizmo /// /// Root of the rig. /// Shoulder of the rig. /// Hand of the rig. public void GetRigPositions(out Vector3 root, out Vector3 shoulder, out Vector3 hand) { var up = VirtualCamera.State.ReferenceUp; var targetRot = FollowTargetRotation; var targetForward = targetRot * Vector3.forward; var heading = GetHeading(targetForward, up); root = m_PreviousFollowTargetPosition; GetRawRigPositions(root, targetRot, heading, out shoulder, out hand); hand = ResolveCollisions(root, hand, CameraRadius * 1.05f); } Quaternion GetHeading(Vector3 targetForward, Vector3 up) { var planeForward = targetForward.ProjectOntoPlane(up); planeForward = Vector3.Cross(up, Vector3.Cross(planeForward, up)); return Quaternion.LookRotation(planeForward, up); } void GetRawRigPositions( Vector3 root, Quaternion targetRot, Quaternion heading, out Vector3 shoulder, out Vector3 hand) { var shoulderPivotReflected = Vector3.Reflect(ShoulderOffset, Vector3.right); var shoulderOffset = Vector3.Lerp(shoulderPivotReflected, ShoulderOffset, CameraSide); shoulderOffset.x += m_DampingCorrection.x; shoulderOffset.y += m_DampingCorrection.y; shoulder = root + heading * shoulderOffset; hand = shoulder + targetRot * new Vector3(0, VerticalArmLength, 0); } Vector3 ResolveCollisions(Vector3 root, Vector3 tip, float cameraRadius) { #if CINEMACHINE_PHYSICS if (CameraCollisionFilter.value == 0) { return tip; } var dir = tip - root; var len = dir.magnitude; dir /= len; var result = tip; float desiredCorrection = 0; if (RuntimeUtility.SphereCastIgnoreTag( root, cameraRadius, dir, out RaycastHit hitInfo, len, CameraCollisionFilter, IgnoreTag)) { var desiredResult = hitInfo.point + hitInfo.normal * cameraRadius; desiredCorrection = (desiredResult - tip).magnitude; } // Apply the correction if (desiredCorrection > Epsilon) result -= dir * desiredCorrection; return result; #else return tip; #endif } } }