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>
187 lines
7.5 KiB
C#
187 lines
7.5 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;
|
|
|
|
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); }
|
|
}
|
|
|
|
// 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) 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;
|
|
}
|
|
}
|
|
}
|