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); } }