- Smooth acceleration/deceleration on ground and air movement - Interpolated scroll zoom and low-pass smoothed orbit rotation - Drive Animator "Running" bool while moving on the ground - Add Paintman animator controllers and animation clips - Update README and GAME_DESIGN docs for sprint, accel, camera smoothing and animator hookup Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
221 lines
7.1 KiB
C#
221 lines
7.1 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("World units the orbit radius changes per mouse wheel notch.")]
|
|
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).")]
|
|
public float minRadius = 1.5f;
|
|
|
|
[Tooltip("Farthest orbit radius (zoom out).")]
|
|
public float maxRadius = 8f;
|
|
|
|
[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);
|
|
|
|
void Update()
|
|
{
|
|
if (!_orbit)
|
|
return;
|
|
|
|
bool modeWantsLook = ShouldRotate();
|
|
|
|
if (manageCursor)
|
|
SetCursorLocked(modeWantsLook);
|
|
|
|
if (modeWantsLook && !LookSuppressed)
|
|
{
|
|
Vector2 delta = ReadMouseDelta();
|
|
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)
|
|
{
|
|
_targetRadius = _orbit.Radius;
|
|
_zoomInit = true;
|
|
}
|
|
|
|
float scroll = 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.
|
|
_targetRadius = Mathf.Clamp(_targetRadius - Mathf.Sign(scroll) * zoomSpeed, minRadius, maxRadius);
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|