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,50 +5,68 @@ using UnityEngine.InputSystem;
namespace Jankenbots.Prototype
{
/// <summary>
/// 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
/// (<see cref="PlayerAvatar.IsSeatedLocal"/>). 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 <see cref="maxDistance"/> 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 (<see cref="PlayerAvatar.IsSeatedLocal"/>).
/// Press V to cycle manually. Cameras are purely client-local, never networked.
/// </summary>
[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
[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;
CinemachineCamera[] _all;
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<CinemachineBrain>() == null) gameObject.AddComponent<CinemachineBrain>();
// Retire the old bespoke bot-only follow cam if it's still on this camera.
var legacy = GetComponent<FollowCamera>();
if (legacy != null) legacy.enabled = false;
@ -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;
}
}
}