The orbit radius (min/max/default and per-notch step) was in absolute world units and ignored the player's localScale, so a small Hider started too far out and a large Hunter too close and could not zoom out far enough. Multiply range, step and default radius by the follow target's lossyScale (PlayerScaler keeps the body uniformly scaled), making the inspector fields size-1 design values that frame every role body-relative. Re-clamp every frame so a runtime grow/shrink stays in range. GameModeManager.BindLocalPlayer calls ResetZoom after rebinding so each new avatar starts at its scaled default zoom. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
278 lines
10 KiB
C#
278 lines
10 KiB
C#
using UnityEngine;
|
|
using Unity.Cinemachine;
|
|
#if ENABLE_INPUT_SYSTEM
|
|
using UnityEngine.InputSystem;
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Feeds mouse input into a <see cref="CinemachineOrbitalFollow"/> so the camera
|
|
/// orbits the follow target. Mouse movement drives yaw/pitch, the wheel zooms.
|
|
/// The orbit behaviour depends on <see cref="LookMode"/> (usually set by the
|
|
/// GameModeManager). Attach to the camera rig (the CinemachineOrbitalFollow).
|
|
/// </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("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?
|
|
|
|
/// <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; }
|
|
|
|
void Awake()
|
|
{
|
|
_orbit = GetComponent<CinemachineOrbitalFollow>();
|
|
if (!_orbit)
|
|
Debug.LogError("[ThirdPersonOrbitInput] No CinemachineOrbitalFollow found on this GameObject.", this);
|
|
}
|
|
|
|
void OnEnable() => ApplyCursorState();
|
|
|
|
void OnDisable() => SetCursorLocked(false);
|
|
|
|
/// <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>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;
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
if (!_orbit)
|
|
return;
|
|
|
|
bool modeWantsLook = ShouldRotate();
|
|
|
|
if (manageCursor)
|
|
SetCursorLocked(modeWantsLook);
|
|
|
|
if (modeWantsLook && !LookSuppressed)
|
|
{
|
|
Vector2 delta = 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();
|
|
|
|
// Shift+wheel is reserved for the paint brush resize (MeshPainter), so the orbit camera
|
|
// ignores the wheel while Shift is held - otherwise it would zoom and resize at once.
|
|
float scroll = ShiftHeld() ? 0f : ReadScroll();
|
|
if (!Mathf.Approximately(scroll, 0f))
|
|
{
|
|
// One wheel notch = one zoom step (sign only), independent of the platform's raw wheel
|
|
// magnitude. Scrolling up zooms in. Step scales with the player so the number of notches 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;
|
|
}
|
|
}
|
|
|
|
bool ShouldRotate()
|
|
{
|
|
switch (lookMode)
|
|
{
|
|
case OrbitLookMode.AlwaysActive: return true;
|
|
case OrbitLookMode.HoldRightMouseButton: return IsRightMouseHeld();
|
|
default: return false;
|
|
}
|
|
}
|
|
|
|
void ApplyCursorState()
|
|
{
|
|
if (manageCursor)
|
|
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)
|
|
{
|
|
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
|
|
}
|
|
|
|
static bool ShiftHeld()
|
|
{
|
|
#if ENABLE_INPUT_SYSTEM
|
|
var kb = Keyboard.current;
|
|
if (kb != null) return kb.leftShiftKey.isPressed || kb.rightShiftKey.isPressed;
|
|
#endif
|
|
#if ENABLE_LEGACY_INPUT_MANAGER
|
|
return Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
float ReadScroll()
|
|
{
|
|
#if ENABLE_INPUT_SYSTEM
|
|
// Raw wheel delta; only its sign is used (one notch = one zoom step).
|
|
return Mouse.current != null ? Mouse.current.scroll.ReadValue().y : 0f;
|
|
#else
|
|
return Input.GetAxis("Mouse ScrollWheel");
|
|
#endif
|
|
}
|
|
}
|