From 3082644dbd275942f609197568923a1ee85a3ee4 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sun, 12 Jul 2026 20:50:30 +0100 Subject: [PATCH] camera: RMB-drag look + scroll zoom on follow cams, big zoom-out range Avatar/Robot follow cams now orbit from player input: hold right-mouse + drag to look around (yaw/pitch clamped), scroll to zoom (3..80m so you can pull back and survey the arena). Per-mode default distance on switch, look angle persists. Stand/Cinematic unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- Assets/Scripts/Camera/CameraDirector.cs | 110 +++++++++++++++++------- 1 file changed, 77 insertions(+), 33 deletions(-) diff --git a/Assets/Scripts/Camera/CameraDirector.cs b/Assets/Scripts/Camera/CameraDirector.cs index ecc6e50..c30d6f2 100644 --- a/Assets/Scripts/Camera/CameraDirector.cs +++ b/Assets/Scripts/Camera/CameraDirector.cs @@ -5,36 +5,49 @@ using UnityEngine.InputSystem; namespace Jankenbots.Prototype { /// - /// JANKENBOTS — client-local camera director. Owns four Cinemachine cameras and - /// lets the local player cycle between them; the CinemachineBrain on this Camera - /// blends smoothly on every switch. + /// JANKENBOTS — client-local camera director. Owns four Cinemachine cameras, lets + /// the local player cycle between them and orbit/zoom the follow cams; the + /// CinemachineBrain on this Camera blends smoothly on every switch. /// - /// • AvatarFollow — 3rd-person chase on YOUR character (on-foot). - /// • RobotFollow — chase cam on the bot (driving; the robot-follow cam). + /// • AvatarFollow — orbitable 3rd-person cam on YOUR character (on-foot). + /// • RobotFollow — orbitable chase cam on the bot (driving). /// • StandView — fixed view from your player stand, looking at the bot. /// • CinematicOrbit— slow auto-orbit of the bot (spectate / flavor). /// - /// The default mode auto-follows the control state: on foot → AvatarFollow, seated - /// (driving) → RobotFollow — reading the SAME seat flag as the control handoff - /// (). Press V to cycle manually; claiming - /// or leaving a seat re-asserts the sensible default. + /// LOOK / ZOOM (Avatar & Robot only): hold RIGHT MOUSE and drag to orbit + /// (yaw/pitch), scroll wheel to zoom in/out (out to so + /// you can pull back and see the arena). RMB-drag keeps the cursor free for the + /// Claim/Leave buttons until the walk-up interaction exists. /// - /// The vcams are intentionally "bare" (no procedural body/aim) — the director poses - /// their transforms each LateUpdate from the live targets, and the Brain just blends - /// between whichever is prioritised. Cameras are purely client-local, never networked. + /// Default mode auto-follows the control state (on foot → Avatar, seated → Robot), + /// reading the same seat flag as the control handoff (). + /// Press V to cycle manually. Cameras are purely client-local, never networked. /// [RequireComponent(typeof(Camera))] public class CameraDirector : MonoBehaviour { public enum Mode { AvatarFollow, RobotFollow, StandView, CinematicOrbit } - [Header("Framing")] - public Vector3 avatarOffset = new Vector3(0f, 4f, -6f); - public Vector3 robotOffset = new Vector3(0f, 8f, -12f); - public float orbitRadius = 20f; - public float orbitHeight = 9f; - public float orbitSpeed = 18f; // deg/sec - public float lookHeight = 1.2f; // aim a bit above the target pivot + [Header("Follow distances")] + public float avatarDistance = 8f; + public float robotDistance = 16f; + + [Header("Look / zoom (Avatar & Robot)")] + [Tooltip("Degrees of orbit per pixel of mouse drag (while holding RMB).")] + public float lookSensitivity = 0.18f; + [Tooltip("Metres of zoom per scroll notch.")] + public float zoomSensitivity = 4f; + public float minDistance = 3f; + [Tooltip("How far you can pull the camera back. Big so you can survey the arena.")] + public float maxDistance = 80f; + public float minPitch = -8f; + public float maxPitch = 78f; + public float lookHeight = 1.2f; // aim a bit above the target pivot + + [Header("Cinematic orbit")] + public float orbitRadius = 22f; + public float orbitHeight = 10f; + public float orbitSpeed = 16f; // deg/sec Transform _bot, _standOverlook; CinemachineCamera _avatar, _robot, _stand, _orbit; @@ -42,18 +55,23 @@ namespace Jankenbots.Prototype Mode _mode = Mode.AvatarFollow; bool _userOverride; bool _lastSeated; - float _orbitAngle; + + // Orbit state shared by the two follow cams (player-controlled). + float _yaw; + float _pitch = 18f; + float _dist = 8f; + float _cineAngle; + PlayerAvatar _local; void Start() { if (GetComponent() == null) gameObject.AddComponent(); - // Retire the old bespoke bot-only follow cam if it's still on this camera. var legacy = GetComponent(); if (legacy != null) legacy.enabled = false; - var b = GameObject.Find("Bot"); if (b != null) _bot = b.transform; - var s = GameObject.Find("StandPoint_0"); if (s != null) _standOverlook = s.transform; + var b = GameObject.Find("Bot"); if (b != null) _bot = b.transform; + var s = GameObject.Find("StandPoint_0"); if (s != null) _standOverlook = s.transform; var holder = new GameObject("CameraRigs").transform; _avatar = Make(holder, "CM_AvatarFollow"); @@ -85,31 +103,50 @@ namespace Jankenbots.Prototype Apply(_mode); } - // A seat claim/leave re-asserts the sensible default (clears a manual override). + // Seat claim/leave re-asserts the sensible default (clears a manual override). if (_local != null && _local.IsSeatedLocal != _lastSeated) { _lastSeated = _local.IsSeatedLocal; _userOverride = false; } - if (!_userOverride && _local != null) { Mode want = _local.IsSeatedLocal ? Mode.RobotFollow : Mode.AvatarFollow; if (want != _mode) { _mode = want; Apply(_mode); } } + + // Look / zoom input — only for the orbitable follow cams. + if (_mode == Mode.AvatarFollow || _mode == Mode.RobotFollow) + { + var m = Mouse.current; + if (m != null) + { + if (m.rightButton.isPressed) + { + Vector2 d = m.delta.ReadValue(); + _yaw += d.x * lookSensitivity; + _pitch = Mathf.Clamp(_pitch - d.y * lookSensitivity, minPitch, maxPitch); + } + float scroll = m.scroll.ReadValue().y; + if (Mathf.Abs(scroll) > 0.01f) + _dist = Mathf.Clamp(_dist - Mathf.Sign(scroll) * zoomSensitivity, minDistance, maxDistance); + } + } } void LateUpdate() { - if (_local != null) PoseFollow(_avatar, _local.transform, avatarOffset); + if (_local != null) PoseOrbit(_avatar, _local.transform); if (_bot != null) { - PoseFollow(_robot, _bot, robotOffset); + PoseOrbit(_robot, _bot); - _orbitAngle += orbitSpeed * Time.deltaTime; - Vector3 off = Quaternion.Euler(0f, _orbitAngle, 0f) * new Vector3(0f, orbitHeight, -orbitRadius); - PoseFollow(_orbit, _bot, off); + _cineAngle += orbitSpeed * Time.deltaTime; + Vector3 focus = _bot.position + Vector3.up * lookHeight; + Vector3 cineOff = Quaternion.Euler(0f, _cineAngle, 0f) * new Vector3(0f, orbitHeight, -orbitRadius); + _orbit.transform.position = focus + cineOff; + _orbit.transform.rotation = Quaternion.LookRotation(focus - _orbit.transform.position); if (_standOverlook != null) { @@ -120,11 +157,14 @@ namespace Jankenbots.Prototype } } - void PoseFollow(CinemachineCamera vc, Transform target, Vector3 worldOffset) + // Player-controllable orbit: spherical offset from the target driven by _yaw/_pitch/_dist. + void PoseOrbit(CinemachineCamera vc, Transform target) { - Vector3 pos = target.position + worldOffset; + Vector3 focus = target.position + Vector3.up * lookHeight; + Vector3 dir = Quaternion.Euler(_pitch, _yaw, 0f) * Vector3.back; + Vector3 pos = focus + dir * _dist; vc.transform.position = pos; - vc.transform.rotation = Quaternion.LookRotation((target.position + Vector3.up * lookHeight) - pos); + vc.transform.rotation = Quaternion.LookRotation(focus - pos); } PlayerAvatar FindLocal() @@ -138,6 +178,10 @@ namespace Jankenbots.Prototype { for (int i = 0; i < _all.Length; i++) _all[i].Priority = ((int)m == i) ? 20 : 0; + + // Reset zoom to the mode's default distance on switch (keep the look angle). + if (m == Mode.AvatarFollow) _dist = avatarDistance; + else if (m == Mode.RobotFollow) _dist = robotDistance; } } }