using UnityEngine; using Unity.Cinemachine; #if ENABLE_INPUT_SYSTEM using UnityEngine.InputSystem; #endif /// /// First-person camera. Drives its OWN dedicated : each frame it snaps /// to the player's dedicated FirstPersonTarget (the eye anchor) for position and /// applies free mouse-look (yaw/pitch) for rotation. While active the player's mesh is hidden so the /// body isn't seen from the inside. Movement stays with the player controller, which reads the /// brain-driven Main Camera forward, so WASD is automatically relative to this view. /// /// Activation is driven by via : it raises the /// vcam priority so the CinemachineBrain blends to it, snaps look to the current view (no jump), hides /// the mesh and locks the cursor. While inactive the vcam rests at a low priority and this component /// does nothing. /// [RequireComponent(typeof(CinemachineCamera))] [DisallowMultipleComponent] public class FirstPersonController : MonoBehaviour { [Header("Target")] [Tooltip("Eye position the camera sits at (the player's FirstPersonTarget). Empty = auto-resolved from the player's \"FirstPersonTarget\" child.")] public Transform cameraTarget; [Tooltip("Renderers hidden while first-person is active (so you don't see your own body). Empty = the player's SkinnedMeshRenderers, auto-resolved.")] public Renderer[] hiddenRenderers; [Header("Look")] [Tooltip("Horizontal look sensitivity (degrees of yaw per pixel).")] public float horizontalSensitivity = 0.2f; [Tooltip("Vertical look sensitivity (degrees of pitch per pixel).")] public float verticalSensitivity = 0.15f; [Tooltip("Invert vertical look (mouse up tilts the camera down).")] public bool invertY = false; [Tooltip("Pitch is clamped to +/- this angle (degrees) so the camera never flips over.")] [Range(1f, 89.9f)] public float maxPitch = 89f; [Header("Smoothing")] [Tooltip("Look smoothing, in seconds. 0 = raw mouse (instant); small values take the edge off jitter without feeling laggy.")] [Range(0f, 0.3f)] public float lookSmoothTime = 0.04f; [Header("Camera")] [Tooltip("Priority the dedicated CinemachineCamera takes while active (must beat the gameplay camera).")] public int activePriority = 100; [Tooltip("Priority the dedicated CinemachineCamera rests at while inactive (must lose to the gameplay camera).")] public int inactivePriority = -100; CinemachineCamera _vcam; PlayerCharacterController _player; // body whose yaw we pin to the camera while active PlayerPoseController _poseController; // supplies the active pose's per-pose first-person eye offset float _yaw; // look target (raw input accumulates here) float _pitch; float _displayYaw; // smoothed value actually applied to the transform float _displayPitch; float _yawSmoothRef; float _pitchSmoothRef; bool _resolved; void Awake() { _vcam = GetComponent(); if (!_vcam) Debug.LogError("[FirstPersonController] No CinemachineCamera on this GameObject; first-person cannot go live.", this); else _vcam.Priority = inactivePriority; ResolveReferences(); // Dormant until FirstPersonMode enters; nothing reads input or moves the rig meanwhile. enabled = false; } /// Enter/leave first-person. Raises/lowers the vcam priority, hides the player mesh, snaps /// look to the live view and manages the cursor. Called by . public void SetActive(bool active) { if (_vcam) _vcam.Priority = active ? activePriority : inactivePriority; SetMeshHidden(active); if (active) { SyncToCurrentView(); SetCursorLocked(true); ApplyTransform(); // place at the eye on the same frame so the blend starts from there if (_player) _player.FaceYaw = _displayYaw; // body faces the camera from frame one } else { SetCursorLocked(false); if (_player) _player.FaceYaw = null; // restore movement-facing } enabled = active; } void LateUpdate() { // LateUpdate so the player has already moved the CameraTarget this frame. LookTick(); ApplyTransform(); if (_player) _player.FaceYaw = _displayYaw; // keep the body's yaw locked to the camera } void LookTick() { Vector2 delta = ReadMouseDelta() * MouseSensitivity.Multiplier; _yaw += delta.x * horizontalSensitivity; float vs = verticalSensitivity * (invertY ? 1f : -1f); _pitch = Mathf.Clamp(_pitch + delta.y * vs, -maxPitch, maxPitch); if (lookSmoothTime > 0f) { // SmoothDampAngle is frame-rate-independent and wraps yaw correctly. _displayYaw = Mathf.SmoothDampAngle(_displayYaw, _yaw, ref _yawSmoothRef, lookSmoothTime); _displayPitch = Mathf.SmoothDampAngle(_displayPitch, _pitch, ref _pitchSmoothRef, lookSmoothTime); } else { _displayYaw = _yaw; _displayPitch = _pitch; } } void ApplyTransform() { Quaternion rot = Quaternion.Euler(_displayPitch, _displayYaw, 0f); if (cameraTarget) { // Sit at the first-person eye anchor, then raise/lower the eye by the active pose's first-person // offset. TransformVector keeps it in the target's local up and scales with the player (so it // tracks PlayerScaler). Vector3 pos = cameraTarget.position; float offset = _poseController ? _poseController.FirstPersonCameraYOffset : 0f; if (offset != 0f) pos += cameraTarget.TransformVector(Vector3.up * offset); transform.SetPositionAndRotation(pos, rot); } else transform.rotation = rot; } /// Seed look from the currently rendered view so going live produces no visible jump. void SyncToCurrentView() { var cam = Camera.main; if (cam) { Vector3 e = cam.transform.rotation.eulerAngles; _yaw = e.y; float p = e.x > 180f ? e.x - 360f : e.x; _pitch = Mathf.Clamp(p, -maxPitch, maxPitch); } // Snap the smoothed state to the fresh pose so entering never lags/jumps to catch up. _displayYaw = _yaw; _displayPitch = _pitch; _yawSmoothRef = 0f; _pitchSmoothRef = 0f; } void SetMeshHidden(bool hidden) { if (hiddenRenderers == null) return; foreach (var r in hiddenRenderers) if (r) r.enabled = !hidden; } /// /// Explicitly binds this rig to the locally-owned networked player (overriding the scene-wide /// auto-resolve, which could otherwise latch onto a remote avatar). Called by the GameModeManager /// when the local player spawns. /// public void SetPlayer(PlayerCharacterController p) { if (!p) return; _player = p; _poseController = p.GetComponent(); Transform t = p.transform.Find("FirstPersonTarget"); if (t) cameraTarget = t; hiddenRenderers = p.GetComponentsInChildren(true); _resolved = true; } void ResolveReferences() { if (_resolved) return; _resolved = true; var player = FindFirstObjectByType(FindObjectsInactive.Include); if (!player) return; _player = player; if (!_poseController) _poseController = player.GetComponent(); if (!cameraTarget) { Transform t = player.transform.Find("FirstPersonTarget"); if (t) cameraTarget = t; } if (hiddenRenderers == null || hiddenRenderers.Length == 0) hiddenRenderers = player.GetComponentsInChildren(true); } Vector2 ReadMouseDelta() { #if ENABLE_INPUT_SYSTEM return Mouse.current != null ? Mouse.current.delta.ReadValue() : Vector2.zero; #else return new Vector2(Input.GetAxisRaw("Mouse X"), Input.GetAxisRaw("Mouse Y")) * 10f; #endif } static void SetCursorLocked(bool locked) { Cursor.lockState = locked ? CursorLockMode.Locked : CursorLockMode.None; Cursor.visible = !locked; } }