using UnityEngine;
using Unity.Cinemachine;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif
///
/// Free-flying spectator camera. Drives its OWN dedicated by moving
/// this transform with WASD (relative to where it looks), Q/E (or Space/Left Ctrl) for down/up
/// and the mouse for look. Collision is a capsule sized like the
/// player's, so the freecam slides along world geometry instead of clipping through walls/floors.
///
/// Activation is driven by via : it raises the vcam
/// priority so the CinemachineBrain blends to it, snaps the rig to the current view (no jump) and
/// locks the cursor. While inactive the vcam rests at a low priority and this component does nothing.
///
[RequireComponent(typeof(CharacterController))]
[DisallowMultipleComponent]
public class FreeCamController : MonoBehaviour
{
[Header("Flight")]
[Tooltip("Base flight speed (units/second).")]
public float moveSpeed = 6f;
[Tooltip("Speed multiplier while the sprint key (Left Shift) is held.")]
public float sprintMultiplier = 3f;
[Header("Look")]
[Tooltip("Horizontal look sensitivity (degrees of yaw per pixel).")]
public float horizontalSensitivity = 0.2f;
[Tooltip("Vertical look sensitivity (degrees of pitch per pixel).")]
public float verticalSensitivity = 0.15f;
[Tooltip("Invert vertical look (mouse up tilts the camera down).")]
public bool invertY = false;
[Tooltip("Pitch is clamped to +/- this angle (degrees) so the camera never flips over.")]
[Range(1f, 89.9f)]
public float maxPitch = 89f;
[Header("Smoothing")]
[Tooltip("Acceleration/deceleration smoothing for flight, in seconds. 0 = instant (snappy starts/stops); higher = floatier ramp-up and coast-down.")]
[Range(0f, 1f)]
public float moveSmoothTime = 0.12f;
[Tooltip("Look smoothing, in seconds. 0 = raw mouse (instant); small values take the edge off jitter without feeling laggy.")]
[Range(0f, 0.3f)]
public float lookSmoothTime = 0.04f;
[Header("Camera")]
[Tooltip("Priority the dedicated CinemachineCamera takes while active (must beat the gameplay camera).")]
public int activePriority = 100;
[Tooltip("Priority the dedicated CinemachineCamera rests at while inactive (must lose to the gameplay camera).")]
public int inactivePriority = -100;
CharacterController _controller;
CinemachineCamera _vcam;
SmoothedLook _look; // yaw/pitch accumulator + smoothing shared with first person
Vector3 _moveVelocity; // smoothed world-space flight velocity
Vector3 _moveVelocityRef;
void Awake()
{
_controller = GetComponent();
_vcam = GetComponent();
if (!_vcam)
Debug.LogError("[FreeCamController] No CinemachineCamera on this GameObject; freecam cannot go live.", this);
else
_vcam.Priority = inactivePriority;
// Dormant until FreeCamMode enters: disable the capsule too, otherwise it lingers as a live
// collider sitting in the play area and blocks players who walk into it (a CharacterController
// stays a physics collider on its own, independent of this self-disabled script).
_controller.enabled = false;
enabled = false;
}
/// Enter/leave freecam. Raises/lowers the vcam priority, snaps to the live view and
/// manages the cursor. Called by .
public void SetActive(bool active)
{
if (_vcam)
_vcam.Priority = active ? activePriority : inactivePriority;
// Only a flying freecam needs its capsule; keep it out of the physics world otherwise so it
// never blocks players. Must be enabled before SyncToCurrentView, whose teleport preserves the
// current enabled state around the position write.
_controller.enabled = active;
if (active)
{
IgnorePlayerCollision();
SyncToCurrentView();
InputUtil.SetCursorLocked(true);
}
else
{
InputUtil.SetCursorLocked(false);
}
enabled = active;
}
void Update()
{
LookTick();
MoveTick();
}
void LookTick()
{
Vector2 delta = InputUtil.ReadMouseDelta() * MouseSensitivity.Multiplier;
_look.Tick(delta, horizontalSensitivity, verticalSensitivity, invertY, maxPitch, lookSmoothTime);
// Rotation is unconstrained by the CharacterController; only position is.
transform.rotation = _look.Rotation;
}
void MoveTick()
{
Vector3 dir = ReadMoveDirection();
if (dir.sqrMagnitude > 1f)
dir.Normalize();
float speed = moveSpeed * (SprintHeld() ? sprintMultiplier : 1f);
Vector3 targetVelocity = dir * speed;
// SmoothDamp eases accel/decel and is already dt-correct; 0 collapses to instant.
_moveVelocity = moveSmoothTime > 0f
? Vector3.SmoothDamp(_moveVelocity, targetVelocity, ref _moveVelocityRef, moveSmoothTime)
: targetVelocity;
_controller.Move(_moveVelocity * Time.deltaTime);
}
/// Place the rig at the currently rendered view so going live produces no visible jump.
void SyncToCurrentView()
{
var cam = Camera.main;
if (!cam) return;
CharacterTeleport.Move(_controller, transform, cam.transform.position, cam.transform.rotation);
_look.SyncTo(transform.rotation, maxPitch);
_moveVelocity = Vector3.zero;
_moveVelocityRef = Vector3.zero;
}
/// Stop the freecam capsule from being blocked by the frozen players' capsules. Re-runs on
/// every activation on purpose: Physics.IgnoreCollision is reset when the freecam capsule is toggled
/// off/on, and the roster of live players can differ between spectate sessions.
void IgnorePlayerCollision()
{
foreach (var player in FindObjectsByType(FindObjectsSortMode.None))
{
var playerCC = player.GetComponent();
// Only an enabled capsule can take part in a collision-ignore (a spectator's body has its CC
// disabled, and there is nothing to collide with there anyway).
if (playerCC && playerCC.enabled)
Physics.IgnoreCollision(_controller, playerCC, true);
}
}
Vector3 ReadMoveDirection()
{
Vector3 dir = Vector3.zero;
#if ENABLE_INPUT_SYSTEM
var kb = Keyboard.current;
if (kb != null)
{
if (kb.wKey.isPressed) dir += transform.forward;
if (kb.sKey.isPressed) dir -= transform.forward;
if (kb.dKey.isPressed) dir += transform.right;
if (kb.aKey.isPressed) dir -= transform.right;
if (kb.eKey.isPressed || kb.spaceKey.isPressed) dir += Vector3.up;
if (kb.qKey.isPressed || kb.leftCtrlKey.isPressed) dir -= Vector3.up;
}
#else
dir += transform.forward * Input.GetAxisRaw("Vertical");
dir += transform.right * Input.GetAxisRaw("Horizontal");
if (Input.GetKey(KeyCode.E) || Input.GetKey(KeyCode.Space)) dir += Vector3.up;
if (Input.GetKey(KeyCode.Q) || Input.GetKey(KeyCode.LeftControl)) dir -= Vector3.up;
#endif
return dir;
}
bool SprintHeld()
{
#if ENABLE_INPUT_SYSTEM
return Keyboard.current != null && Keyboard.current.leftShiftKey.isPressed;
#else
return Input.GetKey(KeyCode.LeftShift);
#endif
}
}