Camera: Cinemachine-based rig with an anchor, a state director, point-of- interest framing, pan input, and dialogue framing volumes. Player: click-to-move over the NavMesh, a pointer input layer, and a motion arbiter that reconciles direct stick input with navigation-driven movement. The pure math is isolated in NightclubArcadia.Locomotion.Math with an EditMode suite, so the arbitration rules are testable without a scene. Interaction: IClickInteractable plus a bridge that routes pointer clicks to it. Supporting project settings: adds the Cinemachine package, the Interactable and Ground tags the raycasts depend on, and NavMesh agent dimensions matched to the player capsule (radius 0.3, height 1.8, climb 0.25). Co-Authored-By: Claude Opus 5 <[email protected]>
163 lines
4.6 KiB
C#
163 lines
4.6 KiB
C#
using NightclubArcadia.Player;
|
|
using NightclubArcadia.Player.Math;
|
|
using UnityEngine;
|
|
|
|
namespace NightclubArcadia.Cinematics
|
|
{
|
|
/// <summary>
|
|
/// Converts mouse drag-pan into CameraRigAnchor manual offset with recenter rules.
|
|
/// </summary>
|
|
public sealed class CameraPanInput : MonoBehaviour
|
|
{
|
|
[SerializeField] Camera brainCamera;
|
|
[SerializeField] CameraRigAnchor anchor;
|
|
[SerializeField] CameraStateDirector stateDirector;
|
|
[SerializeField] PlayerPointerInput pointerInput;
|
|
[SerializeField] float panMetresPerPixel = 0.02f;
|
|
[SerializeField] float maxPanDistance = 12f;
|
|
[SerializeField] float recenterDelay = 2.5f;
|
|
[SerializeField] float recenterSmoothTime = 0.8f;
|
|
[SerializeField] float movementRecenterDelay = 0.4f;
|
|
|
|
float releaseTimer = float.MaxValue;
|
|
Vector3 recenterVelocity;
|
|
Vector3 camRight;
|
|
Vector3 camForward;
|
|
bool basisInitialized;
|
|
|
|
public bool IsManualPanActive => pointerInput != null && pointerInput.IsPanning || releaseTimer < recenterDelay;
|
|
|
|
void Awake()
|
|
{
|
|
if (anchor == null)
|
|
{
|
|
anchor = GetComponent<CameraRigAnchor>();
|
|
}
|
|
|
|
if (stateDirector == null)
|
|
{
|
|
stateDirector = GetComponent<CameraStateDirector>();
|
|
}
|
|
|
|
if (brainCamera == null)
|
|
{
|
|
brainCamera = Camera.main;
|
|
}
|
|
}
|
|
|
|
void Start()
|
|
{
|
|
if (pointerInput == null)
|
|
{
|
|
var player = GameObject.FindGameObjectWithTag("Player");
|
|
if (player != null)
|
|
{
|
|
pointerInput = player.GetComponent<PlayerPointerInput>();
|
|
}
|
|
}
|
|
|
|
if (pointerInput != null)
|
|
{
|
|
pointerInput.PanDelta += OnPanDelta;
|
|
pointerInput.PanEnded += OnPanEnded;
|
|
}
|
|
}
|
|
|
|
void OnDestroy()
|
|
{
|
|
if (pointerInput != null)
|
|
{
|
|
pointerInput.PanDelta -= OnPanDelta;
|
|
pointerInput.PanEnded -= OnPanEnded;
|
|
}
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
if (anchor == null || stateDirector == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (stateDirector.AllowsManualPan && pointerInput != null && pointerInput.IsPanning)
|
|
{
|
|
releaseTimer = 0f;
|
|
}
|
|
else if (releaseTimer < recenterDelay)
|
|
{
|
|
releaseTimer += Time.deltaTime;
|
|
}
|
|
|
|
if (ShouldShortenRecenter())
|
|
{
|
|
releaseTimer = Mathf.Max(releaseTimer, recenterDelay - movementRecenterDelay);
|
|
}
|
|
|
|
if (!stateDirector.AllowsManualPan)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (releaseTimer >= recenterDelay && (pointerInput == null || !pointerInput.IsPanning))
|
|
{
|
|
anchor.ManualPanOffset = Vector3.SmoothDamp(
|
|
anchor.ManualPanOffset,
|
|
Vector3.zero,
|
|
ref recenterVelocity,
|
|
recenterSmoothTime);
|
|
}
|
|
|
|
anchor.ClampManualPan(maxPanDistance);
|
|
}
|
|
|
|
void OnPanDelta(Vector2 delta)
|
|
{
|
|
if (stateDirector == null || !stateDirector.AllowsManualPan || anchor == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
EnsureCameraBasis();
|
|
anchor.Nudge(MotionInputMath.ScreenDeltaToWorldPan(delta, camRight, camForward, panMetresPerPixel));
|
|
anchor.ClampManualPan(maxPanDistance);
|
|
}
|
|
|
|
void OnPanEnded()
|
|
{
|
|
releaseTimer = 0f;
|
|
}
|
|
|
|
bool ShouldShortenRecenter()
|
|
{
|
|
var player = GameObject.FindGameObjectWithTag("Player");
|
|
if (player == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var arbiter = player.GetComponent<PlayerMotionArbiter>();
|
|
return arbiter != null && arbiter.ActiveSource != MotionSource.None;
|
|
}
|
|
|
|
void EnsureCameraBasis()
|
|
{
|
|
if (brainCamera == null)
|
|
{
|
|
brainCamera = Camera.main;
|
|
}
|
|
|
|
if (brainCamera == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!basisInitialized)
|
|
{
|
|
camRight = brainCamera.transform.right;
|
|
camForward = brainCamera.transform.forward;
|
|
basisInitialized = true;
|
|
}
|
|
}
|
|
}
|
|
}
|