using UnityEngine;
///
/// 让 gameplay 画面在任意分辨率/宽高比下都"完整可见、绝不裁剪"(contain 容纳式),
/// 同时尽量维持轨道的可点击尺寸。避免此前 cover 式补偿(缩小FOV=镜头拉近)把上下/背景裁掉的问题。
///
/// 原理(透视相机、垂直 FOV 控制):轨道/背景是世界空间对象(Z=0 平面)。
/// 垂直半可视世界高 ∝ tan(vFov/2);水平半可视世界宽 ∝ tan(vFov/2) · aspect。
/// 要"包含"住 16:9 设计画面(设计横向范围 + 设计纵向范围)且绝不裁剪,取二者所需 FOV 的较大值:
/// tan(vFov/2) = tan(designVFov/2) · max(1, designAspect / aspect)
/// - 16:9:max(1,1)=1 → vFov = designVFov(与原设计逐像素一致)。
/// - 更宽(21:9/32:9,aspect>designAspect):max=1 → vFov 保持 designVFov。
/// 纵向完整不变、横向自然更宽(显示更多内容/背景),一寸不裁;轨道显得细些——
/// 由已有的点击区兜底(OverlapPointAll+最近轨道匹配,横向无缝)保证仍好点,不误触。
/// - 更窄(4:3,aspect1 → vFov 增大(镜头拉远),保证设计横向范围完整可见,
/// 纵向显示更多,同样不裁剪。
/// 本式**永不缩小 FOV**(不拉近),因此绝不会裁掉设计画面——这是与之前 cover 式的本质区别。
///
/// 只调相机一个表现参数(fieldOfView),不动任何 Transform;音符/轨道/点击区在同一世界平面,
/// 相对对齐不受 FOV 影响;TrackTouchInput 每帧用当前相机状态做屏幕→世界命中,自动跟随。
///
/// 用一个 bool(enableStabilization)开关。关闭时恢复设计 FOV,行为与未加此脚本一致。
///
[RequireComponent(typeof(Camera))]
[DisallowMultipleComponent]
public sealed class TrackScreenRatioStabilizer : MonoBehaviour
{
[Tooltip("是否启用轨道屏占比稳定(按宽高比补偿相机垂直FOV)。关闭则恢复设计FOV。")]
// 关闭:contain 式补偿会在窄屏增大垂直FOV(镜头拉远)=轨道变细+视距变长,二者都不符合需求。
// 改由 CameraResolutionTuner(降Z+增X俯角)在保持垂直FOV恒定(视距恒定)的前提下让轨道铺开。
public bool enableStabilization = false;
[Tooltip("设计基准宽高比(轨道占比在此比例下为设计值)。16:9 = 1.7778。")]
public float designAspect = 16f / 9f;
[Tooltip("设计基准垂直FOV(该相机在16:9下的原始FOV)。留 0 则启动时自动取相机当前FOV。")]
public float designVerticalFov = 0f;
[Tooltip("补偿后垂直FOV的安全范围,避免极端宽高比下过小/过大。")]
public float minFov = 20f;
public float maxFov = 100f;
private Camera _cam;
private float _lastAspect = -1f;
private bool _lastEnabled;
private void OnEnable()
{
_cam = GetComponent();
// 自动记录设计 FOV(仅一次):此时相机 FOV 即 16:9 设计值。
if (designVerticalFov <= 0f && _cam != null)
{
designVerticalFov = _cam.fieldOfView;
}
_lastAspect = -1f; // 强制下一次 Apply
Apply();
}
private void Update()
{
float aspect = GetAspect();
// 宽高比或开关变化时才重算。
if (Mathf.Abs(aspect - _lastAspect) > 0.0001f || enableStabilization != _lastEnabled)
{
Apply();
}
}
private float GetAspect()
{
// 优先用相机自身 aspect(可能被 viewport 影响),否则用屏幕。
if (_cam != null && _cam.aspect > 0.0001f) return _cam.aspect;
return (float)Screen.width / Mathf.Max(1, Screen.height);
}
private void Apply()
{
if (_cam == null) _cam = GetComponent();
if (_cam == null || _cam.orthographic) return; // 仅透视相机适用
if (designVerticalFov <= 0f) designVerticalFov = _cam.fieldOfView;
float aspect = GetAspect();
_lastAspect = aspect;
_lastEnabled = enableStabilization;
if (!enableStabilization)
{
_cam.fieldOfView = designVerticalFov; // 关闭:恢复设计 FOV
return;
}
// contain(容纳、绝不裁剪):tan(vFov/2) = tan(designVFov/2) · max(1, designAspect/aspect)
// max(1, …) 保证 FOV 永不小于设计值(不拉近、不裁上下);窄屏才放大以容纳设计横向范围。
float designHalfTan = Mathf.Tan(designVerticalFov * 0.5f * Mathf.Deg2Rad);
float widen = (aspect > 0.0001f) ? Mathf.Max(1f, designAspect / aspect) : 1f;
float targetHalfTan = designHalfTan * widen;
float targetFov = 2f * Mathf.Atan(targetHalfTan) * Mathf.Rad2Deg;
_cam.fieldOfView = Mathf.Clamp(targetFov, minFov, maxFov);
}
}