Add game mode manager with player-control and paint modes

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>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-06-22 15:13:46 +02:00
1 parent 6e8ab4bc6d
commit dcb1fc42a6
12 files changed
+358 -55

No files matched your search

+73 -39
View File
@@ -8,10 +8,14 @@ using UnityEngine.InputSystem;
/// Feeds mouse input into a <see cref="CinemachineOrbitalFollow"/> so the camera
/// orbits around the follow target like a classic third-person camera.
///
/// Rotation is always on by default and the cursor is locked/hidden while looking.
/// Pressing the toggle key (Tab) unlocks the cursor and disables camera rotation,
/// so you can freely use the mouse (e.g. paint the mesh with the left button or
/// click UI). Pressing it again re-locks the cursor and re-enables rotation.
/// 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
@@ -25,6 +29,24 @@ using UnityEngine.InputSystem;
[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;
@@ -35,23 +57,11 @@ public class ThirdPersonOrbitInput : MonoBehaviour
[Tooltip("If ON, moving the mouse up tilts the camera down (inverted pitch).")]
public bool invertY = false;
[Header("Toggle")]
[Tooltip("Key that toggles between look mode (cursor locked, camera rotates) and " +
"free mode (cursor unlocked, camera rotation disabled).")]
#if ENABLE_INPUT_SYSTEM
public Key toggleKey = Key.Tab;
#else
public KeyCode toggleKey = KeyCode.Tab;
#endif
[Tooltip("If ON, the camera starts in look mode (rotation enabled, cursor locked).")]
public bool startInLookMode = true;
[Header("Zoom")]
[Tooltip("If ON, the mouse wheel zooms the camera in and out (changes orbit radius).")]
public bool enableZoom = true;
[Tooltip("How much the orbit radius changes per mouse wheel notch.")]
[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).")]
@@ -61,14 +71,21 @@ public class ThirdPersonOrbitInput : MonoBehaviour
public float maxRadius = 8f;
[Header("Cursor")]
[Tooltip("If ON, the cursor is locked and hidden while in look mode (rotation " +
"enabled), and freed in free mode so you can use the mouse normally.")]
public bool lockCursorInLookMode = true;
[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;
// True = look mode (cursor locked, camera rotates). False = free mode.
bool _rotationEnabled = true;
/// <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()
{
@@ -79,7 +96,6 @@ public class ThirdPersonOrbitInput : MonoBehaviour
void OnEnable()
{
_rotationEnabled = startInLookMode;
ApplyCursorState();
}
@@ -94,14 +110,14 @@ public class ThirdPersonOrbitInput : MonoBehaviour
if (_orbit == null)
return;
// Tab toggles between look mode and free mode.
if (WasTogglePressed())
{
_rotationEnabled = !_rotationEnabled;
ApplyCursorState();
}
bool rotating = ShouldRotate();
if (_rotationEnabled)
// 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)
@@ -116,14 +132,31 @@ public class ThirdPersonOrbitInput : MonoBehaviour
{
float scroll = ReadScroll();
if (!Mathf.Approximately(scroll, 0f))
_orbit.Radius = Mathf.Clamp(_orbit.Radius - scroll * zoomSpeed, minRadius, maxRadius);
{
// 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 (lockCursorInLookMode)
SetCursorLocked(_rotationEnabled);
if (manageCursor)
SetCursorLocked(ShouldRotate());
}
/// <summary>
@@ -155,12 +188,12 @@ public class ThirdPersonOrbitInput : MonoBehaviour
Cursor.visible = !locked;
}
bool WasTogglePressed()
bool IsRightMouseHeld()
{
#if ENABLE_INPUT_SYSTEM
return Keyboard.current != null && Keyboard.current[toggleKey].wasPressedThisFrame;
return Mouse.current != null && Mouse.current.rightButton.isPressed;
#else
return Input.GetKeyDown(toggleKey);
return Input.GetMouseButton(1);
#endif
}
@@ -177,10 +210,11 @@ public class ThirdPersonOrbitInput : MonoBehaviour
float ReadScroll()
{
#if ENABLE_INPUT_SYSTEM
// Scroll y is reported in steps of ~120 per notch; normalize to ~1 per notch.
return Mouse.current != null ? Mouse.current.scroll.ReadValue().y / 120f : 0f;
// 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") * 10f;
return Input.GetAxis("Mouse ScrollWheel");
#endif
}
}