Files
zeling_v2/Assets/_Game/Scripts/Camera/CameraFacingBiasExtension.cs
2026-05-19 11:50:21 +08:00

165 lines
8.4 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 UnityEngine;
using Unity.Cinemachine;
namespace BaseGames.Camera
{
/// <summary>
/// 方向感知水平偏置扩展Facing-aware Horizontal Bias
///
/// 持续估算跟随目标的水平移动速度,动态将相机 X 轴向玩家前进方向偏移,
/// 使玩家始终出现在画面偏后侧,前方预留更多可见视野。
///
/// <para>方向逻辑:</para>
/// <list type="bullet">
/// <item>向右移动 → 相机向右偏置 → 玩家出现在屏幕左侧 → 右方视野增加。</item>
/// <item>向左移动 → 相机向左偏置 → 玩家出现在屏幕右侧 → 左方视野增加。</item>
/// <item>静止或速度低于阈值 → 保持当前偏置方向,不回中(避免静止时镜头漂回)。</item>
/// </list>
///
/// <para>
/// 注意:本扩展通过直接修改 <c>state.RawPosition.x</c> 在 Body 阶段之后叠加偏置,
/// 不与 <see cref="CinemachinePositionComposer"/> 的 ScreenPosition.x 冲突;
/// 但偏置量会占用 Confiner 的裁剪空间,在较窄区域中建议通过
/// <see cref="CameraArea._facingBiasOverride"/> 设为较小值或 0 以防止越界。
/// </para>
///
/// <para>挂载顺序(扩展执行顺序由组件顺序决定):</para>
/// <list type="number">
/// <item><see cref="CameraAsymmetricDampingExtension"/> — Y 轴非对称阻尼</item>
/// <item><see cref="CameraFallBiasExtension"/> — 下坠 Y 偏置</item>
/// <item><b>本扩展CameraFacingBiasExtension</b> — X 轴方向偏置</item>
/// <item><c>CinemachineConfiner3D</c> — 最后裁剪至限位边界</item>
/// </list>
/// </summary>
[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 ──────────────────────────────────────────────────────────
/// <summary>
/// 供 <see cref="CameraStateController"/> 运行时按区域写入偏置量。
/// 0 = 禁用此区域的方向偏置。
/// </summary>
public float FacingBias { get => _facingBias; set => _facingBias = value; }
/// <summary>
/// 由 PlayerController 在精灵翻转时直接注入面朝方向,使偏置立即响应角色转向。
/// direction: +1 = 朝右,-1 = 朝左0 = 清除外部输入(回退到速度估算)。
/// 外部常量优先于速度估算,即使玩家缓慢行走或原地站立翻转,相机也能立即更新。
/// </summary>
public void SetExternalFacing(int direction)
{
_externalFacing = Mathf.Clamp(direction, -1, 1);
}
/// <summary>
/// 重置内部状态。在相机硬切instantCut时由 CameraStateController 调用,
/// 防止旧房间的方向偏置和速度历史带入新房间。
/// </summary>
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;
}
}
}