101 lines
4.8 KiB
C#
101 lines
4.8 KiB
C#
using UnityEngine;
|
||
|
||
/// <summary>
|
||
/// 让 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,aspect<designAspect):max>1 → vFov 增大(镜头拉远),保证设计横向范围完整可见,
|
||
/// 纵向显示更多,同样不裁剪。
|
||
/// 本式**永不缩小 FOV**(不拉近),因此绝不会裁掉设计画面——这是与之前 cover 式的本质区别。
|
||
///
|
||
/// 只调相机一个表现参数(fieldOfView),不动任何 Transform;音符/轨道/点击区在同一世界平面,
|
||
/// 相对对齐不受 FOV 影响;TrackTouchInput 每帧用当前相机状态做屏幕→世界命中,自动跟随。
|
||
///
|
||
/// 用一个 bool(enableStabilization)开关。关闭时恢复设计 FOV,行为与未加此脚本一致。
|
||
/// </summary>
|
||
[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<Camera>();
|
||
// 自动记录设计 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<Camera>();
|
||
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);
|
||
}
|
||
}
|