2021-06-25 07:50:12 -04:00

247 lines
9.7 KiB
C#

#if !UNITY_2019_3_OR_NEWER
#define CINEMACHINE_PHYSICS
#endif
using UnityEngine;
using Cinemachine.Utility;
namespace Cinemachine
{
/// <summary>
/// Third-person follower, with complex pivoting: horizontal about the origin,
/// vertical about the shoulder.
/// </summary>
[AddComponentMenu("")] // Don't display in add component menu
[SaveDuringPlay]
public class Cinemachine3rdPersonFollow : CinemachineComponentBase
{
/// <summary>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.</summary>
[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;
/// <summary>Position of the shoulder pivot relative to the Follow target origin.
/// This offset is in target-local space.</summary>
[Header("Rig")]
[Tooltip("Position of the shoulder pivot relative to the Follow target origin. "
+ "This offset is in target-local space")]
public Vector3 ShoulderOffset;
/// <summary>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.</summary>
[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;
/// <summary>Specifies which shoulder (left, right, or in-between) the camera is on.</summary>
[Tooltip("Specifies which shoulder (left, right, or in-between) the camera is on")]
[Range(0, 1)]
public float CameraSide;
/// <summary>How far baehind the hand the camera will be placed.</summary>
[Tooltip("How far baehind the hand the camera will be placed")]
public float CameraDistance;
#if CINEMACHINE_PHYSICS
/// <summary>Camera will avoid obstacles on these layers.</summary>
[Header("Obstacles")]
[Tooltip("Camera will avoid obstacles on these layers")]
public LayerMask CameraCollisionFilter;
/// <summary>
/// Obstacles with this tag will be ignored. It is a good idea
/// to set this field to the target's tag
/// </summary>
[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;
/// <summary>
/// Specifies how close the camera can get to obstacles
/// </summary>
[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;
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 = 1;
CameraRadius = 0.2f;
#else
CameraRadius = 0.02f;
#endif
}
#if CINEMACHINE_PHYSICS
void OnDestroy()
{
RuntimeUtility.DestroyScratchCollider();
}
#endif
/// <summary>True if component is enabled and has a Follow target defined</summary>
public override bool IsValid => enabled && FollowTarget != null;
/// <summary>Get the Cinemachine Pipeline stage that this component implements.
/// Always returns the Aim stage</summary>
public override CinemachineCore.Stage Stage { get { return CinemachineCore.Stage.Body; } }
/// <summary>
/// Report maximum damping time needed for this component.
/// </summary>
/// <returns>Highest damping setting in this component</returns>
public override float GetMaxDampTime()
{
return Mathf.Max(Damping.x, Mathf.Max(Damping.y, Damping.z));
}
/// <summary>Orients the camera to match the Follow target's orientation</summary>
/// <param name="curState">The current camera state</param>
/// <param name="deltaTime">Elapsed time since last frame, for damping calculations.
/// If negative, previous state is reset.</param>
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);
camPos = ResolveCollisions(collidedHand, camPos, CameraRadius);
// Set state
curState.RawPosition = camPos;
curState.RawOrientation = targetRot;
curState.ReferenceUp = up;
}
/// <summary>
/// Internal use only. Public for the inspector gizmo
/// </summary>
/// <param name="root">Root of the rig.</param>
/// <param name="shoulder">Shoulder of the rig.</param>
/// <param name="hand">Hand of the rig.</param>
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);
var heading = UnityVectorExtensions.SignedAngle(Vector3.forward, planeForward, up);
return Quaternion.AngleAxis(heading, 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 += m_DampingCorrection;
shoulder = root + heading * shoulderOffset;
hand = shoulder + targetRot * new Vector3(0, VerticalArmLength, 0);
}
Vector3 ResolveCollisions(Vector3 root, Vector3 tip, float cameraRadius)
{
#if CINEMACHINE_PHYSICS
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
}
}
}