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]>
162 lines
4.3 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|