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. /// /// How the orbit is driven is controlled by : /// * AlwaysActive - rotates with every mouse move, cursor locked/hidden. /// * HoldRightMouseButton - rotates only while the right mouse button is held; /// the cursor is free otherwise (so the left button can paint). /// * Disabled - no rotation, cursor free. /// /// The look mode is normally set by the GameModeManager when switching modes, /// but can also be configured directly in the Inspector for standalone use. /// /// 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 { /// How the camera orbit is driven. public enum OrbitLookMode { /// Rotate with every mouse move; cursor locked/hidden. AlwaysActive, /// Rotate only while the right mouse button is held; cursor free otherwise. HoldRightMouseButton, /// No rotation; cursor free. Disabled } [Header("Look Mode")] [Tooltip("How the camera orbit is driven. Usually set by GameModeManager:\n" + "AlwaysActive = rotates with every mouse move (cursor locked).\n" + "HoldRightMouseButton = rotates only while RMB is held (cursor free otherwise).\n" + "Disabled = no rotation.")] [SerializeField] private OrbitLookMode lookMode = OrbitLookMode.AlwaysActive; [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("Zoom")] [Tooltip("If ON, the mouse wheel zooms the camera in and out (changes orbit radius).")] public bool enableZoom = true; [Tooltip("How many world units 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 the camera is actively " + "rotating, and freed otherwise so you can use the mouse normally.")] public bool manageCursor = true; CinemachineOrbitalFollow _orbit; /// /// How the camera orbit is currently driven. Setting this also updates the /// cursor state immediately (e.g. when GameModeManager switches modes). /// public OrbitLookMode LookMode { get => lookMode; set { lookMode = value; ApplyCursorState(); } } void Awake() { _orbit = GetComponent(); if (_orbit == null) Debug.LogError("[ThirdPersonOrbitInput] No CinemachineOrbitalFollow found on this GameObject.", this); } void OnEnable() { 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; bool rotating = ShouldRotate(); // Keep the cursor in sync every frame: in HoldRightMouseButton mode the // lock state follows the right mouse button. if (manageCursor) SetCursorLocked(rotating); if (rotating) { 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)) { // One wheel notch = one zoom step. Use only the sign so the step // size is exactly zoomSpeed regardless of the platform's raw wheel // magnitude (the new Input System reports ~1 per notch, the legacy // Windows path used ~120). Scrolling up (positive) zooms in. _orbit.Radius = Mathf.Clamp(_orbit.Radius - Mathf.Sign(scroll) * zoomSpeed, minRadius, maxRadius); } } } /// Whether the camera should orbit this frame, based on the look mode. bool ShouldRotate() { switch (lookMode) { case OrbitLookMode.AlwaysActive: return true; case OrbitLookMode.HoldRightMouseButton: return IsRightMouseHeld(); default: return false; } } void ApplyCursorState() { if (manageCursor) SetCursorLocked(ShouldRotate()); } /// /// 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 IsRightMouseHeld() { #if ENABLE_INPUT_SYSTEM return Mouse.current != null && Mouse.current.rightButton.isPressed; #else return Input.GetMouseButton(1); #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 // Raw wheel delta. Only its sign is used in Update (one notch = one zoom // step), so the platform-dependent magnitude does not matter here. return Mouse.current != null ? Mouse.current.scroll.ReadValue().y : 0f; #else return Input.GetAxis("Mouse ScrollWheel"); #endif } }