165 lines
8.4 KiB
C#
165 lines
8.4 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|