Files
ARPGCastleDefence/Assets/Scripts/Camera/PlayerCameraController.cs

247 lines
11 KiB
C#
Raw Permalink Normal View History

2026-03-31 17:04:13 +01:00
using ActionRPG.Input;
using UnityEngine;
namespace ActionRPG.Camera
{
/// <summary>
/// Third-person camera controller. Place this script on an empty pivot GameObject.
/// Add a child GameObject for the physical camera and assign it to _cameraBoom.
/// </summary>
public class PlayerCameraController : MonoBehaviour
{
// ── References ─────────────────────────────────────────────────
[Header("References")]
[SerializeField] private PlayerInputReader _input;
[SerializeField] private Transform _followTarget;
[SerializeField] private UnityEngine.Camera _camera;
[SerializeField] private Transform _cameraBoom;
// ── Sensitivity & Inversion ────────────────────────────────────
[Header("Sensitivity")]
[SerializeField, Min(0.1f)] private float _mouseSensitivity = 5f;
[SerializeField, Min(0.1f)] private float _gamepadSensitivity = 120f;
[SerializeField] private bool _invertY = false;
// ── Camera Position ────────────────────────────────────────────
[Header("Position")]
[SerializeField] private float _cameraDistance = 5f;
[SerializeField] private float _heightOffset = 1.5f;
[SerializeField] private float _horizontalOffset = 0.5f;
[SerializeField] private float _tiltOffset = 0f;
[SerializeField] private Vector2 _tiltBounds = new Vector2(-20f, 60f);
// ── Lag ────────────────────────────────────────────────────────
[Header("Lag")]
[SerializeField, Min(0.01f)] private float _positionalLag = 0.05f;
[SerializeField, Min(0.01f)] private float _rotationalLag = 0.05f;
// ── Shoulder Switch ────────────────────────────────────────────
[Header("Shoulder Switch")]
[SerializeField] private bool _allowShoulderSwitch = true;
[SerializeField] private float _shoulderSwitchSpeed = 8f;
private float _currentHorizontalOffset;
private bool _isRightShoulder = true;
// ── Collision ──────────────────────────────────────────────────
[Header("Collision")]
[SerializeField] private bool _enableCollision = true;
[SerializeField] private float _collisionRadius = 0.2f;
[SerializeField] private LayerMask _collisionLayers;
// ── Camera Shake ───────────────────────────────────────────────
[Header("Camera Shake")]
[SerializeField, Min(0f)] private float _shakeDecaySpeed = 3f;
private float _shakeMagnitude;
private float _shakeTimer;
// ── Lock-On ────────────────────────────────────────────────────
[Header("Lock-On")]
[SerializeField] private bool _isLockedOn;
[SerializeField] private Transform _lockOnTarget;
[SerializeField, Min(0.1f)] private float _lockOnRotationSpeed = 8f;
// ── Cursor ─────────────────────────────────────────────────────
[Header("Cursor")]
[SerializeField] private bool _hideCursor = true;
// ── Internal state ─────────────────────────────────────────────
private Vector3 _currentPosition;
private float _yaw;
private float _pitch;
private float _currentYaw;
private float _currentPitch;
private void Start()
{
if (_hideCursor)
{
Cursor.visible = false;
Cursor.lockState = CursorLockMode.Locked;
}
if (_followTarget != null)
{
transform.position = _followTarget.position;
_yaw = transform.eulerAngles.y;
_currentYaw = _yaw;
}
_currentHorizontalOffset = _horizontalOffset;
ApplyBoomPosition();
}
private void LateUpdate()
{
if (_followTarget == null) return;
HandleRotation();
HandlePositionalFollow();
HandleShoulderOffset();
HandleCollision();
HandleShake();
ApplyBoomPosition();
}
// ── Rotation ───────────────────────────────────────────────────
private void HandleRotation()
{
if (_isLockedOn && _lockOnTarget != null)
{
// Smoothly rotate to face the lock-on target
Vector3 toTarget = _lockOnTarget.position - transform.position;
toTarget.y = 0f;
if (toTarget != Vector3.zero)
{
float targetYaw = Quaternion.LookRotation(toTarget).eulerAngles.y;
_yaw = Mathf.LerpAngle(_currentYaw, targetYaw, _lockOnRotationSpeed * Time.deltaTime);
}
// Keep pitch level when locked on
_pitch = Mathf.Lerp(_currentPitch, 0f, _lockOnRotationSpeed * Time.deltaTime);
}
else
{
Vector2 look = _input.LookInput;
float invertSign = _invertY ? 1f : -1f;
// Detect gamepad (larger raw values) vs mouse
bool isGamepad = look.magnitude > 1f;
float sensitivity = isGamepad ? _gamepadSensitivity * Time.deltaTime : _mouseSensitivity;
_yaw += look.x * sensitivity;
_pitch += look.y * sensitivity * invertSign;
_pitch = Mathf.Clamp(_pitch, _tiltBounds.x, _tiltBounds.y);
}
_currentYaw = Mathf.LerpAngle(_currentYaw, _yaw, _rotationalLag > 0 ? Time.deltaTime / _rotationalLag : 1f);
_currentPitch = Mathf.LerpAngle(_currentPitch, _pitch, _rotationalLag > 0 ? Time.deltaTime / _rotationalLag : 1f);
transform.rotation = Quaternion.Euler(_currentPitch + _tiltOffset, _currentYaw, 0f);
}
// ── Positional Follow ──────────────────────────────────────────
private void HandlePositionalFollow()
{
Vector3 targetPos = _followTarget.position + Vector3.up * _heightOffset;
_currentPosition = Vector3.Lerp(_currentPosition == Vector3.zero ? targetPos : _currentPosition,
targetPos,
_positionalLag > 0 ? Time.deltaTime / _positionalLag : 1f);
transform.position = _currentPosition;
}
// ── Shoulder Switch ────────────────────────────────────────────
private void HandleShoulderOffset()
{
float targetOffset = _isRightShoulder ? _horizontalOffset : -_horizontalOffset;
_currentHorizontalOffset = Mathf.Lerp(_currentHorizontalOffset, targetOffset, _shoulderSwitchSpeed * Time.deltaTime);
}
// ── Collision ──────────────────────────────────────────────────
private void HandleCollision()
{
if (!_enableCollision || _cameraBoom == null) return;
Vector3 origin = transform.position;
Vector3 dir = _cameraBoom.position - origin;
float maxDist = _cameraDistance;
if (Physics.SphereCast(origin, _collisionRadius, dir.normalized, out RaycastHit hit, maxDist, _collisionLayers))
{
float adjustedDistance = Mathf.Clamp(hit.distance - _collisionRadius, 0.5f, maxDist);
_cameraBoom.localPosition = new Vector3(_currentHorizontalOffset, 0f, -adjustedDistance);
}
}
// ── Camera Shake ───────────────────────────────────────────────
private void HandleShake()
{
if (_shakeTimer > 0f)
{
_shakeTimer -= Time.deltaTime * _shakeDecaySpeed;
float shake = Mathf.Lerp(0f, _shakeMagnitude, _shakeTimer);
_cameraBoom.localPosition += (Vector3)Random.insideUnitCircle * shake;
}
else
{
_shakeTimer = 0f;
}
}
// ── Boom Position ──────────────────────────────────────────────
private void ApplyBoomPosition()
{
if (_cameraBoom == null) return;
_cameraBoom.localPosition = new Vector3(_currentHorizontalOffset, 0f, -_cameraDistance);
_cameraBoom.localEulerAngles = Vector3.zero;
}
// ── Public API ─────────────────────────────────────────────────
/// <summary>Enables or disables lock-on to a target.</summary>
public void SetLockOn(bool enabled, Transform target = null)
{
_isLockedOn = enabled;
_lockOnTarget = target != null ? target : _lockOnTarget;
}
/// <summary>Switches the camera between left and right shoulder.</summary>
public void SwitchShoulder()
{
if (!_allowShoulderSwitch) return;
_isRightShoulder = !_isRightShoulder;
}
/// <summary>Triggers a camera shake effect.</summary>
/// <param name="magnitude">Strength of the shake.</param>
/// <param name="duration">Duration in seconds (controls decay speed).</param>
public void Shake(float magnitude, float duration = 1f)
{
_shakeMagnitude = magnitude;
_shakeTimer = duration;
}
/// <summary>Returns the camera's forward vector with Y zeroed and normalised. Use for movement direction.</summary>
public Vector3 CameraForwardFlat => new Vector3(_camera.transform.forward.x, 0f, _camera.transform.forward.z).normalized;
/// <summary>Returns the camera's right vector with Y zeroed and normalised.</summary>
public Vector3 CameraRightFlat => new Vector3(_camera.transform.right.x, 0f, _camera.transform.right.z).normalized;
/// <summary>Returns the camera's current yaw in world space.</summary>
public float CameraYaw => _currentYaw;
private void OnDrawGizmosSelected()
{
if (_cameraBoom == null) return;
Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(_cameraBoom.position, _collisionRadius);
}
}
}