using UnityEngine; using Unity.Cinemachine; namespace BaseGames.Camera { /// /// 方向感知水平偏置扩展(Facing-aware Horizontal Bias)。 /// /// 持续估算跟随目标的水平移动速度,动态将相机 X 轴向玩家前进方向偏移, /// 使玩家始终出现在画面偏后侧,前方预留更多可见视野。 /// /// 方向逻辑: /// /// 向右移动 → 相机向右偏置 → 玩家出现在屏幕左侧 → 右方视野增加。 /// 向左移动 → 相机向左偏置 → 玩家出现在屏幕右侧 → 左方视野增加。 /// 静止或速度低于阈值 → 保持当前偏置方向,不回中(避免静止时镜头漂回)。 /// /// /// /// 注意:本扩展通过直接修改 state.RawPosition.x 在 Body 阶段之后叠加偏置, /// 不与 的 ScreenPosition.x 冲突; /// 但偏置量会占用 Confiner 的裁剪空间,在较窄区域中建议通过 /// 设为较小值或 0 以防止越界。 /// /// /// 挂载顺序(扩展执行顺序由组件顺序决定): /// /// — Y 轴非对称阻尼 /// — 下坠 Y 偏置 /// 本扩展(CameraFacingBiasExtension) — X 轴方向偏置 /// CinemachineConfiner3D — 最后裁剪至限位边界 /// /// [AddComponentMenu("Cinemachine/Extensions/Camera Facing Bias")] [DisallowMultipleComponent] public class CameraFacingBiasExtension : CinemachineExtension { [Tooltip("相机沿面朝方向的偏移量(世界单位)。\n" + "玩家向右时相机向右偏此值,使玩家出现在画面左侧,前方(右侧)视野增加;反之亦然。\n" + "0 = 禁用。推荐 1.5~2.5,视房间宽度和视口大小而定。")] [Range(0f, 6f)] [SerializeField] private float _facingBias = 2f; [Tooltip("偏置方向切换时的过渡时长(秒)。越大越平滑但延迟越长。\n" + "推荐 0.3~0.5。")] [Min(0f)] [SerializeField] private float _transitionTime = 0.4f; [Tooltip("触发方向切换所需的最小水平速度(世界单位/秒)。\n" + "低于此值时不更新目标偏置,防止静止时微小输入造成镜头反复横跳。\n" + "推荐 1.0~2.0。")] [Min(0f)] [SerializeField] private float _directionThreshold = 1.5f; [Tooltip("水平速度估算的平滑强度(越大响应越快)。推荐 6~10。")] [Min(0.1f)] [SerializeField] private float _velocitySmoothing = 8f; // ── 内部状态 ────────────────────────────────────────────────────────── private float _currentBiasX; // 当前实际偏置(世界单位,已指数平滑) private float _targetBiasX; // 当前目标偏置 private float _estimatedVX; // 平滑后的水平速度估算(世界单位/秒) private float _lastFollowX; private bool _initialized; private int _externalFacing = 0; // 0 = 未设置(回退到速度估算);+1 = 朝右;-1 = 朝左 // ── 公开 API ────────────────────────────────────────────────────────── /// /// 供 运行时按区域写入偏置量。 /// 0 = 禁用此区域的方向偏置。 /// public float FacingBias { get => _facingBias; set => _facingBias = value; } /// /// 由 PlayerController 在精灵翻转时直接注入面朝方向,使偏置立即响应角色转向。 /// direction: +1 = 朝右,-1 = 朝左,0 = 清除外部输入(回退到速度估算)。 /// 外部常量优先于速度估算,即使玩家缓慢行走或原地站立翻转,相机也能立即更新。 /// public void SetExternalFacing(int direction) { _externalFacing = Mathf.Clamp(direction, -1, 1); } /// /// 重置内部状态。在相机硬切(instantCut)时由 CameraStateController 调用, /// 防止旧房间的方向偏置和速度历史带入新房间。 /// public void ResetState() { _initialized = false; _estimatedVX = 0f; // 硬切时直接吸附到当前目标值,不保留旧房间的过渡惯性 _currentBiasX = _targetBiasX; } // ── Cinemachine Extension ───────────────────────────────────────────── protected override void PostPipelineStageCallback( CinemachineVirtualCameraBase vcam, CinemachineCore.Stage stage, ref CameraState state, float deltaTime) { // 在 Body 阶段之后(Composer 已计算出位置)叠加水平偏置 if (stage != CinemachineCore.Stage.Body) return; // 编辑器预览 / 初始帧:重置平滑器 if (deltaTime <= 0f) { _initialized = false; return; } // 偏置量为 0 时跳过,不修改位置 if (_facingBias <= 0f) return; Transform follow = vcam.Follow; if (follow == null) return; // ── 初始化:第一帧直接吸附,不做过渡动画 ──────────────────────── if (!_initialized) { _lastFollowX = follow.position.x; _initialized = true; _currentBiasX = _targetBiasX; return; } // ── 估算水平速度(帧率无关指数平滑)──────────────────────────── float rawVX = (follow.position.x - _lastFollowX) / deltaTime; _lastFollowX = follow.position.x; _estimatedVX = Mathf.Lerp(_estimatedVX, rawVX, 1f - Mathf.Exp(-deltaTime * _velocitySmoothing)); // ── 方向判定(带死区,静止时保持当前目标方向)────────────────── // 优先使用外部注入朝向(PlayerController.OnFlip 传入); // 未设置时回退到速度估算(却保留静止不回中行为)。 if (_externalFacing != 0) { _targetBiasX = _externalFacing * _facingBias; } else { if (_estimatedVX > _directionThreshold) _targetBiasX = +_facingBias; // 向右 → 相机右偏 → 玩家在左,前方(右侧)视野增 else if (_estimatedVX < -_directionThreshold) _targetBiasX = -_facingBias; // 向左 → 相机左偏 → 玩家在右,前方(左侧)视野增 // else: 速度不足,保持 _targetBiasX 不变(不回中) } // ── 指数平滑过渡 ───────────────────────────────────────────────── float t = _transitionTime > 0f ? 1f - Mathf.Exp(-deltaTime / _transitionTime) : 1f; _currentBiasX = Mathf.Lerp(_currentBiasX, _targetBiasX, t); // ── 叠加到 Composer 已计算的位置上 ────────────────────────────── // 注意:此偏置在 CinemachineConfiner3D 之前施加, // Confiner 会在之后将结果裁剪回限位边界内。 var pos = state.RawPosition; pos.x += _currentBiasX; state.RawPosition = pos; } } }