using UnityEngine; using Unity.Cinemachine; namespace BaseGames.Camera { /// /// 相机区域数据组件。一个房间场景内可放置任意数量的 CameraArea, /// 每个区域独立定义限位范围、可视边界、跟随参数与混合配置。 /// /// 运行时由 管理: /// 每个区域均拥有专属的 _dedicatedCamera,进入该区域时激活其专属 VCam。 /// OverrideFollowBehaviour 为真时, 会将本组件中的 /// 跟随参数应用到专属 VCam;否则 VCam 保持 Inspector 中的默认值。 /// 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; /// 世界坐标可视区域(本地 _visibleBounds + transform.position)。 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; /// /// 摄像机到场景平面的有效深度(用于透视视口换算)。 /// 来源:区域专有 _cameraDepth(>0 时) → LensConfig SO。 /// 未绑定 SO 时返回 0,限位同步工具会在 Inspector 中给出警告。 /// public float CameraDepth { get { if (_cameraDepth > 0f) return _cameraDepth; return _lensConfig != null ? _lensConfig.cameraDepth : 0f; } } // ── Lifecycle ──────────────────────────────────────────────────────── private void Awake() { // 确保专属 VCam 启动时 Priority=0,无论 Inspector 保存了什么值。 // CameraStateController.ActivateDedicated 会在进入区域时将其提升到 DedicatedPriority。 if (_dedicatedCamera != null) _dedicatedCamera.Priority = 0; } // ── 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); } } } }