Add free-camera mode with player-equivalent collision
Introduce a free-fly camera as the next entry in the Tab mode cycle (Player Control -> Paint -> Free Cam): - FreeCamController: dedicated CinemachineCamera plus a CharacterController mirroring the player capsule (radius 0.1, height 1.3, slope 59, step 0.15) so the free camera collides with walls/ceiling/floor exactly like the player. WASD/Q-E (and Space/Ctrl) fly relative to view; mouse looks. - FreeCamMode: plain-C# GameMode mirroring PaintMode, with an Exit() that restores camera priority and re-enables player control + camera input. - GameModeManager: register freeCam in the Tab cycle (gated like player). - Scene: FreeCamera object wired, Main Camera custom blends (0.4s) for the vcam priority hand-off. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
0edc59555a
commit
a3ef7055b4
10 files changed
+472
-19
No files matched your search
@@ -0,0 +1,194 @@
|
||||
using UnityEngine;
|
||||
using Unity.Cinemachine;
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
using UnityEngine.InputSystem;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Free-flying spectator camera. Drives its OWN dedicated <see cref="CinemachineCamera"/> 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 <see cref="CharacterController"/> capsule sized like the
|
||||
/// player's, so the freecam slides along world geometry instead of clipping through walls/floors.
|
||||
///
|
||||
/// Activation is driven by <see cref="FreeCamMode"/> via <see cref="SetActive"/>: 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.
|
||||
/// </summary>
|
||||
[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("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;
|
||||
float _yaw;
|
||||
float _pitch;
|
||||
bool _ignoredPlayer;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
_controller = GetComponent<CharacterController>();
|
||||
_vcam = GetComponent<CinemachineCamera>();
|
||||
if (!_vcam)
|
||||
Debug.LogError("[FreeCamController] No CinemachineCamera on this GameObject; freecam cannot go live.", this);
|
||||
else
|
||||
_vcam.Priority = inactivePriority;
|
||||
|
||||
// Dormant until FreeCamMode enters; nothing reads input or moves the rig meanwhile.
|
||||
enabled = false;
|
||||
}
|
||||
|
||||
/// <summary>Enter/leave freecam. Raises/lowers the vcam priority, snaps to the live view and
|
||||
/// manages the cursor. Called by <see cref="FreeCamMode"/>.</summary>
|
||||
public void SetActive(bool active)
|
||||
{
|
||||
if (_vcam)
|
||||
_vcam.Priority = active ? activePriority : inactivePriority;
|
||||
|
||||
if (active)
|
||||
{
|
||||
IgnorePlayerCollision();
|
||||
SyncToCurrentView();
|
||||
SetCursorLocked(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetCursorLocked(false);
|
||||
}
|
||||
|
||||
enabled = active;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
LookTick();
|
||||
MoveTick();
|
||||
}
|
||||
|
||||
void LookTick()
|
||||
{
|
||||
Vector2 delta = ReadMouseDelta();
|
||||
_yaw += delta.x * horizontalSensitivity;
|
||||
float vs = verticalSensitivity * (invertY ? 1f : -1f);
|
||||
_pitch = Mathf.Clamp(_pitch + delta.y * vs, -maxPitch, maxPitch);
|
||||
// Rotation is unconstrained by the CharacterController; only position is.
|
||||
transform.rotation = Quaternion.Euler(_pitch, _yaw, 0f);
|
||||
}
|
||||
|
||||
void MoveTick()
|
||||
{
|
||||
Vector3 dir = ReadMoveDirection();
|
||||
if (dir.sqrMagnitude > 1f)
|
||||
dir.Normalize();
|
||||
|
||||
float speed = moveSpeed * (SprintHeld() ? sprintMultiplier : 1f);
|
||||
_controller.Move(dir * (speed * Time.deltaTime));
|
||||
}
|
||||
|
||||
/// <summary>Place the rig at the currently rendered view so going live produces no visible jump.</summary>
|
||||
void SyncToCurrentView()
|
||||
{
|
||||
var cam = Camera.main;
|
||||
if (!cam) return;
|
||||
|
||||
// CharacterController forbids direct transform writes while enabled, so toggle it for the teleport.
|
||||
bool wasEnabled = _controller.enabled;
|
||||
_controller.enabled = false;
|
||||
transform.SetPositionAndRotation(cam.transform.position, cam.transform.rotation);
|
||||
_controller.enabled = wasEnabled;
|
||||
|
||||
Vector3 e = transform.rotation.eulerAngles;
|
||||
_yaw = e.y;
|
||||
float p = e.x > 180f ? e.x - 360f : e.x;
|
||||
_pitch = Mathf.Clamp(p, -maxPitch, maxPitch);
|
||||
}
|
||||
|
||||
/// <summary>Stop the freecam capsule from being blocked by the (frozen) player's capsule.</summary>
|
||||
void IgnorePlayerCollision()
|
||||
{
|
||||
if (_ignoredPlayer) return;
|
||||
_ignoredPlayer = true;
|
||||
|
||||
var player = FindFirstObjectByType<PlayerCharacterController>();
|
||||
if (player)
|
||||
{
|
||||
var playerCC = player.GetComponent<CharacterController>();
|
||||
if (playerCC)
|
||||
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
|
||||
}
|
||||
|
||||
Vector2 ReadMouseDelta()
|
||||
{
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
return Mouse.current != null ? Mouse.current.delta.ReadValue() : Vector2.zero;
|
||||
#else
|
||||
return new Vector2(Input.GetAxisRaw("Mouse X"), Input.GetAxisRaw("Mouse Y")) * 10f;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void SetCursorLocked(bool locked)
|
||||
{
|
||||
Cursor.lockState = locked ? CursorLockMode.Locked : CursorLockMode.None;
|
||||
Cursor.visible = !locked;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user