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) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-07-12 20:50:30 +01:00
parent e92be317f5
commit 3082644dbd

View file

@ -5,36 +5,49 @@ using UnityEngine.InputSystem;
namespace Jankenbots.Prototype namespace Jankenbots.Prototype
{ {
/// <summary> /// <summary>
/// JANKENBOTS — client-local camera director. Owns four Cinemachine cameras and /// JANKENBOTS — client-local camera director. Owns four Cinemachine cameras, lets
/// lets the local player cycle between them; the CinemachineBrain on this Camera /// the local player cycle between them and orbit/zoom the follow cams; the
/// blends smoothly on every switch. /// CinemachineBrain on this Camera blends smoothly on every switch.
/// ///
/// • AvatarFollow — 3rd-person chase on YOUR character (on-foot). /// • AvatarFollow — orbitable 3rd-person cam on YOUR character (on-foot).
/// • RobotFollow — chase cam on the bot (driving; the robot-follow cam). /// • RobotFollow — orbitable chase cam on the bot (driving).
/// • StandView — fixed view from your player stand, looking at the bot. /// • StandView — fixed view from your player stand, looking at the bot.
/// • CinematicOrbit— slow auto-orbit of the bot (spectate / flavor). /// • CinematicOrbit— slow auto-orbit of the bot (spectate / flavor).
/// ///
/// The default mode auto-follows the control state: on foot → AvatarFollow, seated /// LOOK / ZOOM (Avatar & Robot only): hold RIGHT MOUSE and drag to orbit
/// (driving) → RobotFollow — reading the SAME seat flag as the control handoff /// (yaw/pitch), scroll wheel to zoom in/out (out to <see cref="maxDistance"/> so
/// (<see cref="PlayerAvatar.IsSeatedLocal"/>). Press V to cycle manually; claiming /// you can pull back and see the arena). RMB-drag keeps the cursor free for the
/// or leaving a seat re-asserts the sensible default. /// Claim/Leave buttons until the walk-up interaction exists.
/// ///
/// The vcams are intentionally "bare" (no procedural body/aim) — the director poses /// Default mode auto-follows the control state (on foot → Avatar, seated → Robot),
/// their transforms each LateUpdate from the live targets, and the Brain just blends /// reading the same seat flag as the control handoff (<see cref="PlayerAvatar.IsSeatedLocal"/>).
/// between whichever is prioritised. Cameras are purely client-local, never networked. /// Press V to cycle manually. Cameras are purely client-local, never networked.
/// </summary> /// </summary>
[RequireComponent(typeof(Camera))] [RequireComponent(typeof(Camera))]
public class CameraDirector : MonoBehaviour public class CameraDirector : MonoBehaviour
{ {
public enum Mode { AvatarFollow, RobotFollow, StandView, CinematicOrbit } public enum Mode { AvatarFollow, RobotFollow, StandView, CinematicOrbit }
[Header("Framing")] [Header("Follow distances")]
public Vector3 avatarOffset = new Vector3(0f, 4f, -6f); public float avatarDistance = 8f;
public Vector3 robotOffset = new Vector3(0f, 8f, -12f); public float robotDistance = 16f;
public float orbitRadius = 20f;
public float orbitHeight = 9f; [Header("Look / zoom (Avatar & Robot)")]
public float orbitSpeed = 18f; // deg/sec [Tooltip("Degrees of orbit per pixel of mouse drag (while holding RMB).")]
public float lookHeight = 1.2f; // aim a bit above the target pivot 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; Transform _bot, _standOverlook;
CinemachineCamera _avatar, _robot, _stand, _orbit; CinemachineCamera _avatar, _robot, _stand, _orbit;
@ -42,18 +55,23 @@ namespace Jankenbots.Prototype
Mode _mode = Mode.AvatarFollow; Mode _mode = Mode.AvatarFollow;
bool _userOverride; bool _userOverride;
bool _lastSeated; 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; PlayerAvatar _local;
void Start() void Start()
{ {
if (GetComponent<CinemachineBrain>() == null) gameObject.AddComponent<CinemachineBrain>(); if (GetComponent<CinemachineBrain>() == null) gameObject.AddComponent<CinemachineBrain>();
// Retire the old bespoke bot-only follow cam if it's still on this camera.
var legacy = GetComponent<FollowCamera>(); var legacy = GetComponent<FollowCamera>();
if (legacy != null) legacy.enabled = false; if (legacy != null) legacy.enabled = false;
var b = GameObject.Find("Bot"); if (b != null) _bot = b.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 s = GameObject.Find("StandPoint_0"); if (s != null) _standOverlook = s.transform;
var holder = new GameObject("CameraRigs").transform; var holder = new GameObject("CameraRigs").transform;
_avatar = Make(holder, "CM_AvatarFollow"); _avatar = Make(holder, "CM_AvatarFollow");
@ -85,31 +103,50 @@ namespace Jankenbots.Prototype
Apply(_mode); 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) if (_local != null && _local.IsSeatedLocal != _lastSeated)
{ {
_lastSeated = _local.IsSeatedLocal; _lastSeated = _local.IsSeatedLocal;
_userOverride = false; _userOverride = false;
} }
if (!_userOverride && _local != null) if (!_userOverride && _local != null)
{ {
Mode want = _local.IsSeatedLocal ? Mode.RobotFollow : Mode.AvatarFollow; Mode want = _local.IsSeatedLocal ? Mode.RobotFollow : Mode.AvatarFollow;
if (want != _mode) { _mode = want; Apply(_mode); } 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() void LateUpdate()
{ {
if (_local != null) PoseFollow(_avatar, _local.transform, avatarOffset); if (_local != null) PoseOrbit(_avatar, _local.transform);
if (_bot != null) if (_bot != null)
{ {
PoseFollow(_robot, _bot, robotOffset); PoseOrbit(_robot, _bot);
_orbitAngle += orbitSpeed * Time.deltaTime; _cineAngle += orbitSpeed * Time.deltaTime;
Vector3 off = Quaternion.Euler(0f, _orbitAngle, 0f) * new Vector3(0f, orbitHeight, -orbitRadius); Vector3 focus = _bot.position + Vector3.up * lookHeight;
PoseFollow(_orbit, _bot, off); 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) 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.position = pos;
vc.transform.rotation = Quaternion.LookRotation((target.position + Vector3.up * lookHeight) - pos); vc.transform.rotation = Quaternion.LookRotation(focus - pos);
} }
PlayerAvatar FindLocal() PlayerAvatar FindLocal()
@ -138,6 +178,10 @@ namespace Jankenbots.Prototype
{ {
for (int i = 0; i < _all.Length; i++) for (int i = 0; i < _all.Length; i++)
_all[i].Priority = ((int)m == i) ? 20 : 0; _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;
} }
} }
} }