using UnityEngine; namespace MegaKoop.Game { public class ThirdPersonCamera : MonoBehaviour { [Header("Target")] [SerializeField] private Transform target; [SerializeField] private Vector3 focusOffset = new Vector3(0f, 1.6f, 0f); [Header("Orbit")] [SerializeField] private float mouseSensitivity = 180f; [SerializeField] private float minPitch = -35f; [SerializeField] private float maxPitch = 75f; [SerializeField] private float rotationSmoothTime = 0.1f; [Header("Distance")] [SerializeField] private float distance = 5f; [SerializeField] private float minDistance = 2f; [SerializeField] private float maxDistance = 8f; [SerializeField] private float zoomSpeed = 4f; [SerializeField] private float distanceSmoothTime = 0.1f; [Header("Collision")] [SerializeField] private float obstructionRadius = 0.25f; [SerializeField] private LayerMask obstructionMask = ~0; [SerializeField] private float obstructionBuffer = 0.1f; [Header("Cursor")] [SerializeField] private bool lockCursor = true; private Vector2 orbitAngles = new Vector2(20f, 0f); private Vector2 currentOrbitAngles; private float pitchVelocity; private float yawVelocity; private float desiredDistance; private float distanceVelocity; private static readonly RaycastHit[] ObstructionHits = new RaycastHit[8]; private void OnEnable() { desiredDistance = Mathf.Clamp(distance, minDistance, maxDistance); distance = desiredDistance; currentOrbitAngles = orbitAngles; pitchVelocity = yawVelocity = 0f; if (lockCursor) { Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; } } private void OnDisable() { if (lockCursor) { Cursor.lockState = CursorLockMode.None; Cursor.visible = true; } } private void LateUpdate() { if (target == null) { return; } float deltaTime = Time.deltaTime; ReadOrbitInput(deltaTime); ReadZoomInput(); if (rotationSmoothTime <= 0f) { currentOrbitAngles = orbitAngles; pitchVelocity = yawVelocity = 0f; } else { currentOrbitAngles.x = Mathf.SmoothDamp(currentOrbitAngles.x, orbitAngles.x, ref pitchVelocity, rotationSmoothTime, Mathf.Infinity, deltaTime); currentOrbitAngles.y = Mathf.SmoothDamp(currentOrbitAngles.y, orbitAngles.y, ref yawVelocity, rotationSmoothTime, Mathf.Infinity, deltaTime); } Quaternion lookRotation = Quaternion.Euler(currentOrbitAngles.x, currentOrbitAngles.y, 0f); Vector3 focusPoint = target.TransformPoint(focusOffset); float smoothedDistance; if (distanceSmoothTime <= 0f) { distanceVelocity = 0f; smoothedDistance = desiredDistance; } else { smoothedDistance = Mathf.SmoothDamp(distance, desiredDistance, ref distanceVelocity, distanceSmoothTime, Mathf.Infinity, deltaTime); } float unobstructedDistance = smoothedDistance; float adjustedDistance = ResolveObstructions(focusPoint, lookRotation, unobstructedDistance); float finalDistance = Mathf.Min(unobstructedDistance, adjustedDistance); Vector3 finalPosition = focusPoint - lookRotation * Vector3.forward * finalDistance; transform.SetPositionAndRotation(finalPosition, lookRotation); distance = finalDistance; } private void ReadOrbitInput(float deltaTime) { float lookX = Input.GetAxis("Mouse X"); float lookY = Input.GetAxis("Mouse Y"); if (Mathf.Abs(lookX) > 0.0001f || Mathf.Abs(lookY) > 0.0001f) { orbitAngles.y += lookX * mouseSensitivity * deltaTime; orbitAngles.x -= lookY * mouseSensitivity * deltaTime; orbitAngles.x = Mathf.Clamp(orbitAngles.x, minPitch, maxPitch); } } private void ReadZoomInput() { float scroll = Input.GetAxis("Mouse ScrollWheel"); if (Mathf.Abs(scroll) > 0.0001f) { desiredDistance = Mathf.Clamp(desiredDistance - scroll * zoomSpeed, minDistance, maxDistance); } else { desiredDistance = Mathf.Clamp(desiredDistance, minDistance, maxDistance); } } private float ResolveObstructions(Vector3 focusPoint, Quaternion lookRotation, float targetDistance) { if (targetDistance <= 0.001f) { return targetDistance; } Vector3 direction = lookRotation * Vector3.back; int hitCount = Physics.SphereCastNonAlloc(focusPoint, obstructionRadius, direction, ObstructionHits, targetDistance, obstructionMask, QueryTriggerInteraction.Ignore); if (hitCount == 0) { return targetDistance; } float closestDistance = targetDistance; for (int i = 0; i < hitCount; i++) { RaycastHit hit = ObstructionHits[i]; if (hit.collider == null) { continue; } Transform hitTransform = hit.collider.transform; if (target != null && (hitTransform == target || hitTransform.IsChildOf(target))) { continue; } if (hit.distance < closestDistance) { closestDistance = Mathf.Max(0f, hit.distance - obstructionBuffer); } } return closestDistance; } public void SetTarget(Transform newTarget) { target = newTarget; } private void OnValidate() { minPitch = Mathf.Clamp(minPitch, -89f, 89f); maxPitch = Mathf.Clamp(maxPitch, -89f, 89f); if (maxPitch < minPitch) { float temp = maxPitch; maxPitch = minPitch; minPitch = temp; } mouseSensitivity = Mathf.Max(0f, mouseSensitivity); zoomSpeed = Mathf.Max(0f, zoomSpeed); obstructionRadius = Mathf.Max(0f, obstructionRadius); obstructionBuffer = Mathf.Clamp(obstructionBuffer, 0f, 1f); rotationSmoothTime = Mathf.Max(0f, rotationSmoothTime); distanceSmoothTime = Mathf.Max(0f, distanceSmoothTime); minDistance = Mathf.Max(0.1f, minDistance); maxDistance = Mathf.Max(minDistance, maxDistance); distance = Mathf.Clamp(distance, minDistance, maxDistance); desiredDistance = Mathf.Clamp(distance, minDistance, maxDistance); currentOrbitAngles = orbitAngles; pitchVelocity = yawVelocity = 0f; } } }