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>
349 lines
16 KiB
C#
349 lines
16 KiB
C#
using UnityEngine;
|
|
using Unity.Cinemachine;
|
|
|
|
/// <summary>
|
|
/// Feeds mouse input into a <see cref="CinemachineOrbitalFollow"/>: mouse movement drives yaw/pitch,
|
|
/// the wheel zooms, middle-mouse pans (paint mode only). Orbit behaviour depends on
|
|
/// <see cref="LookMode"/> (usually set by the GameModeManager). Attach to the camera rig.
|
|
/// </summary>
|
|
[RequireComponent(typeof(CinemachineOrbitalFollow))]
|
|
[DisallowMultipleComponent]
|
|
public class ThirdPersonOrbitInput : MonoBehaviour
|
|
{
|
|
public enum OrbitLookMode
|
|
{
|
|
/// <summary>Rotate with every mouse move; cursor locked/hidden.</summary>
|
|
AlwaysActive,
|
|
/// <summary>Rotate only while the right mouse button is held; cursor free otherwise.</summary>
|
|
HoldRightMouseButton,
|
|
/// <summary>No rotation; cursor free.</summary>
|
|
Disabled
|
|
}
|
|
|
|
[Header("Look Mode")]
|
|
[Tooltip("How the camera orbit is driven (usually set by GameModeManager).")]
|
|
[SerializeField] private OrbitLookMode lookMode = OrbitLookMode.AlwaysActive;
|
|
|
|
[Header("Rotation")]
|
|
[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("Eases orbit rotation so quick mouse moves ramp in/out (higher = snappier, 0 = raw/instant).")]
|
|
public float rotationSmoothing = 20f;
|
|
|
|
[Header("Zoom")]
|
|
[Tooltip("Mouse wheel zooms the camera in and out (changes orbit radius).")]
|
|
public bool enableZoom = true;
|
|
|
|
[Tooltip("Scale the zoom range, step and default radius by the follow target's size (PlayerScaler), " +
|
|
"so a small (Hider) or large (Hunter) player is framed at the same body-relative distance. " +
|
|
"The radius fields below are then size-1 design values, multiplied by the target's scale at " +
|
|
"runtime. Off = absolute world units regardless of player size.")]
|
|
public bool scaleWithTarget = true;
|
|
|
|
[Tooltip("World units the orbit radius changes per mouse wheel notch (size-1 design value; scaled by " +
|
|
"the target when Scale With Target is on).")]
|
|
public float zoomSpeed = 0.5f;
|
|
|
|
[Tooltip("Eases the orbit radius toward the zoom target (higher = snappier, 0 = instant).")]
|
|
public float zoomSmoothing = 10f;
|
|
|
|
[Tooltip("Closest orbit radius / zoom in (size-1 design value; scaled by the target).")]
|
|
public float minRadius = 1.5f;
|
|
|
|
[Tooltip("Farthest orbit radius / zoom out (size-1 design value; scaled by the target).")]
|
|
public float maxRadius = 8f;
|
|
|
|
[Tooltip("Orbit radius applied when the camera (re)binds to a player - the starting zoom level " +
|
|
"(size-1 design value; scaled by the target). Set so the bound player is comfortably framed.")]
|
|
public float defaultRadius = 4.5f;
|
|
|
|
[Header("Pan (middle mouse)")]
|
|
[Tooltip("Hold the middle mouse button and drag to pan the camera framing - a temporary offset to bring an " +
|
|
"awkward spot of the model in front of the lens for painting. The pan stays put on release and only " +
|
|
"glides back to center when paint mode is left. Enabled only while PaintMode is active.")]
|
|
public bool enablePan = true;
|
|
|
|
[Tooltip("World units the pan offset moves per pixel of mouse drag, measured at the default zoom. It then " +
|
|
"scales with the live zoom distance so the model slides the same amount on screen per pixel at any " +
|
|
"zoom (closer in = finer). Body scale is folded in automatically via the orbit radius.")]
|
|
public float panSensitivity = 0.004f;
|
|
|
|
[Tooltip("Farthest the framing can pan from center, in world units (body scale folded in). This is NOT scaled " +
|
|
"by zoom, so the maximum pan distance stays the same at every zoom level - only the sensitivity changes.")]
|
|
public float panMaxDistance = 1f;
|
|
|
|
[Tooltip("How snappily the pan glides back to center when paint mode is left (1/second; 0 = snaps home " +
|
|
"instantly). The pan stays put on middle-button release - it only returns on leaving paint mode.")]
|
|
public float panReturnSpeed = 14f;
|
|
|
|
[Header("Cursor")]
|
|
[Tooltip("Lock and hide the cursor while the camera is actively rotating.")]
|
|
public bool manageCursor = true;
|
|
|
|
CinemachineOrbitalFollow _orbit;
|
|
Vector2 _smoothedLook; // low-pass-filtered look delta (deg/frame) for eased rotation
|
|
float _targetRadius; // zoom radius the actual radius eases toward
|
|
bool _zoomInit; // _targetRadius seeded from the live radius yet?
|
|
Vector2 _pan; // accumulated pan in camera-space units (x=right, y=up); stays put on release, eases to 0 on leaving paint mode
|
|
CinemachineCameraOffset _camOffset; // post-aim extension that slides the whole framing (true pan, not an orbit arc)
|
|
|
|
/// <summary>Current orbit mode. Setting it also refreshes the cursor state.</summary>
|
|
public OrbitLookMode LookMode
|
|
{
|
|
get => lookMode;
|
|
set { lookMode = value; ApplyCursorState(); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// While true, mouse-look is paused (orbit frozen) without changing the cursor
|
|
/// state. The player controller sets this while dragging the wall-climb offset so
|
|
/// the same mouse motion is not also spent orbiting the camera.
|
|
/// </summary>
|
|
public bool LookSuppressed { get; set; }
|
|
|
|
/// <summary>While true, holding the middle mouse button and dragging pans the camera framing - a temporary
|
|
/// look-point offset that stays put on release. Set by PaintMode so the pan is only available while painting;
|
|
/// clearing it (on leaving paint mode) is what glides the framing back home.</summary>
|
|
public bool PanEnabled { get; set; }
|
|
|
|
void Awake()
|
|
{
|
|
_orbit = GetComponent<CinemachineOrbitalFollow>();
|
|
if (!_orbit)
|
|
Debug.LogError("[ThirdPersonOrbitInput] No CinemachineOrbitalFollow found on this GameObject.", this);
|
|
|
|
// The pan rides a CinemachineCameraOffset extension applied AFTER aim with PreserveComposition off: it
|
|
// shifts the camera position AND the look-at point by the same world vector, so the framing translates
|
|
// (the model slides across the screen) instead of re-aiming back to center like a body TargetOffset
|
|
// would. Offset is camera-local (x=right, y=up). Added at runtime so no scene/prefab authoring is needed.
|
|
if (enablePan)
|
|
{
|
|
_camOffset = GetComponent<CinemachineCameraOffset>();
|
|
if (!_camOffset) _camOffset = gameObject.AddComponent<CinemachineCameraOffset>();
|
|
_camOffset.ApplyAfter = CinemachineCore.Stage.Aim;
|
|
_camOffset.PreserveComposition = false;
|
|
_camOffset.Offset = Vector3.zero;
|
|
}
|
|
}
|
|
|
|
void OnEnable() => ApplyCursorState();
|
|
|
|
void OnDisable()
|
|
{
|
|
InputUtil.SetCursorLocked(false);
|
|
// Deliberately DON'T zero the pan: only FreeCamMode disables this component, and the freecam
|
|
// snaps to the live (still panned) view - resetting now would jump the blend-FROM camera to
|
|
// center while the freecam sits at the panned pose (the visible snap on the paint->freecam
|
|
// switch). Frozen offset = blend-from == blend-to; the pan glides home once the orbit runs again.
|
|
}
|
|
|
|
/// <summary>Uniform scale of the current follow target (1 when none, or when Scale With Target is off).
|
|
/// The zoom range, step and default radius are multiplied by this so the framing is body-relative across
|
|
/// player sizes. PlayerScaler keeps the player uniformly scaled, so a single lossyScale component is exact.</summary>
|
|
float CurrentScale()
|
|
{
|
|
if (!scaleWithTarget || !_orbit) return 1f;
|
|
Transform t = _orbit.FollowTarget;
|
|
return t ? Mathf.Max(0.0001f, t.lossyScale.x) : 1f;
|
|
}
|
|
|
|
/// <summary>Pan-SENSITIVITY scale with the live zoom: radius / defaultRadius = 1 at the default zoom,
|
|
/// finer zoomed in, coarser zoomed out - the model slides the same amount ON SCREEN per pixel at any
|
|
/// zoom. Body scale cancels out (the radius already embeds it). The clamp deliberately does NOT use
|
|
/// this: panMaxDistance stays body-relative only, constant across zoom levels.</summary>
|
|
float PanZoomScale() => _orbit ? _orbit.Radius / Mathf.Max(0.0001f, defaultRadius) : 1f;
|
|
|
|
/// <summary>Snaps the zoom to the scaled default radius. Called when a new avatar binds (see
|
|
/// GameModeManager.BindLocalPlayer) so each player starts at its body-relative default zoom instead of
|
|
/// inheriting the previous avatar's radius.</summary>
|
|
public void ResetZoom()
|
|
{
|
|
if (!_orbit) return;
|
|
_targetRadius = defaultRadius * CurrentScale();
|
|
_orbit.Radius = _targetRadius;
|
|
_zoomInit = true;
|
|
}
|
|
|
|
/// <summary>Snaps the orbit axes to Camera.main's current view direction so the next orbit mode keeps
|
|
/// that facing instead of the orbit's last-used angle. Called when switching out of a non-orbit rig:
|
|
/// Camera.main still shows that rig's view this frame (the brain hasn't blended yet), so the read is
|
|
/// exact. With WorldSpace binding + Sphere orbit + a centered composer the look rotation is exactly
|
|
/// Euler(VerticalAxis, HorizontalAxis, 0).</summary>
|
|
public void SyncToView()
|
|
{
|
|
if (!_orbit) return;
|
|
var cam = Camera.main;
|
|
if (!cam) return;
|
|
|
|
Vector3 e = cam.transform.rotation.eulerAngles;
|
|
SetAxisValue(ref _orbit.HorizontalAxis, e.y > 180f ? e.y - 360f : e.y);
|
|
SetAxisValue(ref _orbit.VerticalAxis, e.x > 180f ? e.x - 360f : e.x);
|
|
|
|
// Drop any eased look carry-over so the snapped angle isn't immediately dragged back.
|
|
_smoothedLook = Vector2.zero;
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
if (!_orbit)
|
|
return;
|
|
|
|
bool modeWantsLook = ShouldRotate();
|
|
|
|
if (manageCursor)
|
|
InputUtil.SetCursorLocked(modeWantsLook);
|
|
|
|
if (modeWantsLook && !LookSuppressed)
|
|
{
|
|
Vector2 delta = InputUtil.ReadMouseDelta() * MouseSensitivity.Multiplier;
|
|
Vector2 raw = new Vector2(
|
|
delta.x * horizontalSensitivity,
|
|
delta.y * verticalSensitivity * (invertY ? 1f : -1f));
|
|
|
|
// Ease the per-frame look toward the raw delta so fast flicks ramp in/out
|
|
// instead of snapping; steady motion still converges to full sensitivity.
|
|
Vector2 applied = rotationSmoothing > 0f
|
|
? (_smoothedLook = Vector2.Lerp(_smoothedLook, raw, 1f - Mathf.Exp(-rotationSmoothing * Time.deltaTime)))
|
|
: raw;
|
|
|
|
if (applied.sqrMagnitude > 0f)
|
|
{
|
|
ApplyAxisDelta(ref _orbit.HorizontalAxis, applied.x);
|
|
ApplyAxisDelta(ref _orbit.VerticalAxis, applied.y);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_smoothedLook = Vector2.zero;
|
|
}
|
|
|
|
if (enableZoom)
|
|
{
|
|
if (!_zoomInit)
|
|
{
|
|
// Seed the starting zoom at the scaled default (not the live radius) so a fresh bind frames
|
|
// the player body-relative; ResetZoom() does the same on an explicit (re)bind.
|
|
_targetRadius = defaultRadius * CurrentScale();
|
|
_orbit.Radius = _targetRadius;
|
|
_zoomInit = true;
|
|
}
|
|
|
|
float scale = CurrentScale();
|
|
|
|
// The wheel belongs to the UI when the pointer is over an interactive element (e.g. a scroll
|
|
// list), so it scrolls the UI instead of zooming. Shift/Ctrl+wheel is likewise reserved for the
|
|
// paint brush resize (MeshPainter) - but only in paint mode (PanEnabled), where the resize
|
|
// actually claims the wheel; outside paint mode Shift is the sprint key, so the wheel must zoom.
|
|
bool wheelClaimed = InputUtil.PointerOverUi()
|
|
|| (PanEnabled && (InputUtil.ShiftHeld() || InputUtil.CtrlHeld()));
|
|
float scroll = wheelClaimed ? 0f : InputUtil.ReadScroll();
|
|
if (!Mathf.Approximately(scroll, 0f))
|
|
{
|
|
// One wheel notch = one zoom step (sign only, platform-independent). The step scales with
|
|
// the player so the notch count to cross the range is the same at every size.
|
|
_targetRadius -= Mathf.Sign(scroll) * zoomSpeed * scale;
|
|
}
|
|
|
|
// Clamp every frame against the scaled range so a runtime size change (PlayerScaler grow/shrink)
|
|
// keeps the target in-range instead of sticking at a stale absolute bound.
|
|
_targetRadius = Mathf.Clamp(_targetRadius, minRadius * scale, maxRadius * scale);
|
|
|
|
// Ease the actual radius toward the target (frame-rate independent).
|
|
_orbit.Radius = zoomSmoothing > 0f
|
|
? Mathf.Lerp(_orbit.Radius, _targetRadius, 1f - Mathf.Exp(-zoomSmoothing * Time.deltaTime))
|
|
: _targetRadius;
|
|
}
|
|
|
|
UpdatePan(Time.deltaTime);
|
|
}
|
|
|
|
bool ShouldRotate()
|
|
{
|
|
switch (lookMode)
|
|
{
|
|
case OrbitLookMode.AlwaysActive: return true;
|
|
case OrbitLookMode.HoldRightMouseButton: return InputUtil.RightMouseHeld();
|
|
default: return false;
|
|
}
|
|
}
|
|
|
|
void ApplyCursorState()
|
|
{
|
|
if (manageCursor)
|
|
InputUtil.SetCursorLocked(ShouldRotate());
|
|
}
|
|
|
|
/// <summary>Adds to an input axis; wrapping axes wrap, others clamp to their range.</summary>
|
|
static void ApplyAxisDelta(ref InputAxis axis, float amount)
|
|
=> SetAxisValue(ref axis, axis.Value + amount);
|
|
|
|
/// <summary>Sets an input axis to an absolute value; wrapping axes wrap, others clamp to their range.</summary>
|
|
static void SetAxisValue(ref InputAxis axis, float value)
|
|
{
|
|
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;
|
|
}
|
|
|
|
/// <summary>Middle-mouse pan (while PaintMode enables it): slides the whole framing along the screen
|
|
/// right/up axes via the camera-offset extension - a temporary offset to line up an awkward spot of
|
|
/// the model for painting. Eases home outside paint mode / while the orbit is Disabled.</summary>
|
|
void UpdatePan(float dt)
|
|
{
|
|
if (!enablePan || !_camOffset)
|
|
return;
|
|
|
|
bool active = PanEnabled && lookMode != OrbitLookMode.Disabled;
|
|
float zoom = PanZoomScale();
|
|
|
|
if (active && InputUtil.MiddleMouseHeld())
|
|
{
|
|
Vector2 d = InputUtil.ReadMouseDelta() * MouseSensitivity.Multiplier;
|
|
// Subtracting gives a hand-tool feel (the model follows the cursor). The step scales with the
|
|
// zoom (same on-screen feel, see PanZoomScale); the clamp stays body-relative only.
|
|
_pan -= d * (panSensitivity * zoom);
|
|
_pan = Vector2.ClampMagnitude(_pan, panMaxDistance * CurrentScale());
|
|
}
|
|
else if (active)
|
|
{
|
|
return; // still painting, middle button released - hold the pan where it was left
|
|
}
|
|
else if (_pan != Vector2.zero)
|
|
{
|
|
// Left paint mode (or look disabled) - glide the framing back to center.
|
|
if (panReturnSpeed > 0f)
|
|
_pan = Vector2.Lerp(_pan, Vector2.zero, 1f - Mathf.Exp(-panReturnSpeed * dt));
|
|
else
|
|
_pan = Vector2.zero;
|
|
// Snap the last sliver so the offset settles exactly on center and this branch stops running.
|
|
if (_pan.sqrMagnitude < 1e-8f)
|
|
_pan = Vector2.zero;
|
|
}
|
|
else
|
|
{
|
|
return; // at rest and not panning - nothing to write
|
|
}
|
|
|
|
// Offset is camera-local (x=right, y=up); the extension translates eye + look-at together -> true pan.
|
|
_camOffset.Offset = new Vector3(_pan.x, _pan.y, 0f);
|
|
}
|
|
}
|