using Unity.Cinemachine; using UnityEngine; using UnityEngine.InputSystem; namespace Jankenbots.Prototype { /// /// 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 — 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). /// /// 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. /// /// 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("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; // Orbit state shared by the two follow cams (player-controlled). float _yaw; float _pitch = 18f; float _dist = 8f; float _cineAngle; bool _cursorFree; // Esc toggles the locked cursor free (for GUI / to escape play) PlayerAvatar _local; void Start() { if (GetComponent() == null) gameObject.AddComponent(); 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 holder = new GameObject("CameraRigs").transform; _avatar = Make(holder, "CM_AvatarFollow"); _robot = Make(holder, "CM_RobotFollow"); _stand = Make(holder, "CM_StandView"); _orbit = Make(holder, "CM_CinematicOrbit"); _all = new[] { _avatar, _robot, _stand, _orbit }; Apply(_mode); } CinemachineCamera Make(Transform parent, string name) { var go = new GameObject(name); go.transform.SetParent(parent, false); var vc = go.AddComponent(); vc.Priority = 0; return vc; } void Update() { if (_local == null) _local = FindLocal(); var kb = Keyboard.current; if (kb != null && kb.vKey.wasPressedThisFrame) { _mode = (Mode)(((int)_mode + 1) % _all.Length); _userOverride = true; Apply(_mode); } // 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); } } // Cursor + look. On-foot (AvatarFollow) uses ALWAYS-ON mouse-look with a // locked cursor (no RMB needed); other modes keep the cursor free (RobotFollow // orbits while RMB is held). Esc toggles the cursor free to reach GUI / escape. if (kb != null && kb.escapeKey.wasPressedThisFrame) _cursorFree = !_cursorFree; bool avatarLook = _mode == Mode.AvatarFollow && !_cursorFree; Cursor.lockState = avatarLook ? CursorLockMode.Locked : CursorLockMode.None; Cursor.visible = !avatarLook; if (_mode == Mode.AvatarFollow || _mode == Mode.RobotFollow) { var m = Mouse.current; if (m != null) { bool look = _mode == Mode.AvatarFollow ? avatarLook : m.rightButton.isPressed; if (look) { 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) PoseOrbit(_avatar, _local.transform); if (_bot != null) { PoseOrbit(_robot, _bot); _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) { _stand.transform.position = _standOverlook.position + Vector3.up * 1.5f; _stand.transform.rotation = Quaternion.LookRotation( (_bot.position + Vector3.up * lookHeight) - _stand.transform.position); } } } // Player-controllable orbit: spherical offset from the target driven by _yaw/_pitch/_dist. void PoseOrbit(CinemachineCamera vc, Transform target) { 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(focus - pos); } PlayerAvatar FindLocal() { foreach (var a in FindObjectsByType(FindObjectsSortMode.None)) if (a.IsOwner) return a; return null; } void Apply(Mode m) { 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; } } }