Files
chameleon/Assets/Scripts/Components/Controller/FirstPersonController.cs
T
adminandClaude Fable 5 6c03e5d3f6 Refactor Assets/Scripts: trim restating comments, minor structure cleanups
Sweep over 48 files (Editor/ excluded): remove comments that restate the
code, condense class summaries, fix three stale doc statements
(HsvColorPicker alpha bar, PaintHud layoutPrefab, MasterVolume
FallbackDefault). Inline two single-use wrappers (IsPointerOverUi,
BuildModes), drop an unused using, replace a magic literal with the
existing ParallaxBase constant, and convert ~10 UnityEngine.Object null
checks to the implicit-bool convention. No serialized fields, signatures,
RPC/NetworkVariable names, defaults, or #if blocks touched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 08:29:18 +02:00

193 lines
7.5 KiB
C#

using UnityEngine;
using Unity.Cinemachine;
using Chameleon.Components;
/// <summary>
/// First-person camera on its own dedicated <see cref="CinemachineCamera"/>: snaps to the player's eye
/// anchor, applies free mouse-look, and hides the player mesh while active. Driven by
/// <see cref="FirstPersonMode"/> via <see cref="SetActive"/>; WASD stays view-relative automatically
/// because movement reads the brain-driven Main Camera forward.
/// </summary>
[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;
[Tooltip("Fur renderers disabled while first-person is active (they draw via GPU instancing, not a Renderer, so they need their own toggle). Empty = the player's FurShellRenderers, auto-resolved.")]
public FurShellRenderer[] hiddenFurRenderers;
[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
SmoothedLook _look; // yaw/pitch accumulator + smoothing shared with the freecam
bool _resolved;
void Awake()
{
_vcam = GetComponent<CinemachineCamera>();
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;
}
/// <summary>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 <see cref="FirstPersonMode"/>.</summary>
public void SetActive(bool active)
{
if (_vcam)
_vcam.Priority = active ? activePriority : inactivePriority;
SetMeshHidden(active);
if (active)
{
SyncToCurrentView();
InputUtil.SetCursorLocked(true);
ApplyTransform(); // place at the eye on the same frame so the blend starts from there
if (_player) _player.FaceYaw = _look.DisplayYaw; // body faces the camera from frame one
}
else
{
InputUtil.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 = _look.DisplayYaw; // keep the body's yaw locked to the camera
}
void LookTick()
{
Vector2 delta = InputUtil.ReadMouseDelta() * MouseSensitivity.Multiplier;
_look.Tick(delta, horizontalSensitivity, verticalSensitivity, invertY, maxPitch, lookSmoothTime);
}
void ApplyTransform()
{
Quaternion rot = _look.Rotation;
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;
}
/// <summary>Seed look from the currently rendered view so going live produces no visible jump.</summary>
void SyncToCurrentView()
{
var cam = Camera.main;
if (cam)
_look.SyncTo(cam.transform.rotation, maxPitch);
else
_look.Snap(); // no camera: keep the current angles but still snap the smoothed state
}
void SetMeshHidden(bool hidden)
{
if (hiddenRenderers != null)
foreach (var r in hiddenRenderers)
if (r) r.enabled = !hidden;
// The fur draws from RenderPipelineManager.beginCameraRendering, not through a Renderer, so hiding
// the SkinnedMeshRenderer above doesn't stop it - disabling the component unsubscribes that hook.
if (hiddenFurRenderers != null)
foreach (var f in hiddenFurRenderers)
if (f) f.enabled = !hidden;
}
/// <summary>
/// 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.
/// </summary>
public void SetPlayer(PlayerCharacterController p)
{
if (!p) return;
_player = p;
_poseController = p.GetComponent<PlayerPoseController>();
Transform t = p.transform.Find("FirstPersonTarget");
if (t) cameraTarget = t;
hiddenRenderers = p.GetComponentsInChildren<SkinnedMeshRenderer>(true);
hiddenFurRenderers = p.GetComponentsInChildren<FurShellRenderer>(true);
_resolved = true;
}
void ResolveReferences()
{
if (_resolved) return;
_resolved = true;
var player = FindFirstObjectByType<PlayerCharacterController>(FindObjectsInactive.Include);
if (!player) return;
_player = player;
if (!_poseController)
_poseController = player.GetComponent<PlayerPoseController>();
if (!cameraTarget)
{
Transform t = player.transform.Find("FirstPersonTarget");
if (t) cameraTarget = t;
}
if (hiddenRenderers == null || hiddenRenderers.Length == 0)
hiddenRenderers = player.GetComponentsInChildren<SkinnedMeshRenderer>(true);
if (hiddenFurRenderers == null || hiddenFurRenderers.Length == 0)
hiddenFurRenderers = player.GetComponentsInChildren<FurShellRenderer>(true);
}
}