Introduce an extensible game mode system toggled with Tab: - Player Control: WASD movement with always-on mouse camera orbit - Paint: player frozen, left-click paints, right-click holds to orbit Add GameMode base class, GameModeManager, and the two concrete modes. Refactor controlled scripts to expose clean state flags instead of handling input themselves: ThirdPersonOrbitInput.LookMode, PlayerCharacterController.ControlEnabled, MeshPainter.PaintingEnabled. Also fix camera zoom: remove the obsolete /120 wheel normalization so one scroll notch maps to one zoomSpeed step under the new Input System. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
221 lines
7.5 KiB
C#
221 lines
7.5 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 around the follow target like a classic third-person camera.
|
|
///
|
|
/// How the orbit is driven is controlled by <see cref="LookMode"/>:
|
|
/// * AlwaysActive - rotates with every mouse move, cursor locked/hidden.
|
|
/// * HoldRightMouseButton - rotates only while the right mouse button is held;
|
|
/// the cursor is free otherwise (so the left button can paint).
|
|
/// * Disabled - no rotation, cursor free.
|
|
///
|
|
/// The look mode is normally set by the GameModeManager when switching modes,
|
|
/// but can also be configured directly in the Inspector for standalone use.
|
|
///
|
|
/// Horizontal/vertical mouse movement drives the orbital axes (yaw/pitch); the
|
|
/// mouse wheel zooms by changing the orbit radius. The script reads the new Input
|
|
/// System (Mouse.current / Keyboard.current) when enabled and falls back to the
|
|
/// legacy Input Manager otherwise. The project currently runs the new Input System only.
|
|
///
|
|
/// Attach this to the same GameObject as the CinemachineCamera /
|
|
/// CinemachineOrbitalFollow (the camera rig, not the player).
|
|
/// </summary>
|
|
[RequireComponent(typeof(CinemachineOrbitalFollow))]
|
|
[DisallowMultipleComponent]
|
|
public class ThirdPersonOrbitInput : MonoBehaviour
|
|
{
|
|
/// <summary>How the camera orbit is driven.</summary>
|
|
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:\n" +
|
|
"AlwaysActive = rotates with every mouse move (cursor locked).\n" +
|
|
"HoldRightMouseButton = rotates only while RMB is held (cursor free otherwise).\n" +
|
|
"Disabled = no rotation.")]
|
|
[SerializeField] private OrbitLookMode lookMode = OrbitLookMode.AlwaysActive;
|
|
|
|
[Header("Rotation")]
|
|
[Tooltip("Horizontal look sensitivity in degrees of yaw per pixel of mouse movement.")]
|
|
public float horizontalSensitivity = 0.2f;
|
|
|
|
[Tooltip("Vertical look sensitivity in degrees of pitch per pixel of mouse movement.")]
|
|
public float verticalSensitivity = 0.15f;
|
|
|
|
[Tooltip("If ON, moving the mouse up tilts the camera down (inverted pitch).")]
|
|
public bool invertY = false;
|
|
|
|
[Header("Zoom")]
|
|
[Tooltip("If ON, the mouse wheel zooms the camera in and out (changes orbit radius).")]
|
|
public bool enableZoom = true;
|
|
|
|
[Tooltip("How many world units the orbit radius changes per mouse wheel notch.")]
|
|
public float zoomSpeed = 0.5f;
|
|
|
|
[Tooltip("Closest the camera can zoom in (orbit radius in units).")]
|
|
public float minRadius = 1.5f;
|
|
|
|
[Tooltip("Farthest the camera can zoom out (orbit radius in units).")]
|
|
public float maxRadius = 8f;
|
|
|
|
[Header("Cursor")]
|
|
[Tooltip("If ON, the cursor is locked and hidden while the camera is actively " +
|
|
"rotating, and freed otherwise so you can use the mouse normally.")]
|
|
public bool manageCursor = true;
|
|
|
|
CinemachineOrbitalFollow _orbit;
|
|
|
|
/// <summary>
|
|
/// How the camera orbit is currently driven. Setting this also updates the
|
|
/// cursor state immediately (e.g. when GameModeManager switches modes).
|
|
/// </summary>
|
|
public OrbitLookMode LookMode
|
|
{
|
|
get => lookMode;
|
|
set { lookMode = value; ApplyCursorState(); }
|
|
}
|
|
|
|
void Awake()
|
|
{
|
|
_orbit = GetComponent<CinemachineOrbitalFollow>();
|
|
if (_orbit == null)
|
|
Debug.LogError("[ThirdPersonOrbitInput] No CinemachineOrbitalFollow found on this GameObject.", this);
|
|
}
|
|
|
|
void OnEnable()
|
|
{
|
|
ApplyCursorState();
|
|
}
|
|
|
|
void OnDisable()
|
|
{
|
|
// Make sure we never leave the cursor locked/hidden when this is turned off.
|
|
SetCursorLocked(false);
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
if (_orbit == null)
|
|
return;
|
|
|
|
bool rotating = ShouldRotate();
|
|
|
|
// Keep the cursor in sync every frame: in HoldRightMouseButton mode the
|
|
// lock state follows the right mouse button.
|
|
if (manageCursor)
|
|
SetCursorLocked(rotating);
|
|
|
|
if (rotating)
|
|
{
|
|
Vector2 delta = ReadMouseDelta();
|
|
if (delta.sqrMagnitude > 0f)
|
|
{
|
|
ApplyAxisDelta(ref _orbit.HorizontalAxis, delta.x * horizontalSensitivity);
|
|
float pitch = delta.y * verticalSensitivity * (invertY ? 1f : -1f);
|
|
ApplyAxisDelta(ref _orbit.VerticalAxis, pitch);
|
|
}
|
|
}
|
|
|
|
if (enableZoom)
|
|
{
|
|
float scroll = ReadScroll();
|
|
if (!Mathf.Approximately(scroll, 0f))
|
|
{
|
|
// One wheel notch = one zoom step. Use only the sign so the step
|
|
// size is exactly zoomSpeed regardless of the platform's raw wheel
|
|
// magnitude (the new Input System reports ~1 per notch, the legacy
|
|
// Windows path used ~120). Scrolling up (positive) zooms in.
|
|
_orbit.Radius = Mathf.Clamp(_orbit.Radius - Mathf.Sign(scroll) * zoomSpeed, minRadius, maxRadius);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Whether the camera should orbit this frame, based on the look mode.</summary>
|
|
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 <paramref name="amount"/> to an input axis, respecting its range:
|
|
/// wrapping axes (e.g. yaw) wrap around, non-wrapping axes (e.g. pitch) clamp.
|
|
/// </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
|
|
// Mouse.current.delta is already the per-frame pixel movement (no deltaTime).
|
|
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 in Update (one notch = one zoom
|
|
// step), so the platform-dependent magnitude does not matter here.
|
|
return Mouse.current != null ? Mouse.current.scroll.ReadValue().y : 0f;
|
|
#else
|
|
return Input.GetAxis("Mouse ScrollWheel");
|
|
#endif
|
|
}
|
|
}
|