using UnityEngine; using Unity.Cinemachine; #if ENABLE_INPUT_SYSTEM using UnityEngine.InputSystem; #endif /// /// Feeds mouse input into a so the camera /// orbits around the follow target like a classic third-person camera. /// /// Rotation is always on by default and the cursor is locked/hidden while looking. /// Pressing the toggle key (Tab) unlocks the cursor and disables camera rotation, /// so you can freely use the mouse (e.g. paint the mesh with the left button or /// click UI). Pressing it again re-locks the cursor and re-enables rotation. /// /// Horizontal/vertical mouse movement drives the orbital axes (yaw/pitch); the /// mouse wheel zooms by changing the orbit radius. The script reads the new Input /// System (Mouse.current / Keyboard.current) when enabled and falls back to the /// legacy Input Manager otherwise. The project currently runs the new Input System only. /// /// Attach this to the same GameObject as the CinemachineCamera / /// CinemachineOrbitalFollow (the camera rig, not the player). /// [RequireComponent(typeof(CinemachineOrbitalFollow))] [DisallowMultipleComponent] public class ThirdPersonOrbitInput : MonoBehaviour { [Header("Rotation")] [Tooltip("Horizontal look sensitivity in degrees of yaw per pixel of mouse movement.")] public float horizontalSensitivity = 0.2f; [Tooltip("Vertical look sensitivity in degrees of pitch per pixel of mouse movement.")] public float verticalSensitivity = 0.15f; [Tooltip("If ON, moving the mouse up tilts the camera down (inverted pitch).")] public bool invertY = false; [Header("Toggle")] [Tooltip("Key that toggles between look mode (cursor locked, camera rotates) and " + "free mode (cursor unlocked, camera rotation disabled).")] #if ENABLE_INPUT_SYSTEM public Key toggleKey = Key.Tab; #else public KeyCode toggleKey = KeyCode.Tab; #endif [Tooltip("If ON, the camera starts in look mode (rotation enabled, cursor locked).")] public bool startInLookMode = true; [Header("Zoom")] [Tooltip("If ON, the mouse wheel zooms the camera in and out (changes orbit radius).")] public bool enableZoom = true; [Tooltip("How much the orbit radius changes per mouse wheel notch.")] public float zoomSpeed = 0.5f; [Tooltip("Closest the camera can zoom in (orbit radius in units).")] public float minRadius = 1.5f; [Tooltip("Farthest the camera can zoom out (orbit radius in units).")] public float maxRadius = 8f; [Header("Cursor")] [Tooltip("If ON, the cursor is locked and hidden while in look mode (rotation " + "enabled), and freed in free mode so you can use the mouse normally.")] public bool lockCursorInLookMode = true; CinemachineOrbitalFollow _orbit; // True = look mode (cursor locked, camera rotates). False = free mode. bool _rotationEnabled = true; void Awake() { _orbit = GetComponent(); if (_orbit == null) Debug.LogError("[ThirdPersonOrbitInput] No CinemachineOrbitalFollow found on this GameObject.", this); } void OnEnable() { _rotationEnabled = startInLookMode; ApplyCursorState(); } void OnDisable() { // Make sure we never leave the cursor locked/hidden when this is turned off. SetCursorLocked(false); } void Update() { if (_orbit == null) return; // Tab toggles between look mode and free mode. if (WasTogglePressed()) { _rotationEnabled = !_rotationEnabled; ApplyCursorState(); } if (_rotationEnabled) { Vector2 delta = ReadMouseDelta(); if (delta.sqrMagnitude > 0f) { ApplyAxisDelta(ref _orbit.HorizontalAxis, delta.x * horizontalSensitivity); float pitch = delta.y * verticalSensitivity * (invertY ? 1f : -1f); ApplyAxisDelta(ref _orbit.VerticalAxis, pitch); } } if (enableZoom) { float scroll = ReadScroll(); if (!Mathf.Approximately(scroll, 0f)) _orbit.Radius = Mathf.Clamp(_orbit.Radius - scroll * zoomSpeed, minRadius, maxRadius); } } void ApplyCursorState() { if (lockCursorInLookMode) SetCursorLocked(_rotationEnabled); } /// /// Adds to an input axis, respecting its range: /// wrapping axes (e.g. yaw) wrap around, non-wrapping axes (e.g. pitch) clamp. /// static void ApplyAxisDelta(ref InputAxis axis, float amount) { float value = axis.Value + amount; float min = axis.Range.x; float max = axis.Range.y; if (axis.Wrap && max > min) { float span = max - min; value = ((value - min) % span + span) % span + min; } else { value = Mathf.Clamp(value, min, max); } axis.Value = value; } static void SetCursorLocked(bool locked) { Cursor.lockState = locked ? CursorLockMode.Locked : CursorLockMode.None; Cursor.visible = !locked; } bool WasTogglePressed() { #if ENABLE_INPUT_SYSTEM return Keyboard.current != null && Keyboard.current[toggleKey].wasPressedThisFrame; #else return Input.GetKeyDown(toggleKey); #endif } Vector2 ReadMouseDelta() { #if ENABLE_INPUT_SYSTEM // Mouse.current.delta is already the per-frame pixel movement (no deltaTime). return Mouse.current != null ? Mouse.current.delta.ReadValue() : Vector2.zero; #else return new Vector2(Input.GetAxisRaw("Mouse X"), Input.GetAxisRaw("Mouse Y")) * 10f; #endif } float ReadScroll() { #if ENABLE_INPUT_SYSTEM // Scroll y is reported in steps of ~120 per notch; normalize to ~1 per notch. return Mouse.current != null ? Mouse.current.scroll.ReadValue().y / 120f : 0f; #else return Input.GetAxis("Mouse ScrollWheel") * 10f; #endif } }