jankenbots/Assets/Scripts/Camera/CameraDirector.cs
megaproxy 30d0b27813 controls: on-foot always-on mouse-look + camera-relative WASD + E/Q claim
- CameraDirector: AvatarFollow now uses always-on mouse-look (no RMB) with a
  locked/hidden cursor; Esc toggles the cursor free; RobotFollow keeps RMB-look.
- PlayerAvatar: movement is camera-relative (W = camera forward, character turns
  to face where it runs) instead of world/cardinal.
- PlayerAvatar: E claims a tread on foot, Q leaves while seated (locked cursor
  makes the OnGUI buttons unclickable).
- Verified in play: cursor locks in on-foot mode, camera-relative basis correct,
  no runtime errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:02:22 +01:00

196 lines
8.2 KiB
C#

using Unity.Cinemachine;
using UnityEngine;
using UnityEngine.InputSystem;
namespace Jankenbots.Prototype
{
/// <summary>
/// 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 <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.
///
/// 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("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<CinemachineBrain>() == null) gameObject.AddComponent<CinemachineBrain>();
var legacy = GetComponent<FollowCamera>();
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<CinemachineCamera>();
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<PlayerAvatar>(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;
}
}
}