Files
nightclub-arcadia/NightclubArcadia/Assets/Scripts/Camera/CameraPointOfInterestDirector.cs
T
lennartandClaude Opus 5 932cd0582f add camera rig, click-to-move locomotion, and click interaction
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]>
2026-08-25 19:03:18 +02:00

162 lines
4.3 KiB
C#

using System.Collections.Generic;
using UnityEngine;
namespace NightclubArcadia.Cinematics
{
/// <summary>
/// Scores registered POIs and writes bias onto the camera anchor.
/// </summary>
public sealed class CameraPointOfInterestDirector : MonoBehaviour
{
static readonly List<CameraPointOfInterest> Registered = new List<CameraPointOfInterest>();
[SerializeField] CameraRigAnchor anchor;
[SerializeField] CameraStateDirector stateDirector;
[SerializeField] float poiSmoothTime = 1f;
[SerializeField] float poiReleaseTime = 1.4f;
[SerializeField] float maxHeadingAngle = 100f;
Vector3 poiVelocity;
float distanceVelocity;
CameraPointOfInterest activePoi;
public bool HasActivePoi => activePoi != null;
public static void Register(CameraPointOfInterest poi)
{
if (poi != null && !Registered.Contains(poi))
{
Registered.Add(poi);
}
}
public static void Unregister(CameraPointOfInterest poi)
{
Registered.Remove(poi);
}
void Awake()
{
if (anchor == null)
{
anchor = GetComponent<CameraRigAnchor>();
}
if (stateDirector == null)
{
stateDirector = GetComponent<CameraStateDirector>();
}
}
void LateUpdate()
{
if (anchor == null || stateDirector == null)
{
return;
}
var follow = anchor.FollowTarget;
if (follow == null)
{
return;
}
if (!stateDirector.AllowsPointOfInterest)
{
return;
}
if (stateDirector.Current == CameraState.ManualPan)
{
return;
}
activePoi = FindBestPoi(follow);
if (activePoi == null)
{
ReleasePoi();
return;
}
var toPoi = activePoi.Target.position - follow.position;
toPoi.y = 0f;
var targetBias = Vector3.ClampMagnitude(toPoi, activePoi.MaxBias);
anchor.PoiBias = Vector3.SmoothDamp(
anchor.PoiBias,
targetBias,
ref poiVelocity,
poiSmoothTime);
anchor.DistanceBias = Mathf.SmoothDamp(
anchor.DistanceBias,
activePoi.ExtraDistance,
ref distanceVelocity,
poiSmoothTime);
}
CameraPointOfInterest FindBestPoi(Transform follow)
{
CameraPointOfInterest best = null;
var bestScore = 0f;
var forward = follow.forward;
foreach (var poi in Registered)
{
if (poi == null || !poi.isActiveAndEnabled)
{
continue;
}
var offset = poi.Target.position - follow.position;
offset.y = 0f;
var distance = offset.magnitude;
if (distance > poi.TriggerRadius || distance <= 0.01f)
{
continue;
}
var angle = Vector3.Angle(forward, offset);
if (angle > maxHeadingAngle)
{
continue;
}
var score = poi.Weight * (1f - distance / poi.TriggerRadius);
if (score > bestScore)
{
bestScore = score;
best = poi;
}
}
return best;
}
void ReleasePoi()
{
if (anchor == null)
{
return;
}
anchor.PoiBias = Vector3.SmoothDamp(
anchor.PoiBias,
Vector3.zero,
ref poiVelocity,
poiReleaseTime);
anchor.DistanceBias = Mathf.SmoothDamp(
anchor.DistanceBias,
0f,
ref distanceVelocity,
poiReleaseTime);
if (anchor.PoiBias.sqrMagnitude < 0.0001f && Mathf.Abs(anchor.DistanceBias) < 0.01f)
{
activePoi = null;
}
}
}
}