Files
zeling_v2/Assets/_Game/Scripts/Camera/CameraArea.cs

271 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using UnityEngine;
using Unity.Cinemachine;
namespace BaseGames.Camera
{
/// <summary>
/// 相机区域数据组件。一个房间场景内可放置任意数量的 CameraArea
/// 每个区域独立定义限位范围、可视边界、跟随参数与混合配置。
///
/// 运行时由 <see cref="CameraStateController"/> 管理:
/// 每个区域均拥有专属的 <c>_dedicatedCamera</c>,进入该区域时激活其专属 VCam。
/// <c>OverrideFollowBehaviour</c> 为真时,<see cref="CameraStateController"/> 会将本组件中的
/// 跟随参数应用到专属 VCam否则 VCam 保持 Inspector 中的默认值。
/// </summary>
public class CameraArea : MonoBehaviour
{
[Header("限位区域")]
[Tooltip("定义相机移动边界的 BoxCollider通常挂载在子节点 AreaBoundary 上)。\n" +
"会被绑定到专属 VCam 的 CinemachineConfiner3D.BoundingVolume。")]
[SerializeField] private BoxCollider _confinerCollider;
[Header("可视区域(透视相机)")]
[Tooltip("摄像机应显示的最大可视矩形(本地坐标,相对于此 GameObject 的 Transform 位置)。\n" +
"Scene 视图中可直接拖拽四条边编辑,然后点击 Inspector 中的\n" +
"「从可视区域更新限位区域(透视)」按钮将其换算为限位多边形。")]
[SerializeField] private Rect _visibleBounds = new Rect(-12f, -6f, 24f, 12f);
[HideInInspector]
[SerializeField] private float _cameraDepth = 0f;
[Header("镜头配置")]
[Tooltip("相机镜头参数 SO提供 FOV、相机深度等参数。\n" +
"SO 中的 fieldOfView 发生变化时,编辑器会自动重新同步限位多边形。")]
[SerializeField] private CameraLensConfigSO _lensConfig;
// 编辑器通过它检测限位多边形是否需要重新同步(不展示在 Inspector 中)
[HideInInspector]
[SerializeField] private float _lastSyncFOV = 0f;
// ── 跟随行为 ──────────────────────────────────────────────────────────
[Header("跟随行为(专属 VCam 参数)")]
[Tooltip("启用后,激活此区域时将以下参数应用到专属 VCam\n" +
"关闭则 VCam 保持 Inspector 中的默认参数不作任何覆写。")]
[SerializeField] private bool _overrideFollowBehaviour = true;
[Tooltip("玩家跟踪点在屏幕上的位置0 = 中心±0.5 = 边缘)。\n" +
"推荐 (0, -0.1):玩家稍低于中心,上方有更多视野(对横版山洞类游戏第三视角推荐)。")]
[SerializeField] private Vector2 _screenPosition = new Vector2(0f, -0.15f);
[Tooltip("水平X/ 垂直Y跟随阻尼。越大越滞后。\n" +
"推荐X=0.5 Y=0.2(水平稍慢、垂直快速响应)。\n" +
"同时挂载 CameraAsymmetricDampingExtension 时Y 分量自动被清零,改由下方两个字段控制。")]
[SerializeField] private Vector2 _damping = new Vector2(0.5f, 0.2f);
[Tooltip("非对称 Y 阻尼 —— 相机向下(下落)时的 Y 轴阻尼(秒)。越小相机越快跟随,玩家能提前看到地面。")]
[SerializeField] private float _dampingDown = 0.06f;
[Tooltip("非对称 Y 阻尼 —— 相机向上(起跳)时的 Y 轴阻尼(秒)。越大越慢,保留地面视野不被立刻拉高。")]
[SerializeField] private float _dampingUp = 0.65f;
[Tooltip("死区范围(全屏 = 1。玩家在死区内相机不移动产生松散跟随感。\n" +
"推荐X=0.1 Y=0.05。")]
[SerializeField] private Vector2 _deadZoneSize = new Vector2(0.15f, 0.05f);
[Tooltip("引领预测时长0 = 不引领)。相机超前于玩家移动方向,令玩家更早看到前方地形。")]
[Range(0f, 1f)]
[SerializeField] private float _lookaheadTime = 0.28f;
[Tooltip("引领算法平滑度0~30。越大越平滑但预测延迟更大。")]
[Range(0f, 30f)]
[SerializeField] private float _lookaheadSmoothing = 5f;
[Header("下坠视野偏置(需配合 CameraFallBiasExtension")]
[Tooltip("禁用此区域的下坠视野偏置效果。\n"
+ "在垂直高度较小的房间(短走廊 / 矬间)中建议开启,\n"
+ "防止相机因偏置超出 Confiner 边界。")]
[SerializeField] private bool _disableFallBias = false;
// ── 方向感知水平偏置 ──────────────────────────────────────────────────
[Header("方向感知偏置(需配合 CameraFacingBiasExtension")]
[Tooltip("是否覆盖此区域的方向感知水平偏置量。\n"
+ "关闭时使用 CameraFacingBiasExtension 组件的默认值;\n"
+ "开启后可将此区域的偏置设为 0禁用或更小值如窄走廊。")]
[SerializeField] private bool _overrideFacingBias = false;
[Tooltip("方向感知水平偏置量(世界单位)。\n"
+ "0 = 禁用此区域的方向偏置(推荐用于宽度受限的走廊)。\n"
+ "仅在 Override Facing Bias = true 时生效。")]
[Min(0f)]
[SerializeField] private float _facingBiasOverride = 0f;
// ── 轴向约束 ──────────────────────────────────────────────────────────
[Header("轴向约束")]
[Tooltip("锁定相机 X 轴垂直竖井相机仅上下移动X 固定在限位区域中心)。")]
[SerializeField] private bool _lockHorizontal = false;
[Tooltip("锁定相机 Y 轴水平走廊相机仅左右移动Y 固定在限位区域中心)。")]
[SerializeField] private bool _lockVertical = false;
[Header("镜头尺寸(视野缩放)")]
[Tooltip("进入此区域时的目标可视半高世界单位0 = 不覆盖)。\n" +
"等价于正交相机的 OrthographicSize透视相机下自动换算为 FOV。\n" +
"适用于 Boss 战拉远或精密解谜区域拉近。")]
[SerializeField] private float _lensSize = 0f;
[Tooltip("镜头尺寸过渡时长。0 = 瞬间切换。")]
[Min(0f)]
[SerializeField] private float _lensSizeDuration = 0.5f;
[Header("混合配置")]
[SerializeField] private CameraBlendProfileSO _blendProfile;
[Header("相机噪音(氛围震动)")]
[Tooltip("此区域的相机噪音配置Noise Settings 资产)。\n"
+ "洞穴、水下、机械区等需要氛围震动时使用留空则禁用噪音AmplitudeGain = 0。\n"
+ "专属 VCam 已包含 CinemachineBasicMultiChannelPerlin 组件(使用工具创建时自动附加)。")]
[SerializeField] private NoiseSettings _noiseProfile;
[Tooltip("噪音振幅增益0 = 无震动,推荐 0.2 ~ 0.8)。")]
[Min(0f)]
[SerializeField] private float _noiseAmplitude = 0.5f;
[Tooltip("噪音频率增益倍率1 = 资产原始频率,越大震动越快)。")]
[Min(0.01f)]
[SerializeField] private float _noiseFrequency = 1f;
[Header("虚拟相机")]
[Tooltip("此区域的专属 CinemachineCamera。\n" +
"每个区域均应配置自己的专属 VCam可通过编辑器工具Camera Area Setup一键创建。")]
[SerializeField] private CinemachineCamera _dedicatedCamera;
[Tooltip("专属 VCam 激活时使用的优先级,默认 20。")]
[SerializeField] private int _dedicatedPriority = 20;
// ── 公开属性 ──────────────────────────────────────────────────────────
public BoxCollider ConfinerCollider => _confinerCollider;
public CameraLensConfigSO LensConfig => _lensConfig;
public float LastSyncFOV => _lastSyncFOV;
public CameraBlendProfileSO BlendProfile => _blendProfile;
/// <summary>世界坐标可视区域(本地 _visibleBounds + transform.position。</summary>
public Rect VisibleBounds => new Rect(
_visibleBounds.x + transform.position.x,
_visibleBounds.y + transform.position.y,
_visibleBounds.width, _visibleBounds.height);
public bool HasDedicated => _dedicatedCamera != null;
public CinemachineCamera DedicatedCamera => _dedicatedCamera;
public int DedicatedPriority => _dedicatedPriority;
public bool OverrideFollowBehaviour => _overrideFollowBehaviour;
public Vector2 ScreenPosition => _screenPosition;
public Vector2 Damping => _damping;
public Vector2 DeadZoneSize => _deadZoneSize;
public float LookaheadTime => _lookaheadTime;
public float LookaheadSmoothing => _lookaheadSmoothing;
public bool DisableFallBias => _disableFallBias;
public bool OverrideFacingBias => _overrideFacingBias;
public float FacingBiasOverride => _facingBiasOverride;
public bool LockHorizontal => _lockHorizontal;
public bool LockVertical => _lockVertical;
public float DampingDown => _dampingDown;
public float DampingUp => _dampingUp;
public float LensSize => _lensSize;
public float LensSizeDuration => _lensSizeDuration;
public NoiseSettings NoiseProfile => _noiseProfile;
public float NoiseAmplitude => _noiseAmplitude;
public float NoiseFrequency => _noiseFrequency;
/// <summary>
/// 摄像机到场景平面的有效深度(用于透视视口换算)。
/// 来源:区域专有 _cameraDepth>0 时) → LensConfig SO。
/// 未绑定 SO 时返回 0限位同步工具会在 Inspector 中给出警告。
/// </summary>
public float CameraDepth
{
get
{
if (_cameraDepth > 0f) return _cameraDepth;
return _lensConfig != null ? _lensConfig.cameraDepth : 0f;
}
}
// ── 静态注册表 ────────────────────────────────────────────────────────
private static readonly List<CameraArea> s_All = new();
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetStaticState() => s_All.Clear();
/// <summary>
/// 从所有已激活的 <see cref="CameraArea"/> 中找出距 <paramref name="worldPos"/> 最近的一个。
/// 仅考虑拥有专属 VCam 的区域;无可用区域时返回 <c>null</c>。
/// 距离以各区域 <c>transform.position</c> 为参考点。
/// </summary>
public static CameraArea FindNearest(Vector3 worldPos)
{
CameraArea best = null;
float bestSqDist = float.MaxValue;
foreach (var area in s_All)
{
if (area == null || !area.HasDedicated) continue;
float d = (area.transform.position - worldPos).sqrMagnitude;
if (d < bestSqDist) { bestSqDist = d; best = area; }
}
return best;
}
// ── Lifecycle ────────────────────────────────────────────────────────
private void Awake()
{
// 确保专属 VCam 启动时 Priority=0无论 Inspector 保存了什么值。
// CameraStateController.ActivateDedicated 会在进入区域时将其提升到 DedicatedPriority。
if (_dedicatedCamera != null)
_dedicatedCamera.Priority = 0;
}
private void OnEnable()
{
if (Application.isPlaying && !s_All.Contains(this))
s_All.Add(this);
}
private void OnDisable()
{
s_All.Remove(this);
}
// ── Gizmo ────────────────────────────────────────────────────────────
private void OnDrawGizmosSelected()
{
// 黄色:可视区域(本地坐标 + transform.position = 世界坐标)
Vector3 center = new Vector3(
_visibleBounds.center.x + transform.position.x,
_visibleBounds.center.y + transform.position.y, 0f);
Vector3 size = new Vector3(_visibleBounds.width, _visibleBounds.height, 0.01f);
Gizmos.color = new Color(1f, 0.85f, 0.15f, 0.10f);
Gizmos.DrawCube(center, size);
Gizmos.color = new Color(1f, 0.85f, 0.15f, 0.90f);
Gizmos.DrawWireCube(center, size);
// 青色:轴向锁定指示
if ((_lockHorizontal || _lockVertical) && _confinerCollider != null)
{
Gizmos.color = new Color(0.2f, 0.8f, 1f, 0.9f);
var bounds = _confinerCollider.bounds;
if (_lockHorizontal)
Gizmos.DrawLine(
new Vector3(bounds.center.x, bounds.min.y, 0f),
new Vector3(bounds.center.x, bounds.max.y, 0f));
if (_lockVertical)
Gizmos.DrawLine(
new Vector3(bounds.min.x, bounds.center.y, 0f),
new Vector3(bounds.max.x, bounds.center.y, 0f));
}
// 青绿:专有 VCam 指示线
if (_dedicatedCamera != null)
{
Gizmos.color = new Color(0.2f, 1f, 0.8f, 0.8f);
Gizmos.DrawLine(transform.position,
_dedicatedCamera.transform.position);
}
}
}
}