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:
1 parent
6e8ab4bc6d
commit
dcb1fc42a6
12 files changed
+358
-55
No files matched your search
@@ -0,0 +1,29 @@
|
||||
/// <summary>
|
||||
/// Base class for a high-level game mode (state) handled by
|
||||
/// <see cref="GameModeManager"/>. A mode configures the relevant scene systems
|
||||
/// when it becomes active (<see cref="Enter"/>), can clean up when it is left
|
||||
/// (<see cref="Exit"/>) and may run per-frame logic in <see cref="Tick"/>.
|
||||
///
|
||||
/// Modes are plain C# classes (not MonoBehaviours). To add a new mode, derive
|
||||
/// from this class and register an instance in GameModeManager.BuildModes().
|
||||
/// </summary>
|
||||
public abstract class GameMode
|
||||
{
|
||||
/// <summary>The owning manager, providing access to the controlled systems.</summary>
|
||||
protected GameModeManager Manager { get; private set; }
|
||||
|
||||
/// <summary>Human-readable name (shown in logs / debug UI).</summary>
|
||||
public abstract string DisplayName { get; }
|
||||
|
||||
/// <summary>Wires the mode to its manager. Called once during registration.</summary>
|
||||
internal void Bind(GameModeManager manager) => Manager = manager;
|
||||
|
||||
/// <summary>Called once when this mode becomes the active mode.</summary>
|
||||
public virtual void Enter() { }
|
||||
|
||||
/// <summary>Called once when this mode stops being the active mode.</summary>
|
||||
public virtual void Exit() { }
|
||||
|
||||
/// <summary>Called every frame while this mode is the active mode.</summary>
|
||||
public virtual void Tick() { }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1dbd23638c056cc4db7ce7c0ffffe53c
|
||||
@@ -0,0 +1,117 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
using UnityEngine.InputSystem;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Central state manager that switches the game between high-level modes.
|
||||
/// Currently two modes exist:
|
||||
/// 0. Player Control - WASD movement, camera orbits freely with the mouse.
|
||||
/// 1. Paint - player stands still, left mouse paints the mesh, the
|
||||
/// camera orbits only while the right mouse button is held.
|
||||
///
|
||||
/// Pressing the switch key (Tab) cycles to the next registered mode, so adding
|
||||
/// more modes later automatically extends the cycle.
|
||||
///
|
||||
/// The manager owns references to the systems a mode may toggle (player
|
||||
/// controller, orbit camera input, mesh painter) and exposes them to the modes.
|
||||
/// Modes are plain C# classes derived from <see cref="GameMode"/>; register
|
||||
/// additional ones in <see cref="BuildModes"/>.
|
||||
///
|
||||
/// Attach this to a single, persistent GameObject in the scene.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class GameModeManager : MonoBehaviour
|
||||
{
|
||||
[Header("Controlled Systems")]
|
||||
[Tooltip("Player movement controller. Empty = found automatically in the scene.")]
|
||||
public PlayerCharacterController player;
|
||||
|
||||
[Tooltip("Third-person orbit camera input. Empty = found automatically in the scene.")]
|
||||
public ThirdPersonOrbitInput cameraInput;
|
||||
|
||||
[Tooltip("Mesh painter. Empty = found automatically in the scene.")]
|
||||
public MeshPainter painter;
|
||||
|
||||
[Header("Switching")]
|
||||
[Tooltip("Key that cycles to the next mode.")]
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
public Key switchKey = Key.Tab;
|
||||
#else
|
||||
public KeyCode switchKey = KeyCode.Tab;
|
||||
#endif
|
||||
|
||||
[Tooltip("Index of the mode that is active at startup (0 = Player Control).")]
|
||||
public int startModeIndex = 0;
|
||||
|
||||
// --- accessors used by the modes ---
|
||||
public PlayerCharacterController Player => player;
|
||||
public ThirdPersonOrbitInput CameraInput => cameraInput;
|
||||
public MeshPainter Painter => painter;
|
||||
|
||||
private readonly List<GameMode> _modes = new List<GameMode>();
|
||||
private int _activeIndex = -1;
|
||||
|
||||
/// <summary>The currently active mode (null before startup).</summary>
|
||||
public GameMode ActiveMode =>
|
||||
(_activeIndex >= 0 && _activeIndex < _modes.Count) ? _modes[_activeIndex] : null;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Auto-resolve references so the manager works with minimal wiring.
|
||||
if (player == null) player = FindFirstObjectByType<PlayerCharacterController>();
|
||||
if (cameraInput == null) cameraInput = FindFirstObjectByType<ThirdPersonOrbitInput>();
|
||||
if (painter == null) painter = FindFirstObjectByType<MeshPainter>();
|
||||
|
||||
BuildModes();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (_modes.Count > 0)
|
||||
SwitchTo(Mathf.Clamp(startModeIndex, 0, _modes.Count - 1));
|
||||
}
|
||||
|
||||
/// <summary>Registers all available modes. Add new modes here.</summary>
|
||||
private void BuildModes()
|
||||
{
|
||||
_modes.Clear();
|
||||
_modes.Add(new PlayerControlMode());
|
||||
_modes.Add(new PaintMode());
|
||||
|
||||
foreach (var m in _modes)
|
||||
m.Bind(this);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_modes.Count == 0) return;
|
||||
|
||||
if (WasSwitchPressed())
|
||||
SwitchTo((_activeIndex + 1) % _modes.Count);
|
||||
|
||||
ActiveMode?.Tick();
|
||||
}
|
||||
|
||||
/// <summary>Activates the mode at <paramref name="index"/> (Exit old, Enter new).</summary>
|
||||
public void SwitchTo(int index)
|
||||
{
|
||||
if (index < 0 || index >= _modes.Count || index == _activeIndex) return;
|
||||
|
||||
ActiveMode?.Exit();
|
||||
_activeIndex = index;
|
||||
ActiveMode.Enter();
|
||||
|
||||
Debug.Log($"[GameModeManager] Mode -> {ActiveMode.DisplayName}", this);
|
||||
}
|
||||
|
||||
private bool WasSwitchPressed()
|
||||
{
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
return Keyboard.current != null && Keyboard.current[switchKey].wasPressedThisFrame;
|
||||
#else
|
||||
return Input.GetKeyDown(switchKey);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 730466b26610d95498d22ab748d855a7
|
||||
@@ -29,6 +29,12 @@ using UnityEngine.InputSystem;
|
||||
[DisallowMultipleComponent]
|
||||
public class MeshPainter : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// When false, mouse painting is ignored (the canvas/material stays as-is).
|
||||
/// Toggled by the GameModeManager (painting is only enabled in paint mode).
|
||||
/// </summary>
|
||||
public bool PaintingEnabled { get; set; } = true;
|
||||
|
||||
[Header("Backend")]
|
||||
[Tooltip("ON = GPU painting (shader/RenderTexture, fast). OFF = CPU painting. " +
|
||||
"Can be toggled live in Play mode - the painted content is carried over.")]
|
||||
@@ -429,6 +435,13 @@ public class MeshPainter : MonoBehaviour
|
||||
// Live backend switching.
|
||||
if (useGpu != _gpuActive) SwitchBackend(useGpu);
|
||||
|
||||
// Painting disabled by the game mode (e.g. player-control mode): do nothing.
|
||||
if (!PaintingEnabled)
|
||||
{
|
||||
_hasLastScreen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (updateColliderEveryFrame && _skinned != null)
|
||||
{
|
||||
BakeColliderMesh();
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/// <summary>
|
||||
/// Mode 2: the player stands still and the mesh is painted with the left mouse
|
||||
/// button. The camera orbits only while the right mouse button is held, so
|
||||
/// dragging with the left button paints instead of rotating the view.
|
||||
/// </summary>
|
||||
public class PaintMode : GameMode
|
||||
{
|
||||
public override string DisplayName => "Paint";
|
||||
|
||||
public override void Enter()
|
||||
{
|
||||
if (Manager.Player != null)
|
||||
Manager.Player.ControlEnabled = false;
|
||||
|
||||
if (Manager.CameraInput != null)
|
||||
Manager.CameraInput.LookMode = ThirdPersonOrbitInput.OrbitLookMode.HoldRightMouseButton;
|
||||
|
||||
if (Manager.Painter != null)
|
||||
Manager.Painter.PaintingEnabled = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4815d31a23d2b14fb3fd22d9e13630d
|
||||
@@ -71,6 +71,13 @@ public class PlayerCharacterController : MonoBehaviour
|
||||
[Range(0f, 1f)]
|
||||
public float slideControl = 0.3f;
|
||||
|
||||
/// <summary>
|
||||
/// When false, movement/jump input is ignored (the player stands still), but
|
||||
/// gravity and ground contact keep running so the character stays grounded.
|
||||
/// Toggled by the GameModeManager (e.g. disabled in paint mode).
|
||||
/// </summary>
|
||||
public bool ControlEnabled { get; set; } = true;
|
||||
|
||||
private CharacterController _controller;
|
||||
private float _verticalVelocity;
|
||||
|
||||
@@ -92,21 +99,26 @@ public class PlayerCharacterController : MonoBehaviour
|
||||
float h = 0f, v = 0f;
|
||||
bool jumpPressed = false;
|
||||
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
var keyboard = Keyboard.current;
|
||||
if (keyboard != null)
|
||||
// Skip input entirely when control is disabled (e.g. paint mode); gravity
|
||||
// and grounding below still run so the player stays planted on the ground.
|
||||
if (ControlEnabled)
|
||||
{
|
||||
if (keyboard.aKey.isPressed) h -= 1f;
|
||||
if (keyboard.dKey.isPressed) h += 1f;
|
||||
if (keyboard.sKey.isPressed) v -= 1f;
|
||||
if (keyboard.wKey.isPressed) v += 1f;
|
||||
jumpPressed = keyboard.spaceKey.wasPressedThisFrame;
|
||||
}
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
var keyboard = Keyboard.current;
|
||||
if (keyboard != null)
|
||||
{
|
||||
if (keyboard.aKey.isPressed) h -= 1f;
|
||||
if (keyboard.dKey.isPressed) h += 1f;
|
||||
if (keyboard.sKey.isPressed) v -= 1f;
|
||||
if (keyboard.wKey.isPressed) v += 1f;
|
||||
jumpPressed = keyboard.spaceKey.wasPressedThisFrame;
|
||||
}
|
||||
#else
|
||||
h = Input.GetAxisRaw("Horizontal");
|
||||
v = Input.GetAxisRaw("Vertical");
|
||||
jumpPressed = Input.GetButtonDown("Jump");
|
||||
h = Input.GetAxisRaw("Horizontal");
|
||||
v = Input.GetAxisRaw("Vertical");
|
||||
jumpPressed = Input.GetButtonDown("Jump");
|
||||
#endif
|
||||
}
|
||||
|
||||
// --- Build the horizontal move direction ----------------------------
|
||||
Vector3 inputDir = new Vector3(h, 0f, v);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/// <summary>
|
||||
/// Mode 1: the player walks with WASD/Space and the camera orbits freely with
|
||||
/// the mouse (cursor locked/hidden). Mesh painting is disabled.
|
||||
/// </summary>
|
||||
public class PlayerControlMode : GameMode
|
||||
{
|
||||
public override string DisplayName => "Player Control";
|
||||
|
||||
public override void Enter()
|
||||
{
|
||||
if (Manager.Player != null)
|
||||
Manager.Player.ControlEnabled = true;
|
||||
|
||||
if (Manager.CameraInput != null)
|
||||
Manager.CameraInput.LookMode = ThirdPersonOrbitInput.OrbitLookMode.AlwaysActive;
|
||||
|
||||
if (Manager.Painter != null)
|
||||
Manager.Painter.PaintingEnabled = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c2f4d0884ba29d849afcaf6121027023
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user