M1: player stand + control handoff + Cinemachine camera system
- Install Cinemachine 3.1.7 - Stand (raised platform behind north wall) with StandPoint_0/_1 - PlayerAvatar: seat-ownership drives the mode — hold a seat => teleport to your stand, freeze, refuse input (control transfers/locks to your tread); release => teleport back, resume on-foot. Resolves the WASD overlap off one flag. - CameraDirector on Main Camera: 4 Cinemachine vcams (AvatarFollow / RobotFollow / StandView / CinematicOrbit), Brain blends; V cycles; default auto-follows the seat flag (on-foot=Avatar, driving=Robot). Retires the old FollowCamera. - Verified in play: claim->teleport+freeze+RobotFollow, release->return+AvatarFollow Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e93e6e76a3
commit
e92be317f5
6 changed files with 490 additions and 25 deletions
143
Assets/Scripts/Camera/CameraDirector.cs
Normal file
143
Assets/Scripts/Camera/CameraDirector.cs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
using Unity.Cinemachine;
|
||||
using UnityEngine;
|
||||
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.
|
||||
///
|
||||
/// • AvatarFollow — 3rd-person chase on YOUR character (on-foot).
|
||||
/// • RobotFollow — chase cam on the bot (driving; the robot-follow cam).
|
||||
/// • 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.
|
||||
///
|
||||
/// 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.
|
||||
/// </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
|
||||
public float lookHeight = 1.2f; // aim a bit above the target pivot
|
||||
|
||||
Transform _bot, _standOverlook;
|
||||
CinemachineCamera _avatar, _robot, _stand, _orbit;
|
||||
CinemachineCamera[] _all;
|
||||
Mode _mode = Mode.AvatarFollow;
|
||||
bool _userOverride;
|
||||
bool _lastSeated;
|
||||
float _orbitAngle;
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// A 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); }
|
||||
}
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
if (_local != null) PoseFollow(_avatar, _local.transform, avatarOffset);
|
||||
|
||||
if (_bot != null)
|
||||
{
|
||||
PoseFollow(_robot, _bot, robotOffset);
|
||||
|
||||
_orbitAngle += orbitSpeed * Time.deltaTime;
|
||||
Vector3 off = Quaternion.Euler(0f, _orbitAngle, 0f) * new Vector3(0f, orbitHeight, -orbitRadius);
|
||||
PoseFollow(_orbit, _bot, off);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PoseFollow(CinemachineCamera vc, Transform target, Vector3 worldOffset)
|
||||
{
|
||||
Vector3 pos = target.position + worldOffset;
|
||||
vc.transform.position = pos;
|
||||
vc.transform.rotation = Quaternion.LookRotation((target.position + Vector3.up * lookHeight) - 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Camera/CameraDirector.cs.meta
Normal file
2
Assets/Scripts/Camera/CameraDirector.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: cd21368c9a8b1064d913c375fb607c0b
|
||||
Loading…
Add table
Add a link
Reference in a new issue