M1: add client-local follow camera (position-follow, anti-nausea)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
41d8e29632
commit
03cbfc8cc9
1 changed files with 124 additions and 0 deletions
124
Assets/Scripts/Camera/FollowCamera.cs
Normal file
124
Assets/Scripts/Camera/FollowCamera.cs
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Jankenbots.Prototype
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// JANKENBOTS M1 — client-local follow camera for the shared bot.
|
||||||
|
///
|
||||||
|
/// WHY THIS EXISTS: two pilots are each fighting for control of one janky body;
|
||||||
|
/// the camera's whole job is to let a friend WATCH that chaos without becoming
|
||||||
|
/// part of it. This is deliberately NOT a NetworkBehaviour — every client frames
|
||||||
|
/// their own view locally off the shared, replicated chassis transform. There is
|
||||||
|
/// nothing here for the host to authorize or sync.
|
||||||
|
///
|
||||||
|
/// THE NAUSEA DECISION: the bot is designed to tip, spin, and lurch (that's the
|
||||||
|
/// whole "janky" bit — see TreadPart's lock-pivot + passive-lean). A camera that
|
||||||
|
/// rigidly inherits the chassis' rotation would roll and yaw right along with it,
|
||||||
|
/// which reads as motion-sickness fuel, not slapstick. So this rig follows the
|
||||||
|
/// chassis POSITION ONLY, holds a FIXED WORLD-SPACE offset (behind + above, using
|
||||||
|
/// world axes, not the bot's own forward/up), and separately looks-at the bot.
|
||||||
|
/// The tradeoff: the camera won't "face the way the bot is facing" the way a
|
||||||
|
/// chase-cam in a racing game would, so if the bot spins in place the framing
|
||||||
|
/// doesn't spin with it — on purpose. If M1 playtesting says the view feels too
|
||||||
|
/// detached (the bot wanders out of frame during a hard turn), the next lever to
|
||||||
|
/// pull is following bot YAW ONLY (ignore pitch/roll) rather than full rotation —
|
||||||
|
/// not implemented here to keep v0.1 dead simple and guaranteed comfortable.
|
||||||
|
///
|
||||||
|
/// Position smooths with SmoothDamp (velocity-aware, no overshoot-then-snap-back);
|
||||||
|
/// the look-at direction smooths separately with a Slerp so the camera doesn't
|
||||||
|
/// whip-pan when the bot lurches sideways. Tuning fields are public so we can dial
|
||||||
|
/// the feel live in the inspector while friends are playing.
|
||||||
|
/// </summary>
|
||||||
|
public class FollowCamera : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Target")]
|
||||||
|
[Tooltip("The bot chassis to follow. Leave empty to auto-find at Start (by name 'Bot', falling back to any SeatManager in the scene) — handy if the bot ends up spawned rather than placed in-scene.")]
|
||||||
|
public Transform target;
|
||||||
|
|
||||||
|
[Tooltip("Name to search for if Target is empty. Matches the in-scene Bot chassis GameObject.")]
|
||||||
|
public string targetFallbackName = "Bot";
|
||||||
|
|
||||||
|
[Header("Framing — fixed WORLD-space offset from the target")]
|
||||||
|
[Tooltip("Offset from the target's position, in WORLD axes (NOT the bot's local axes — see the nausea note in the class doc). Default sits behind and above a bot-scale body.")]
|
||||||
|
public Vector3 worldOffset = new Vector3(0f, 8f, -12f);
|
||||||
|
|
||||||
|
[Header("Smoothing")]
|
||||||
|
[Tooltip("Approximate time (seconds) for the camera position to catch up to the target. Bigger = lazier/smoother, smaller = snappier/closer to rigid.")]
|
||||||
|
[Range(0.02f, 2f)]
|
||||||
|
public float positionSmoothTime = 0.25f;
|
||||||
|
|
||||||
|
[Tooltip("Degrees/sec cap the look-at direction can rotate. Keeps a sudden bot lurch from whip-panning the view.")]
|
||||||
|
public float lookAtDegreesPerSecond = 180f;
|
||||||
|
|
||||||
|
[Tooltip("Extra height added to the look-at point above the target's pivot, so we frame the bot's body rather than staring at its feet.")]
|
||||||
|
public float lookAtHeightOffset = 1f;
|
||||||
|
|
||||||
|
// SmoothDamp's running velocity state — it owns this between calls, we just hand it back each frame.
|
||||||
|
Vector3 _positionVelocity;
|
||||||
|
|
||||||
|
// Current look direction, slewed toward the target each frame — separate from position
|
||||||
|
// smoothing so a fast reposition doesn't also whip the facing around.
|
||||||
|
Quaternion _currentLookRotation;
|
||||||
|
|
||||||
|
void Start()
|
||||||
|
{
|
||||||
|
if (target == null)
|
||||||
|
target = FindTarget();
|
||||||
|
|
||||||
|
if (target != null)
|
||||||
|
{
|
||||||
|
// Snap framing on the first frame instead of smoothing in from wherever the
|
||||||
|
// camera happens to sit in the scene/editor.
|
||||||
|
transform.position = target.position + worldOffset;
|
||||||
|
_currentLookRotation = Quaternion.LookRotation(LookPoint() - transform.position, Vector3.up);
|
||||||
|
transform.rotation = _currentLookRotation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs AFTER Update/physics/NetworkTransform have moved the target this frame, so we're
|
||||||
|
// always chasing its latest replicated position rather than a stale one.
|
||||||
|
void LateUpdate()
|
||||||
|
{
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
target = FindTarget();
|
||||||
|
if (target == null) return; // bot not in the scene yet — nothing to frame
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Position: SmoothDamp toward target + fixed world offset ---------------
|
||||||
|
Vector3 desiredPosition = target.position + worldOffset;
|
||||||
|
transform.position = Vector3.SmoothDamp(
|
||||||
|
transform.position, desiredPosition, ref _positionVelocity, positionSmoothTime);
|
||||||
|
|
||||||
|
// ---- Look-at: Slerp the facing toward the target, capped in degrees/sec -----
|
||||||
|
// Position-follow-only (see class doc) means we still need to actively aim at
|
||||||
|
// the bot rather than inherit its rotation — this is that aim, smoothed so a
|
||||||
|
// sudden tip doesn't snap the view.
|
||||||
|
Vector3 toTarget = LookPoint() - transform.position;
|
||||||
|
if (toTarget.sqrMagnitude > 0.0001f)
|
||||||
|
{
|
||||||
|
Quaternion desiredLook = Quaternion.LookRotation(toTarget, Vector3.up);
|
||||||
|
_currentLookRotation = Quaternion.RotateTowards(
|
||||||
|
_currentLookRotation, desiredLook, lookAtDegreesPerSecond * Time.deltaTime);
|
||||||
|
transform.rotation = _currentLookRotation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 LookPoint() => target.position + Vector3.up * lookAtHeightOffset;
|
||||||
|
|
||||||
|
// Fallback target resolution: by name first (matches the in-scene "Bot" chassis),
|
||||||
|
// then by any SeatManager in the scene (the chassis is where SeatManager lives —
|
||||||
|
// see SeatManager.cs) in case the bot ends up renamed or spawned from a prefab.
|
||||||
|
Transform FindTarget()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(targetFallbackName))
|
||||||
|
{
|
||||||
|
var byName = GameObject.Find(targetFallbackName);
|
||||||
|
if (byName != null) return byName.transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
var seats = Object.FindFirstObjectByType<SeatManager>();
|
||||||
|
return seats != null ? seats.transform : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue