角色能力,存档
This commit is contained in:
@@ -77,7 +77,7 @@ namespace BaseGames.Camera
|
||||
|
||||
float rawSpeedX = Mathf.Abs(follow.position.x - _lastFollowX) / deltaTime;
|
||||
_lastFollowX = follow.position.x;
|
||||
_estimatedSpeedX = Mathf.Lerp(_estimatedSpeedX, rawSpeedX, deltaTime * _speedSmoothing);
|
||||
_estimatedSpeedX = Mathf.Lerp(_estimatedSpeedX, rawSpeedX, 1f - Mathf.Exp(-deltaTime * _speedSmoothing));
|
||||
}
|
||||
|
||||
// ── 速度映射 → Lookahead 时长 ─────────────────────────────────────
|
||||
|
||||
@@ -5,20 +5,19 @@ namespace BaseGames.Camera
|
||||
{
|
||||
/// <summary>
|
||||
/// 相机区域数据组件。一个房间场景内可放置任意数量的 CameraArea,
|
||||
/// 每个区域独立定义限位范围、可视边界与混合配置。
|
||||
/// 每个区域独立定义限位范围、可视边界、跟随参数与混合配置。
|
||||
///
|
||||
/// 运行时由 <see cref="CameraStateController"/> 管理:
|
||||
/// - <c>_dedicatedCamera</c> 为空 → 使用 Persistent 场景中的两台全局 VCam 交替承接,
|
||||
/// 减少场景内 VCam 数量,相机参数统一由全局 VCam 配置。
|
||||
/// - <c>_dedicatedCamera</c> 不为空 → 激活该专有 VCam(优先级高于全局 VCam),
|
||||
/// 适用于需要独特相机参数(FOV / Offset / 阻尼)的特殊区域。
|
||||
/// 每个区域均拥有专属的 <c>_dedicatedCamera</c>,进入该区域时激活其专属 VCam。
|
||||
/// <c>OverrideFollowBehaviour</c> 为真时,<see cref="CameraStateController"/> 会将本组件中的
|
||||
/// 跟随参数应用到专属 VCam;否则 VCam 保持 Inspector 中的默认值。
|
||||
/// </summary>
|
||||
public class CameraArea : MonoBehaviour
|
||||
{
|
||||
[Header("限位区域")]
|
||||
[Tooltip("定义相机移动边界的 PolygonCollider2D(通常挂载在子节点 AreaBoundary 上)。\n" +
|
||||
"会被赋给全局 VCam 的 CinemachineConfiner2D.BoundingShape2D。")]
|
||||
[SerializeField] private PolygonCollider2D _confinerCollider;
|
||||
[Tooltip("定义相机移动边界的 BoxCollider(通常挂载在子节点 AreaBoundary 上)。\n" +
|
||||
"会被绑定到专属 VCam 的 CinemachineConfiner3D.BoundingVolume。")]
|
||||
[SerializeField] private BoxCollider _confinerCollider;
|
||||
|
||||
[Header("可视区域(透视相机)")]
|
||||
[Tooltip("摄像机应显示的最大可视矩形(本地坐标,相对于此 GameObject 的 Transform 位置)。\n" +
|
||||
@@ -30,8 +29,7 @@ namespace BaseGames.Camera
|
||||
[SerializeField] private float _cameraDepth = 0f;
|
||||
|
||||
[Header("镜头配置")]
|
||||
[Tooltip("全局相机镜头参数 SO。与 CameraStateController 引用同一资产,\n" +
|
||||
"保证 FOV 等参数在 Room 场景中也能正确读取。\n" +
|
||||
[Tooltip("相机镜头参数 SO,提供 FOV、相机深度等参数。\n" +
|
||||
"SO 中的 fieldOfView 发生变化时,编辑器会自动重新同步限位多边形。")]
|
||||
[SerializeField] private CameraLensConfigSO _lensConfig;
|
||||
|
||||
@@ -40,9 +38,9 @@ namespace BaseGames.Camera
|
||||
[SerializeField] private float _lastSyncFOV = 0f;
|
||||
// ── 跟随行为 ──────────────────────────────────────────────────────────
|
||||
|
||||
[Header("跟随行为(覆盖全局 VCam 参数)")]
|
||||
[Tooltip("启用后,进入此区域时将把以下参数写入全局 VCam;\n" +
|
||||
"关闭则 VCam 保持上一区域或 Inspector 中的默认值。")]
|
||||
[Header("跟随行为(专属 VCam 参数)")]
|
||||
[Tooltip("启用后,激活此区域时将以下参数应用到专属 VCam;\n" +
|
||||
"关闭则 VCam 保持 Inspector 中的默认参数不作任何覆写。")]
|
||||
[SerializeField] private bool _overrideFollowBehaviour = true;
|
||||
|
||||
[Tooltip("玩家跟踪点在屏幕上的位置(0 = 中心,±0.5 = 边缘)。\n" +
|
||||
@@ -78,6 +76,20 @@ namespace BaseGames.Camera
|
||||
+ "防止相机因偏置超出 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("轴向约束")]
|
||||
@@ -87,8 +99,9 @@ namespace BaseGames.Camera
|
||||
[Tooltip("锁定相机 Y 轴(水平走廊:相机仅左右移动,Y 固定在限位区域中心)。")]
|
||||
[SerializeField] private bool _lockVertical = false;
|
||||
|
||||
[Header("镜头尺寸(正交相机)")]
|
||||
[Tooltip("进入此区域时的目标正交尺寸(0 = 不覆盖当前尺寸)。\n" +
|
||||
[Header("镜头尺寸(视野缩放)")]
|
||||
[Tooltip("进入此区域时的目标可视半高(世界单位,0 = 不覆盖)。\n" +
|
||||
"等价于正交相机的 OrthographicSize,透视相机下自动换算为 FOV。\n" +
|
||||
"适用于 Boss 战拉远或精密解谜区域拉近。")]
|
||||
[SerializeField] private float _lensSize = 0f;
|
||||
|
||||
@@ -99,18 +112,31 @@ namespace BaseGames.Camera
|
||||
[Header("混合配置")]
|
||||
[SerializeField] private CameraBlendProfileSO _blendProfile;
|
||||
|
||||
[Header("专有虚拟相机(可选)")]
|
||||
[Tooltip("为空时由全局双 VCam 交替过渡(推荐,节省 VCam 数量)。\n" +
|
||||
"不为空时激活此专有 CinemachineCamera,优先级高于全局 VCam。\n" +
|
||||
"适用于需要独特 FOV / Noise / LookAt 等参数的特殊区域。")]
|
||||
[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 激活时使用的优先级,须高于全局 VCam 的 _globalActivePriority(默认 10)。")]
|
||||
[Tooltip("专属 VCam 激活时使用的优先级,默认 20。")]
|
||||
[SerializeField] private int _dedicatedPriority = 20;
|
||||
|
||||
// ── 公开属性 ──────────────────────────────────────────────────────────
|
||||
|
||||
public PolygonCollider2D ConfinerCollider => _confinerCollider;
|
||||
public BoxCollider ConfinerCollider => _confinerCollider;
|
||||
public CameraLensConfigSO LensConfig => _lensConfig;
|
||||
public float LastSyncFOV => _lastSyncFOV;
|
||||
public CameraBlendProfileSO BlendProfile => _blendProfile;
|
||||
@@ -129,12 +155,17 @@ namespace BaseGames.Camera
|
||||
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>
|
||||
/// 摄像机到场景平面的有效深度(用于透视视口换算)。
|
||||
@@ -150,6 +181,16 @@ namespace BaseGames.Camera
|
||||
}
|
||||
}
|
||||
|
||||
// ── Lifecycle ────────────────────────────────────────────────────────
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// 确保专属 VCam 启动时 Priority=0,无论 Inspector 保存了什么值。
|
||||
// CameraStateController.ActivateDedicated 会在进入区域时将其提升到 DedicatedPriority。
|
||||
if (_dedicatedCamera != null)
|
||||
_dedicatedCamera.Priority = 0;
|
||||
}
|
||||
|
||||
// ── Gizmo ────────────────────────────────────────────────────────────
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
|
||||
@@ -34,10 +34,18 @@ namespace BaseGames.Camera
|
||||
{
|
||||
if (stage != CinemachineCore.Stage.Body) return;
|
||||
|
||||
var pos = state.RawPosition;
|
||||
if (LockX) pos.x = LockedX;
|
||||
if (LockY) pos.y = LockedY;
|
||||
state.RawPosition = pos;
|
||||
// 通过覆写 PositionCorrection 而非 RawPosition 来锁定轴向。
|
||||
// 最终相机位置 = RawPosition + PositionCorrection。
|
||||
// 只修改 PositionCorrection 可确保:
|
||||
// - 锁定轴:final = RawPos + (LockedVal - RawPos) = LockedVal,无论 Confiner 之前写入了什么修正量
|
||||
// - 非锁定轴:保留 CinemachineConfiner3D 已计算好的 PositionCorrection,限位正常生效
|
||||
// 若改为修改 RawPosition,在 Confiner 之后运行时会导致
|
||||
// final = LockedVal + ConfinementCorrection ≠ LockedVal,使 Confiner 修正量错误叠加。
|
||||
var rawPos = state.RawPosition;
|
||||
var correction = state.PositionCorrection;
|
||||
if (LockX) correction.x = LockedX - rawPos.x;
|
||||
if (LockY) correction.y = LockedY - rawPos.y;
|
||||
state.PositionCorrection = correction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
164
Assets/_Game/Scripts/Camera/CameraFacingBiasExtension.cs
Normal file
164
Assets/_Game/Scripts/Camera/CameraFacingBiasExtension.cs
Normal file
@@ -0,0 +1,164 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db8aa13bc9ef2124d8512b64b173d768
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -15,7 +15,7 @@ namespace BaseGames.Camera
|
||||
/// <list type="number">
|
||||
/// <item><see cref="CameraAsymmetricDampingExtension"/> — 先对 Y 轴做非对称阻尼平滑;</item>
|
||||
/// <item><b>本扩展(CameraFallBiasExtension)</b> — 将偏置叠加到平滑后的 Y 上;</item>
|
||||
/// <item><c>CinemachineConfiner2D</c> — 最后将偏置后的位置裁剪回限位边界内。</item>
|
||||
/// <item><c>CinemachineConfiner3D</c> — 最后将偏置后的位置裁剪回限位边界内。</item>
|
||||
/// </list>
|
||||
/// <para>如果顺序错误(本扩展在 Confiner 之后),偏置会导致相机超出限位边界且不被修正。</para>
|
||||
/// </summary>
|
||||
@@ -98,10 +98,10 @@ namespace BaseGames.Camera
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 估算玩家 Y 轴速度(负值 = 下落)────────────────────────────
|
||||
// ── 估算玩家 Y 轴速度(帧率无关指数平滑)─────────────────────
|
||||
float rawVY = (follow.position.y - _lastFollowY) / deltaTime;
|
||||
_lastFollowY = follow.position.y;
|
||||
_smoothedVY = Mathf.Lerp(_smoothedVY, rawVY, deltaTime * 10f);
|
||||
_smoothedVY = Mathf.Lerp(_smoothedVY, rawVY, 1f - Mathf.Exp(-deltaTime * 10f));
|
||||
|
||||
bool isFalling = _smoothedVY < -_fallSpeedThreshold;
|
||||
|
||||
@@ -115,7 +115,7 @@ namespace BaseGames.Camera
|
||||
// 超过 activationDelay 后线性增加偏置;0.4s 达到最大
|
||||
float effectiveMax = _configuredMaxShift >= 0f ? _configuredMaxShift : _maxShift;
|
||||
float ratio = Mathf.Clamp01((_fallTimer - _activationDelay) / 0.4f);
|
||||
float targetShift = -effectiveMax * ratio; // 负就:相机向下
|
||||
float targetShift = -effectiveMax * ratio; // 负值:相机向下
|
||||
|
||||
// 使用指数衰减公式(帧率无关)替代 Lerp*deltaTime
|
||||
float dampingTime = targetShift < _currentShift
|
||||
|
||||
@@ -3,12 +3,10 @@ using UnityEngine;
|
||||
namespace BaseGames.Camera
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局相机镜头配置。
|
||||
/// 相机镜头配置。
|
||||
///
|
||||
/// 作为 <see cref="CameraStateController"/> 和各 <see cref="CameraArea"/> 之间的
|
||||
/// 单一参数来源:
|
||||
/// - Persistent 场景的 <see cref="CameraStateController"/> 在 Awake 时将
|
||||
/// <see cref="fieldOfView"/> 写入两台全局 VCam 的 Lens。
|
||||
/// - Room 场景的 <see cref="CameraArea"/> 引用同一 SO,编辑器工具在计算限位多边形
|
||||
/// 时直接读取,无需依赖 Persistent 场景是否已加载。
|
||||
///
|
||||
@@ -21,15 +19,14 @@ namespace BaseGames.Camera
|
||||
[CreateAssetMenu(menuName = "BaseGames/Camera/Lens Config", fileName = "CameraLensConfig")]
|
||||
public class CameraLensConfigSO : ScriptableObject
|
||||
{
|
||||
[Tooltip("全局虚拟相机的垂直 FOV(度)。\n" +
|
||||
"修改此值后,编辑器会自动对所有已打开场景中的 CameraArea 重新同步限位多边形。\n" +
|
||||
"运行时由 CameraStateController 在 Awake 时应用到全局 VCam。")]
|
||||
[Tooltip("虚拟相机的垂直 FOV(度)。\n" +
|
||||
"修改此値后,编辑器会自动对所有已打开场景中的 CameraArea 重新同步限位多边形。")]
|
||||
[Range(1f, 179f)]
|
||||
public float fieldOfView = 60f;
|
||||
|
||||
[Tooltip("摄像机到场景平面(Z = 0)的垂直距离(世界单位)。\n" +
|
||||
"与 fieldOfView 共同决定透视相机的视口尺寸,\n" +
|
||||
"用于将可视区域(VisibleBounds)换算为 CinemachineConfiner2D 限位多边形。\n" +
|
||||
"用于将可视区域(VisibleBounds)换算为 CinemachineConfiner3D 限位体积。\n" +
|
||||
"推荐与 Persistent 场景中相机 Transform 的 |Z| 保持一致(通常为 10)。\n" +
|
||||
"CameraArea._cameraDepth > 0 时以区域专有值优先覆盖此全局值。")]
|
||||
[Min(0.1f)]
|
||||
|
||||
@@ -150,7 +150,7 @@ namespace BaseGames.Camera
|
||||
if (dt > 0f)
|
||||
{
|
||||
float rawSpeed = (_baseTarget.position - _lastBasePosition).magnitude / dt;
|
||||
_estimatedSpeed = Mathf.Lerp(_estimatedSpeed, rawSpeed, dt * 8f);
|
||||
_estimatedSpeed = Mathf.Lerp(_estimatedSpeed, rawSpeed, 1f - Mathf.Exp(-dt * 8f));
|
||||
}
|
||||
_lastBasePosition = _baseTarget.position;
|
||||
|
||||
@@ -170,7 +170,7 @@ namespace BaseGames.Camera
|
||||
}
|
||||
|
||||
float speedV = Mathf.Abs(_targetOffsetY) < 0.01f ? _resetSpeedV : _lookSpeedV;
|
||||
_currentOffsetY = Mathf.Lerp(_currentOffsetY, _targetOffsetY, dt * speedV);
|
||||
_currentOffsetY = Mathf.Lerp(_currentOffsetY, _targetOffsetY, 1f - Mathf.Exp(-dt * speedV));
|
||||
|
||||
// ── 水平窥视 ──────────────────────────────────────────────────────
|
||||
if (withinGate && Mathf.Abs(_inputX) > 0.5f)
|
||||
@@ -186,7 +186,7 @@ namespace BaseGames.Camera
|
||||
}
|
||||
|
||||
float speedH = Mathf.Abs(_targetOffsetX) < 0.01f ? _resetSpeedH : _lookSpeedH;
|
||||
_currentOffsetX = Mathf.Lerp(_currentOffsetX, _targetOffsetX, dt * speedH);
|
||||
_currentOffsetX = Mathf.Lerp(_currentOffsetX, _targetOffsetX, 1f - Mathf.Exp(-dt * speedH));
|
||||
|
||||
// ── 虚拟目标 = 玩家位置 + 双轴偏移 ────────────────────────────────
|
||||
_virtualTargetTransform.position = _baseTarget.position
|
||||
|
||||
114
Assets/_Game/Scripts/Camera/CameraPixelSnapper.cs
Normal file
114
Assets/_Game/Scripts/Camera/CameraPixelSnapper.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using UnityEngine;
|
||||
using Unity.Cinemachine;
|
||||
|
||||
namespace BaseGames.Camera
|
||||
{
|
||||
/// <summary>
|
||||
/// 相机像素对齐(Pixel-perfect Snapping)。
|
||||
///
|
||||
/// 每帧在 CinemachineBrain LateUpdate 完成后,将 Unity 主相机的世界坐标
|
||||
/// 四舍五入到最近的像素网格,消除像素艺术精灵的亚像素抖动(sub-pixel jitter)。
|
||||
///
|
||||
/// <para>
|
||||
/// 挂载位置:Persistent 场景中与 <see cref="CinemachineBrain"/> 同一 GameObject([Camera] 节点)。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 执行顺序:+1000,确保在 CinemachineBrain(默认 -1000)的 LateUpdate 之后运行。
|
||||
/// 直接修改 Unity Camera 的 <c>transform.position</c>,不影响 Cinemachine 内部状态。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 混合进行中时可选择暂停对齐(<see cref="_disableDuringBlend"/> = true),
|
||||
/// 避免两台 VCam 插值结果在整数像素间跳变,产生阶梯感。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(1000)]
|
||||
[AddComponentMenu("BaseGames/Camera/Camera Pixel Snapper")]
|
||||
[RequireComponent(typeof(UnityEngine.Camera))]
|
||||
public class CameraPixelSnapper : MonoBehaviour
|
||||
{
|
||||
[Tooltip("每世界单位的像素数(PPU)。须与项目精灵的 Pixels Per Unit 设置一致。\n" +
|
||||
"0 = 禁用像素对齐。常用值:16(粗像素)、32(标准)、100(高分辨率)。")]
|
||||
[Min(0f)]
|
||||
[SerializeField] private float _pixelsPerUnit = 16f;
|
||||
|
||||
[Tooltip("相机混合动画(Blend)进行中时暂停像素对齐,待混合结束后恢复。\n" +
|
||||
"开启时混合动画更平滑(无阶梯感);关闭时混合期间也精确对齐但可能有轻微跳帧。\n" +
|
||||
"推荐保持开启。")]
|
||||
[SerializeField] private bool _disableDuringBlend = true;
|
||||
|
||||
[SerializeField] private CinemachineBrain _brain;
|
||||
|
||||
private UnityEngine.Camera _camera;
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
_brain = GetComponent<CinemachineBrain>();
|
||||
_camera = GetComponent<UnityEngine.Camera>();
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_camera = GetComponent<UnityEngine.Camera>();
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (_pixelsPerUnit <= 0f || _camera == null) return;
|
||||
|
||||
// 混合进行中时跳过,避免插值位置在像素网格间产生阶梯感
|
||||
if (_disableDuringBlend && _brain != null && _brain.IsBlending) return;
|
||||
|
||||
float ppu = _pixelsPerUnit;
|
||||
Vector3 pos = _camera.transform.position;
|
||||
pos.x = Mathf.Round(pos.x * ppu) / ppu;
|
||||
pos.y = Mathf.Round(pos.y * ppu) / ppu;
|
||||
// Z 轴保持不变(透视深度不需要对齐)
|
||||
|
||||
// 像素取整可能将相机推出 Confiner 边界(最多 0.5/PPU 的微小超出)。
|
||||
// 对取整后的位置施加限位矩形钳制,确保不超出当前激活区域的 Confiner 边界。
|
||||
if (TryGetActiveConfinerBounds(out Bounds confinerBounds))
|
||||
{
|
||||
pos.x = Mathf.Clamp(pos.x, confinerBounds.min.x, confinerBounds.max.x);
|
||||
pos.y = Mathf.Clamp(pos.y, confinerBounds.min.y, confinerBounds.max.y);
|
||||
}
|
||||
|
||||
_camera.transform.position = pos;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前活跃 VCam 的 CinemachineConfiner3D 边界盒(世界空间 AABB)。
|
||||
/// 用于在像素取整后将相机钳制回限位区域内。
|
||||
/// </summary>
|
||||
private bool TryGetActiveConfinerBounds(out Bounds bounds)
|
||||
{
|
||||
bounds = default;
|
||||
if (_brain == null) return false;
|
||||
var vcam = _brain.ActiveVirtualCamera as CinemachineCamera;
|
||||
if (vcam == null) return false;
|
||||
var confiner = vcam.GetComponent<CinemachineConfiner3D>();
|
||||
if (confiner == null || !confiner.IsValid) return false;
|
||||
bounds = confiner.BoundingVolume.bounds;
|
||||
return true;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>编辑器模式下实时预览对齐效果。</summary>
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (_pixelsPerUnit <= 0f || _camera == null) return;
|
||||
float ppu = _pixelsPerUnit;
|
||||
float cellW = 1f / ppu;
|
||||
// 在 Scene 视图中围绕相机位置绘制 5×5 像素网格示意(仅辅助调试用)
|
||||
Gizmos.color = new Color(0.6f, 1f, 0.6f, 0.4f);
|
||||
Vector3 origin = _camera.transform.position;
|
||||
origin.x = Mathf.Floor(origin.x / cellW) * cellW;
|
||||
origin.y = Mathf.Floor(origin.y / cellW) * cellW;
|
||||
for (int ix = -2; ix <= 3; ix++)
|
||||
for (int iy = -2; iy <= 3; iy++)
|
||||
Gizmos.DrawWireCube(
|
||||
new Vector3(origin.x + ix * cellW, origin.y + iy * cellW, 0f),
|
||||
new Vector3(cellW, cellW, 0.01f));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Camera/CameraPixelSnapper.cs.meta
Normal file
11
Assets/_Game/Scripts/Camera/CameraPixelSnapper.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c2869ceeab4c184180a029bfc710cd0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
@@ -10,14 +11,9 @@ namespace BaseGames.Camera
|
||||
/// <summary>
|
||||
/// 相机状态单例控制器。须放置在 Persistent 场景中。
|
||||
///
|
||||
/// 支持两种相机切换模式:
|
||||
/// 1. 全局双 VCam 模式(推荐):<see cref="SwitchArea"/>
|
||||
/// 两台全局 CinemachineCamera(<c>_vcamA</c> / <c>_vcamB</c>)交替承接各区域,
|
||||
/// 通过优先级 ping-pong 触发 Cinemachine 混合过渡。场景内无需每个区域都放置 VCam。
|
||||
///
|
||||
/// 2. 专有 VCam 模式:<see cref="SwitchArea"/>(区域含 dedicatedCamera 时自动使用)
|
||||
/// 激活该区域专属的 CinemachineCamera(优先级 > 全局 VCam),
|
||||
/// 适用于需要独特相机参数的特殊区域。
|
||||
/// 每个 <see cref="CameraArea"/> 均拥有自己专属的 <c>DedicatedCamera</c>,
|
||||
/// 进入区域时调用 <see cref="ActivateDedicated"/> 激活对应 VCam,
|
||||
/// Cinemachine Brain 自动处理混合过渡。
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-100)]
|
||||
public class CameraStateController : MonoBehaviour, ICameraService
|
||||
@@ -27,43 +23,28 @@ namespace BaseGames.Camera
|
||||
[SerializeField] private CinemachineImpulseSource _impulseSource;
|
||||
[SerializeField] private CameraLookSystem _lookSystem;
|
||||
|
||||
[Header("全局双 VCam(Persistent 场景中放置两台通用虚拟相机)")]
|
||||
[Tooltip("两台 VCam 交替承接各相机区域,通过优先级 ping-pong 触发混合过渡。\n" +
|
||||
"须各自挂载 CinemachineCamera + CinemachineConfiner2D;\n" +
|
||||
"Follow 指向 Player/CameraFollowTarget(或运行时调用 SetFollowTarget 赋值)。")]
|
||||
[SerializeField] private CinemachineCamera _vcamA;
|
||||
[SerializeField] private CinemachineCamera _vcamB;
|
||||
|
||||
[Tooltip("全局 VCam 激活时的优先级。专有 VCam 的 _dedicatedPriority 须高于此值。")]
|
||||
[SerializeField] private int _globalActivePriority = 10;
|
||||
|
||||
[Tooltip("待机 VCam 的优先级。\n" +
|
||||
"Cinemachine 3.x 中 Priority = 0 的 VCam 不会被 Brain 选中,导致主相机停止跟随。\n" +
|
||||
"必须 > 0 且 < _globalActivePriority,确保 Brain 始终有可用 VCam,\n" +
|
||||
"同时切换时两台 VCam 均在 Brain 视野内以完成正确的混合过渡。")]
|
||||
[SerializeField] private int _standbyPriority = 1;
|
||||
|
||||
[Header("默认混合配置")]
|
||||
[SerializeField] private CameraBlendProfileSO _defaultBlendProfile;
|
||||
|
||||
[Header("镜头配置")]
|
||||
[Tooltip("全局镜头参数 SO。Awake 时将 fieldOfView 应用到 _vcamA / _vcamB。\n" +
|
||||
"与各 CameraArea 引用同一资产,确保 FOV 参数一致。")]
|
||||
[Tooltip("相机镜头参数 SO,提供 FOV 与相机深度。\n" +
|
||||
"与各 CameraArea 引用同一资产,确保 SetLensSize 换算结果与 VCam 配置一致。")]
|
||||
[SerializeField] private CameraLensConfigSO _lensConfig;
|
||||
|
||||
[Header("玩家跟随")]
|
||||
[Tooltip("PlayerController 生成时广播的事件频道(EVT_PlayerSpawned)。\n" +
|
||||
"收到后自动查找 CameraFollowTarget 子节点并赋值给两台全局 VCam 的 Follow。")]
|
||||
"收到后自动查找 CameraFollowTarget 子节点并作为 VCam Follow 赋值。")]
|
||||
[SerializeField] private TransformEventChannelSO _onPlayerSpawned;
|
||||
|
||||
// ── 状态 ──────────────────────────────────────────────────────────────
|
||||
private int _activeSlot = -1; // -1 = 未初始化;0 = A;1 = B
|
||||
private CameraArea _roomBaselineArea; // SwitchArea(priority=0) 写入的房间基线,不被触发事件删除
|
||||
private readonly List<(CameraArea area, int priority)> _activeZones = new(); // 玩家当前所在的触发区域集合(priority>0)
|
||||
private CameraArea _currentArea;
|
||||
private CinemachineCamera _activeDedicatedCam;
|
||||
private CinemachineConfiner2D _confinerA;
|
||||
private CinemachineConfiner2D _confinerB;
|
||||
private CinemachineCamera _cutsceneCamera; // 过场模式专用高优先级 VCam
|
||||
private const int CutscenePriority = 100; // 高于专有区域 VCam(默认 20)
|
||||
private int _lastExternalFacing = 0; // 最近一次 SetPlayerFacing 的值,用于新激活 VCam 的初始化
|
||||
private Transform _currentFollowTarget; // 最后一次 SetFollowTarget 设置的目标,激活 VCam 时自动同步
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
@@ -74,17 +55,14 @@ namespace BaseGames.Camera
|
||||
if (ServiceLocator.GetOrDefault<ICameraService>() != null) { Destroy(gameObject); return; }
|
||||
ServiceLocator.Register<ICameraService>(this);
|
||||
|
||||
// 缓存 Confiner 引用
|
||||
if (_vcamA != null) _confinerA = _vcamA.GetComponent<CinemachineConfiner2D>();
|
||||
if (_vcamB != null) _confinerB = _vcamB.GetComponent<CinemachineConfiner2D>();
|
||||
|
||||
// 初始两台 VCam 均处于待机优先级(> 0)
|
||||
// Cinemachine 3.x 中 Priority = 0 的 VCam 不被 Brain 选中,主相机会停止运动
|
||||
if (_vcamA != null) _vcamA.Priority = _standbyPriority;
|
||||
if (_vcamB != null) _vcamB.Priority = _standbyPriority;
|
||||
|
||||
// 将 SO 中的 FOV 应用到两台全局 VCam
|
||||
ApplyLensConfig();
|
||||
// 重置运行时状态,防止禁用 Domain Reload 时上一次 Play Mode 的数据残留。
|
||||
// 非序列化字段在 Domain Reload 禁用时不会自动清零。
|
||||
_roomBaselineArea = null;
|
||||
_activeZones.Clear();
|
||||
_currentArea = null;
|
||||
_activeDedicatedCam = null;
|
||||
_lastExternalFacing = 0;
|
||||
_currentFollowTarget = null;
|
||||
|
||||
// 订阅 PlayerSpawned 事件,运行时自动为 VCam 赋值 Follow
|
||||
_onPlayerSpawned?.Subscribe(OnPlayerSpawned).AddTo(_subs);
|
||||
@@ -103,36 +81,19 @@ namespace BaseGames.Camera
|
||||
SetFollowTarget(follow);
|
||||
}
|
||||
|
||||
private void ApplyLensConfig()
|
||||
private void Start()
|
||||
{
|
||||
if (_lensConfig == null) return;
|
||||
float fov = _lensConfig.fieldOfView;
|
||||
float depth = _lensConfig.cameraDepth;
|
||||
ApplyLensToVcam(_vcamA, fov, depth);
|
||||
ApplyLensToVcam(_vcamB, fov, depth);
|
||||
// 场景启动时扫描全部 VCam,提前暴露组件顺序错误,
|
||||
// 无需等待各区域被激活才触发检测。
|
||||
var allVCams = FindObjectsByType<CinemachineCamera>(FindObjectsSortMode.None);
|
||||
foreach (var vcam in allVCams)
|
||||
{
|
||||
var confiner = vcam.GetComponent<CinemachineConfiner3D>();
|
||||
if (confiner != null)
|
||||
ValidateVCamExtensionOrder(vcam, confiner);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyLensToVcam(CinemachineCamera vcam, float fov, float depth)
|
||||
{
|
||||
if (vcam == null) return;
|
||||
var lens = vcam.Lens;
|
||||
lens.FieldOfView = fov;
|
||||
vcam.Lens = lens;
|
||||
// CinemachinePositionComposer.CameraDistance 是运行时真正控制 Z 距离的属性,
|
||||
// 必须同步,否则 Transform Z 被 Cinemachine Pipeline 覆盖
|
||||
var composer = vcam.GetComponent<CinemachinePositionComposer>();
|
||||
if (composer != null)
|
||||
composer.CameraDistance = depth;
|
||||
// 同步 Transform Z,保证编辑器预览与运行时一致
|
||||
var pos = vcam.transform.position;
|
||||
pos.z = -depth;
|
||||
vcam.transform.position = pos;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate() => ApplyLensConfig();
|
||||
#endif
|
||||
|
||||
// ── 公开 API ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
@@ -156,7 +117,7 @@ namespace BaseGames.Camera
|
||||
_activeZones.RemoveAll(e => e.area == area);
|
||||
_activeZones.Add((area, priority));
|
||||
|
||||
// 仅当此区域是当前最优且尚未激活时才切换,避免不必要的 ping-pong
|
||||
// 仅当此区域是当前最优且尚未激活时才切换
|
||||
CameraArea best = GetEffectiveArea();
|
||||
if (best == area && area != _currentArea)
|
||||
ActivateArea(area, instantCut);
|
||||
@@ -172,7 +133,8 @@ namespace BaseGames.Camera
|
||||
|
||||
bool wasActive = releasedArea == _currentArea;
|
||||
int removed = _activeZones.RemoveAll(e => e.area == releasedArea);
|
||||
if (removed == 0) return;
|
||||
// 若区域本就不在栈中,且又不是当前激活区,则无需任何操作
|
||||
if (removed == 0 && !wasActive) return;
|
||||
|
||||
if (!wasActive) return;
|
||||
|
||||
@@ -209,10 +171,10 @@ namespace BaseGames.Camera
|
||||
Time = 0f,
|
||||
};
|
||||
// 重置窥视偏移,避免旧房间的窥视状态残留
|
||||
_lookSystem?.ResetOffsets(snap: true); // 重置所有 VCam 扩展的内部状态,防止旧房间的速度/阻尼估算带入新房间
|
||||
ResetVCamExtensions(_vcamA);
|
||||
ResetVCamExtensions(_vcamB);
|
||||
if (area.HasDedicated) ResetVCamExtensions(area.DedicatedCamera); }
|
||||
_lookSystem?.ResetOffsets(snap: true);
|
||||
// 重置专属 VCam 扩展的内部状态,防止旧房间的速度/阻尼估算带入新房间
|
||||
if (area.HasDedicated) ResetVCamExtensions(area.DedicatedCamera);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyBlendProfile(area.BlendProfile ?? _defaultBlendProfile);
|
||||
@@ -224,12 +186,12 @@ namespace BaseGames.Camera
|
||||
if (area.HasDedicated)
|
||||
ActivateDedicated(area);
|
||||
else
|
||||
ActivateGlobalSlot(area);
|
||||
Debug.LogError($"[CameraStateController] {area.name} 缺少专属 VCam!请通过 Camera Area Setup 工具为此区域创建 DedicatedCamera。");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 运行时为全局双 VCam 设置跟随目标。
|
||||
/// 若存在 <see cref="CameraLookSystem"/>,VCam 跟随系统输出的虚拟目标(含窥视偏移)。
|
||||
/// 运行时设置跟随目标。
|
||||
/// 若存在 <see cref="CameraLookSystem"/>,VCam 跟随系统输出的虚拟目标(含窗斥偏移)。
|
||||
/// </summary>
|
||||
public void SetFollowTarget(Transform followTarget)
|
||||
{
|
||||
@@ -239,9 +201,8 @@ namespace BaseGames.Camera
|
||||
_lookSystem.SetBaseTarget(followTarget);
|
||||
actual = _lookSystem.VirtualTarget;
|
||||
}
|
||||
_currentFollowTarget = actual; // 缓存供后续激活 VCam 时同步
|
||||
if (_vcamA != null) _vcamA.Follow = actual;
|
||||
if (_vcamB != null) _vcamB.Follow = actual;
|
||||
_currentFollowTarget = actual;
|
||||
SyncFollowToVCam(_activeDedicatedCam); // 立即同步到当前活跃专有 VCam
|
||||
}
|
||||
|
||||
/// <summary>触发屏幕抖动。</summary>
|
||||
@@ -255,30 +216,72 @@ namespace BaseGames.Camera
|
||||
=> TriggerImpulse(Vector3.down * strength);
|
||||
|
||||
/// <summary>
|
||||
/// 平滑过渡正交相机尺寸。<paramref name="duration"/> = 0 时瞬间切换。
|
||||
/// 平滑过渡视野尺寸(可视半高,世界单位)。<paramref name="duration"/> = 0 时瞬间切换。
|
||||
/// 透视相机下自动换算为 FOV;语义等价于正交相机 OrthographicSize。
|
||||
/// 区域进入时由 <see cref="CameraArea"/> 自动调用;游戏代码也可直接调用。
|
||||
/// </summary>
|
||||
public void SetLensSize(float orthographicSize, float duration = 0f)
|
||||
public void SetLensSize(float visibleHalfHeight, float duration = 0f)
|
||||
{
|
||||
if (_lensCoroutine != null) StopCoroutine(_lensCoroutine);
|
||||
if (duration <= 0f) { ApplyLensSizeToAll(orthographicSize); return; }
|
||||
_lensCoroutine = StartCoroutine(LensSizeCo(orthographicSize, duration));
|
||||
if (duration <= 0f) { ApplyLensSizeToAll(visibleHalfHeight); return; }
|
||||
_lensCoroutine = StartCoroutine(LensSizeCo(visibleHalfHeight, duration));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 进入过场模式,将指定 VCam 提升至最高优先级,Brain 自动混合到它。
|
||||
/// <para>
|
||||
/// 过场 VCam 由设计者在 Inspector 中预先配置(位置、Follow、LookAt、Lens 等);
|
||||
/// 此方法不强制覆写 Follow,保留 Inspector 配置不变。
|
||||
/// </para>
|
||||
/// <para>适用场景:Boss 登场固定镜头、对话拉近、剧情事件全景等。</para>
|
||||
/// </summary>
|
||||
public void EnterCutsceneMode(CinemachineCamera cutsceneCamera)
|
||||
{
|
||||
if (cutsceneCamera == null) return;
|
||||
if (_cutsceneCamera != null && _cutsceneCamera != cutsceneCamera)
|
||||
_cutsceneCamera.Priority = 0;
|
||||
_cutsceneCamera = cutsceneCamera;
|
||||
_cutsceneCamera.Priority = CutscenePriority;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 退出过场模式,撤销过场 VCam 的优先级,Brain 自动混合回当前区域相机。
|
||||
/// </summary>
|
||||
public void ExitCutsceneMode()
|
||||
{
|
||||
if (_cutsceneCamera == null) return;
|
||||
_cutsceneCamera.Priority = 0;
|
||||
_cutsceneCamera = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知相机系统玩家的面朝方向,转发至所有活跃 VCam 的方向偏置扩展。
|
||||
/// 由 PlayerController 在精灵翻转时调用:<c>ICameraService.SetPlayerFacing(翻转方向 ? +1 : -1)</c>。
|
||||
/// </summary>
|
||||
public void SetPlayerFacing(int direction)
|
||||
{
|
||||
_lastExternalFacing = direction;
|
||||
SetFacingOnVCam(_activeDedicatedCam, direction);
|
||||
}
|
||||
|
||||
private static void SetFacingOnVCam(CinemachineCamera vcam, int direction)
|
||||
=> vcam?.GetComponent<CameraFacingBiasExtension>()?.SetExternalFacing(direction);
|
||||
|
||||
private Coroutine _lensCoroutine;
|
||||
|
||||
private void ApplyLensSizeToAll(float size)
|
||||
{
|
||||
SetVcamLens(_vcamA, size);
|
||||
SetVcamLens(_vcamB, size);
|
||||
if (_activeDedicatedCam != null) SetVcamLens(_activeDedicatedCam, size);
|
||||
}
|
||||
|
||||
private static void SetVcamLens(CinemachineCamera vcam, float size)
|
||||
// size = 可见半高(世界单位),透视相机下等效于正交 OrthographicSize。
|
||||
// 换算公式:FOV = 2 * atan(size / depth)
|
||||
private void SetVcamLens(CinemachineCamera vcam, float size)
|
||||
{
|
||||
if (vcam == null) return;
|
||||
var lens = vcam.Lens;
|
||||
lens.OrthographicSize = size;
|
||||
float depth = _lensConfig != null ? _lensConfig.cameraDepth : 10f;
|
||||
lens.FieldOfView = 2f * Mathf.Atan(size / depth) * Mathf.Rad2Deg;
|
||||
vcam.Lens = lens;
|
||||
}
|
||||
|
||||
@@ -286,23 +289,24 @@ namespace BaseGames.Camera
|
||||
{
|
||||
CinemachineCamera active = GetActiveVcam();
|
||||
if (active == null) { _lensCoroutine = null; yield break; }
|
||||
float start = active.Lens.OrthographicSize;
|
||||
// 透视相机:从当前 FOV 反算等效可见半高,作为插值起点
|
||||
float depth = _lensConfig != null ? _lensConfig.cameraDepth : 10f;
|
||||
float start = depth * Mathf.Tan(active.Lens.FieldOfView * 0.5f * Mathf.Deg2Rad);
|
||||
float elapsed = 0f;
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
ApplyLensSizeToAll(Mathf.Lerp(start, target, elapsed / duration));
|
||||
// 使用平滑过渡曲线(ease-in-out),视野缩放手感更自然。
|
||||
// 线性插值会让镜头拉远感觉机械;平滑步多出平稳的起笔和收尾弹性。
|
||||
float t = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(elapsed / duration));
|
||||
ApplyLensSizeToAll(Mathf.Lerp(start, target, t));
|
||||
yield return null;
|
||||
}
|
||||
ApplyLensSizeToAll(target);
|
||||
_lensCoroutine = null;
|
||||
}
|
||||
|
||||
private CinemachineCamera GetActiveVcam()
|
||||
{
|
||||
if (_activeDedicatedCam != null) return _activeDedicatedCam;
|
||||
return _activeSlot == 0 ? _vcamA : (_vcamB != null ? _vcamB : _vcamA);
|
||||
}
|
||||
private CinemachineCamera GetActiveVcam() => _activeDedicatedCam;
|
||||
|
||||
// ── 内部方法 ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -315,64 +319,89 @@ namespace BaseGames.Camera
|
||||
|
||||
_activeDedicatedCam = area.DedicatedCamera;
|
||||
_activeDedicatedCam.Priority = area.DedicatedPriority;
|
||||
SyncFollowToVCam(_activeDedicatedCam); // 确保专有 VCam 的 Follow 指向当前跟随目标
|
||||
SetFacingOnVCam(_activeDedicatedCam, _lastExternalFacing); // 应用当前玩家朝向
|
||||
|
||||
// 应用 CameraArea 参数(Confiner、Composer、扩展组件等)
|
||||
var dedicatedConfiner = _activeDedicatedCam.GetComponent<CinemachineConfiner3D>();
|
||||
ValidateVCamExtensionOrder(_activeDedicatedCam, dedicatedConfiner);
|
||||
ConfigureSlot(_activeDedicatedCam, dedicatedConfiner, area);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用全局 VCam ping-pong 切换到新区域。
|
||||
/// 配置非活跃 VCam 的 Confiner → 提升其优先级 → 降低旧 VCam 优先级。
|
||||
/// Cinemachine Brain 检测到优先级变化后自动触发混合。
|
||||
/// 检查 VCam 上各扩展组件的挂载顺序是否正确。
|
||||
/// <list type="bullet">
|
||||
/// <item>FallBiasExtension / FacingBiasExtension 必须在 CinemachineConfiner3D 之前</item>
|
||||
/// <item>AxisLockExtension 必须在 CinemachineConfiner3D 之后</item>
|
||||
/// </list>
|
||||
/// 顺序错误时相机会在应用偏置后逃出限位区域,或轴锁被 Confiner 覆盖失效。
|
||||
/// </summary>
|
||||
private void ActivateGlobalSlot(CameraArea area)
|
||||
private static void ValidateVCamExtensionOrder(CinemachineCamera vcam, CinemachineConfiner3D confiner)
|
||||
{
|
||||
// 收回专有 VCam
|
||||
if (_activeDedicatedCam != null)
|
||||
{
|
||||
_activeDedicatedCam.Priority = 0;
|
||||
_activeDedicatedCam = null;
|
||||
}
|
||||
if (vcam == null) return;
|
||||
|
||||
bool noVCams = _vcamA == null && _vcamB == null;
|
||||
if (noVCams)
|
||||
if (confiner == null)
|
||||
{
|
||||
Debug.LogWarning("[CameraStateController] 全局 VCam A / B 均未绑定,无法切换相机区域。");
|
||||
Debug.LogWarning(
|
||||
$"[CameraStateController] VCam <b>{vcam.name}</b> 缺少 CinemachineConfiner3D 组件!" +
|
||||
"相机将不受任何限位约束,请通过 CameraAreaEditor 重新生成此 VCam 或手动添加。");
|
||||
return;
|
||||
}
|
||||
|
||||
// 首次调用:直接激活 VCamA(场景淡入阶段,无需混合动画)
|
||||
if (_activeSlot < 0)
|
||||
Component[] comps = vcam.GetComponents<Component>();
|
||||
int confinerIdx = -1;
|
||||
int fallBiasIdx = -1;
|
||||
int facingBiasIdx = -1;
|
||||
int axisLockIdx = -1;
|
||||
int asymDampIdx = -1;
|
||||
int adaptiveLahIdx = -1;
|
||||
|
||||
for (int i = 0; i < comps.Length; i++)
|
||||
{
|
||||
var cam = _vcamA ?? _vcamB;
|
||||
var confiner = _vcamA != null ? _confinerA : _confinerB;
|
||||
ConfigureSlot(cam, confiner, area);
|
||||
SyncFollowToVCam(cam);
|
||||
cam.Priority = _globalActivePriority;
|
||||
_activeSlot = _vcamA != null ? 0 : 1;
|
||||
return;
|
||||
switch (comps[i])
|
||||
{
|
||||
case CinemachineConfiner3D _: confinerIdx = i; break;
|
||||
case CameraFallBiasExtension _: fallBiasIdx = i; break;
|
||||
case CameraFacingBiasExtension _: facingBiasIdx = i; break;
|
||||
case CameraAxisLockExtension _: axisLockIdx = i; break;
|
||||
case CameraAsymmetricDampingExtension _: asymDampIdx = i; break;
|
||||
case CameraAdaptiveLookaheadExtension _: adaptiveLahIdx = i; break;
|
||||
}
|
||||
}
|
||||
|
||||
// 只有一台 VCam 时:直接重新配置,不做优先级 ping-pong
|
||||
// (之前的 null 保护令 inactiveCam == activeCam,导致先升后降为 0 自毁)
|
||||
if (_vcamA == null || _vcamB == null)
|
||||
{
|
||||
var cam = _vcamA ?? _vcamB;
|
||||
var confiner = _vcamA != null ? _confinerA : _confinerB;
|
||||
ConfigureSlot(cam, confiner, area);
|
||||
SyncFollowToVCam(cam);
|
||||
cam.Priority = _globalActivePriority; // 保持激活,不改变 _activeSlot
|
||||
return;
|
||||
}
|
||||
if (asymDampIdx >= 0 && asymDampIdx > confinerIdx)
|
||||
Debug.LogError(
|
||||
$"[CameraStateController] VCam <b>{vcam.name}</b>:" +
|
||||
"CameraAsymmetricDampingExtension 必须在 CinemachineConfiner3D 之前!" +
|
||||
"当前顺序导致Y轴阻尼平滑值将相机推出限位区域而不被重新裁剪。" +
|
||||
"请在 RoomCameraSetupTool 中点击「自动修正组件顺序」按钮。");
|
||||
|
||||
// 双 VCam ping-pong:配置非活跃槽 → 升级其优先级 → 降低活跃槽优先级
|
||||
bool nextIsA = _activeSlot != 0;
|
||||
var inactiveCam = nextIsA ? _vcamA : _vcamB;
|
||||
var activeCam = nextIsA ? _vcamB : _vcamA;
|
||||
var inactiveConfiner = nextIsA ? _confinerA : _confinerB;
|
||||
if (fallBiasIdx >= 0 && fallBiasIdx > confinerIdx)
|
||||
Debug.LogError(
|
||||
$"[CameraStateController] VCam <b>{vcam.name}</b>:" +
|
||||
"CameraFallBiasExtension 必须在 CinemachineConfiner3D 之前!" +
|
||||
"当前顺序导致下坠偏置将相机推出限位区域而不被重新裁剪。" +
|
||||
"请在 RoomCameraSetupTool 中点击「自动修正组件顺序」按钮。");
|
||||
|
||||
ConfigureSlot(inactiveCam, inactiveConfiner, area);
|
||||
SyncFollowToVCam(inactiveCam); // 确保 Follow 正确(防止 SetFollowTarget 未被调用)
|
||||
inactiveCam.Priority = _globalActivePriority;
|
||||
activeCam.Priority = _standbyPriority; // 降到待机但仍 > 0,Brain 可在混合期间读取其状态
|
||||
_activeSlot = nextIsA ? 0 : 1;
|
||||
if (facingBiasIdx >= 0 && facingBiasIdx > confinerIdx)
|
||||
Debug.LogError(
|
||||
$"[CameraStateController] VCam <b>{vcam.name}</b>:" +
|
||||
"CameraFacingBiasExtension 必须在 CinemachineConfiner3D 之前!" +
|
||||
"当前顺序导致朝向偏置将相机推出限位区域而不被重新裁剪。" +
|
||||
"请在 RoomCameraSetupTool 中点击「自动修正组件顺序」按钮。");
|
||||
|
||||
if (adaptiveLahIdx >= 0 && adaptiveLahIdx > confinerIdx)
|
||||
Debug.LogError(
|
||||
$"[CameraStateController] VCam <b>{vcam.name}</b>:" +
|
||||
"CameraAdaptiveLookaheadExtension 必须在 CinemachineConfiner3D 之前!" +
|
||||
"请在 RoomCameraSetupTool 中点击「自动修正组件顺序」按钮。");
|
||||
|
||||
if (axisLockIdx >= 0 && axisLockIdx < confinerIdx)
|
||||
Debug.LogError(
|
||||
$"[CameraStateController] VCam <b>{vcam.name}</b>:" +
|
||||
"CameraAxisLockExtension 必须在 CinemachineConfiner3D 之后!" +
|
||||
"当前顺序导致轴向锁定被 Confiner 覆盖而失效。" +
|
||||
"请在 RoomCameraSetupTool 中点击「自动修正组件顺序」按钮。");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -386,31 +415,18 @@ namespace BaseGames.Camera
|
||||
}
|
||||
|
||||
private static void ConfigureSlot(
|
||||
CinemachineCamera vcam, CinemachineConfiner2D confiner, CameraArea area)
|
||||
CinemachineCamera vcam, CinemachineConfiner3D confiner, CameraArea area)
|
||||
{
|
||||
// 1. Confiner
|
||||
if (confiner != null && area.ConfinerCollider != null)
|
||||
{
|
||||
confiner.BoundingShape2D = area.ConfinerCollider;
|
||||
// 限位多边形已在编辑器中预收缩(可视区域 - 视口半尺寸 = 相机中心运动范围)。
|
||||
// OversizeWindow.MaxWindowSize = 0.001f(极小正值):
|
||||
// 使 Cinemachine 将实际视口高度裁剪至 0.001,几乎不再对多边形额外收缩,
|
||||
// 从而以预收缩后的多边形直接作为相机中心约束边界。
|
||||
// 对于小于视口的房间(预收缩后多边形退化为点),仍正确固定相机于中心。
|
||||
confiner.OversizeWindow = new CinemachineConfiner2D.OversizeWindowSettings
|
||||
{
|
||||
Enabled = true,
|
||||
MaxWindowSize = 0.001f,
|
||||
Padding = 0.1f,
|
||||
};
|
||||
// BoundingShape2D 变更后必须刷新内部缓存路径,否则限位仍使用旧边界
|
||||
confiner.InvalidateLensCache();
|
||||
confiner.BoundingVolume = area.ConfinerCollider;
|
||||
}
|
||||
else if (confiner != null && area.ConfinerCollider == null)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[CameraStateController] {area.name} 未绑定 ConfinerCollider!" +
|
||||
"请将子节点 AreaBoundary 的 PolygonCollider2D 拖入 CameraArea._confinerCollider 字段。");
|
||||
"请将子节点 AreaBoundary 的 BoxCollider 拖入 CameraArea._confinerCollider 字段。");
|
||||
}
|
||||
|
||||
// 2. 跟随行为覆盖
|
||||
@@ -476,6 +492,26 @@ namespace BaseGames.Camera
|
||||
var fallBias = vcam.GetComponent<CameraFallBiasExtension>();
|
||||
if (fallBias != null)
|
||||
fallBias.SetConfiguredMax(area.DisableFallBias ? 0f : -1f);
|
||||
|
||||
// 5. 方向感知水平偏置
|
||||
// X 轴已锁定时强制关闭偏置:两者均在 Body Stage 执行,若偏置后于锁定运行会破坏轴锁。
|
||||
var facingBias = vcam.GetComponent<CameraFacingBiasExtension>();
|
||||
if (facingBias != null)
|
||||
{
|
||||
if (area.LockHorizontal)
|
||||
facingBias.FacingBias = 0f;
|
||||
else if (area.OverrideFacingBias)
|
||||
facingBias.FacingBias = area.FacingBiasOverride;
|
||||
}
|
||||
|
||||
// 6. 相机噪音(区域氛围震动:洞穴、水下、机械等)
|
||||
var noise = vcam.GetComponent<CinemachineBasicMultiChannelPerlin>();
|
||||
if (noise != null)
|
||||
{
|
||||
noise.NoiseProfile = area.NoiseProfile;
|
||||
noise.AmplitudeGain = area.NoiseProfile != null ? area.NoiseAmplitude : 0f;
|
||||
noise.FrequencyGain = area.NoiseFrequency;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResetVCamExtensions(CinemachineCamera vcam)
|
||||
@@ -484,6 +520,7 @@ namespace BaseGames.Camera
|
||||
vcam.GetComponent<CameraAsymmetricDampingExtension>()?.ResetState();
|
||||
vcam.GetComponent<CameraFallBiasExtension>()?.ResetState();
|
||||
vcam.GetComponent<CameraAdaptiveLookaheadExtension>()?.ResetState();
|
||||
vcam.GetComponent<CameraFacingBiasExtension>()?.ResetState();
|
||||
}
|
||||
|
||||
private void ApplyBlendProfile(CameraBlendProfileSO profile)
|
||||
@@ -545,17 +582,17 @@ namespace BaseGames.Camera
|
||||
float x = 12f, y = 12f, w = 320f;
|
||||
|
||||
// 计算高度(先收集内容)
|
||||
string areaName = _currentArea != null ? _currentArea.name : "<无>";
|
||||
string slotLabel = _activeSlot < 0 ? "未初始化"
|
||||
: _activeSlot == 0 ? "VCam A"
|
||||
: "VCam B";
|
||||
string followLabel = _currentFollowTarget != null
|
||||
? _currentFollowTarget.name
|
||||
: "<未设置>";
|
||||
string areaName = _currentArea != null ? _currentArea.name : "<无>";
|
||||
string dedicatedLabel = _activeDedicatedCam != null
|
||||
? $"{_activeDedicatedCam.name} (P={_activeDedicatedCam.Priority})"
|
||||
: "<无激活 VCam>";
|
||||
string followLabel = _currentFollowTarget != null
|
||||
? _currentFollowTarget.name
|
||||
: "<未设置>";
|
||||
|
||||
bool warnFollow = _currentFollowTarget == null;
|
||||
bool warnNoVCam = _vcamA == null && _vcamB == null;
|
||||
bool warnNoBrain = _brain == null;
|
||||
bool warnFollow = _currentFollowTarget == null;
|
||||
bool warnNoVCam = _activeDedicatedCam == null;
|
||||
bool warnNoBrain = _brain == null;
|
||||
|
||||
// 区域状态(基线 + 触发区域集合)
|
||||
var zoneLines = new System.Collections.Generic.List<string>();
|
||||
@@ -569,7 +606,7 @@ namespace BaseGames.Camera
|
||||
zoneLines.Add($" [{e.priority}] {(e.area != null ? e.area.name : "null")}{marker}");
|
||||
}
|
||||
|
||||
int lineCount = 5 + zoneLines.Count + (warnFollow ? 1 : 0) + (warnNoVCam ? 1 : 0) + (warnNoBrain ? 1 : 0);
|
||||
int lineCount = 4 + zoneLines.Count + (warnFollow ? 1 : 0) + (warnNoVCam ? 1 : 0) + (warnNoBrain ? 1 : 0);
|
||||
float rowH = 19f;
|
||||
float h = 28f + lineCount * rowH + 8f;
|
||||
|
||||
@@ -580,12 +617,7 @@ namespace BaseGames.Camera
|
||||
cy += 22f;
|
||||
|
||||
GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), $"当前区域:{areaName}", _debugRowStyle); cy += rowH;
|
||||
GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), $"活跃 VCam 槽:{slotLabel}", _debugRowStyle); cy += rowH;
|
||||
|
||||
string vcamALabel = _vcamA != null ? $"{_vcamA.name} (P={_vcamA.Priority})" : "<未绑定>";
|
||||
string vcamBLabel = _vcamB != null ? $"{_vcamB.name} (P={_vcamB.Priority})" : "<未绑定>";
|
||||
GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), $"VCam A:{vcamALabel}", _debugRowStyle); cy += rowH;
|
||||
GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), $"VCam B:{vcamBLabel}", _debugRowStyle); cy += rowH;
|
||||
GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), $"专有 VCam:{dedicatedLabel}", warnNoVCam ? _debugWarnStyle : _debugRowStyle); cy += rowH;
|
||||
GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), $"Follow 目标:{followLabel}", warnFollow ? _debugWarnStyle : _debugRowStyle); cy += rowH;
|
||||
|
||||
GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), "区域状态(基线 + 触发区域):", _debugRowStyle); cy += rowH;
|
||||
@@ -598,7 +630,7 @@ namespace BaseGames.Camera
|
||||
if (warnFollow)
|
||||
{ GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), "⚠ Follow 目标未设置(检查 _onPlayerSpawned)", _debugWarnStyle); cy += rowH; }
|
||||
if (warnNoVCam)
|
||||
{ GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), "⚠ VCam A/B 均未绑定", _debugWarnStyle); cy += rowH; }
|
||||
{ GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), "⚠ 当前区域无激活专有 VCam(检查 DedicatedCamera 绑定)", _debugWarnStyle); cy += rowH; }
|
||||
if (warnNoBrain)
|
||||
{ GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), "⚠ CinemachineBrain 未绑定", _debugWarnStyle); cy += rowH; }
|
||||
}
|
||||
|
||||
@@ -5,10 +5,27 @@ using BaseGames.Core;
|
||||
|
||||
namespace BaseGames.Camera
|
||||
{
|
||||
/// <summary>
|
||||
/// 相机区域切换模式。
|
||||
/// </summary>
|
||||
public enum CameraZoneSwitchMode
|
||||
{
|
||||
/// <summary>进入即切换(默认)。
|
||||
/// 只要走进触发区域就立刻切换,不需要完全离开当前区域。</summary>
|
||||
Immediate,
|
||||
|
||||
/// <summary>必须离开当前区域才切换。
|
||||
/// 进入新区域后仅将其加入候选列表,等玩家完全离开当前激活区域后再接管。</summary>
|
||||
ExitFirst,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 相机区域切换触发器。
|
||||
/// 当触发区域重叠时,玩家必须先离开当前所在的触发区域,才会切换到下一个区域,
|
||||
/// 而不是进入重叠区域时立即切换。
|
||||
/// 支持两种切换模式,可通过 Inspector 配置:
|
||||
/// <list type="bullet">
|
||||
/// <item><b>Immediate</b>:进入即切换,不等待离开旧区域。</item>
|
||||
/// <item><b>ExitFirst</b>:必须离开当前激活区域后才切换。</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[ExecuteAlways]
|
||||
[RequireComponent(typeof(PolygonCollider2D))]
|
||||
@@ -24,17 +41,36 @@ namespace BaseGames.Camera
|
||||
"相同优先级则后进入的胜出(推荐默认值 1)。")]
|
||||
[SerializeField] private int _priority = 1;
|
||||
|
||||
[Tooltip("切换模式。\n" +
|
||||
"Immediate:进入即切换,无需离开当前区域(默认)。\n" +
|
||||
"ExitFirst:必须离开当前激活区域后才切换。")]
|
||||
[SerializeField] private CameraZoneSwitchMode _switchMode = CameraZoneSwitchMode.Immediate;
|
||||
|
||||
[SerializeField] private string _playerTag = "Player";
|
||||
|
||||
private PolygonCollider2D _collider;
|
||||
private bool _isPlayerInside;
|
||||
|
||||
/// <summary>触发区域优先级(只读),供外部按优先级选择最佳区域。</summary>
|
||||
public int Priority => _priority;
|
||||
|
||||
// ── 静态:跨实例共享触发状态 ──────────────────────────────────────────
|
||||
// 玩家当前物理上所在的所有触发区域(按进入顺序排列)
|
||||
private static readonly List<CameraTriggerZone> s_InsideZones = new();
|
||||
// 当前已向 ICameraService 发出 SwitchArea 请求的触发区域
|
||||
private static CameraTriggerZone s_ActiveZone;
|
||||
|
||||
/// <summary>
|
||||
/// 在每次进入 Play Mode 前(或禁用 Domain Reload 时的跨会话)重置静态状态,
|
||||
/// 防止上一次游戏会话残留的区域引用导致触发逻辑错误。
|
||||
/// </summary>
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void ResetStaticState()
|
||||
{
|
||||
s_InsideZones.Clear();
|
||||
s_ActiveZone = null;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_collider = GetComponent<PolygonCollider2D>();
|
||||
@@ -44,12 +80,7 @@ namespace BaseGames.Camera
|
||||
private void OnDisable()
|
||||
{
|
||||
if (!Application.isPlaying) return;
|
||||
if (!_isPlayerInside) return;
|
||||
|
||||
_isPlayerInside = false;
|
||||
s_InsideZones.Remove(this);
|
||||
if (s_ActiveZone == this)
|
||||
Deactivate(this);
|
||||
HandlePlayerExit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -76,46 +107,103 @@ namespace BaseGames.Camera
|
||||
s_InsideZones.Add(this);
|
||||
}
|
||||
|
||||
if (s_ActiveZone == null)
|
||||
Activate(this);
|
||||
EvaluateAndSwitch();
|
||||
}
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (!Application.isPlaying) return;
|
||||
if (!other.CompareTag(_playerTag)) return;
|
||||
// 兼容碰撞体挂在子节点的玩家结构:先检查碰撞体本身标签,
|
||||
// 再检查其挂载的 Rigidbody2D 所在节点标签(通常为带标签的角色根节点)。
|
||||
if (!other.CompareTag(_playerTag) &&
|
||||
(other.attachedRigidbody == null || !other.attachedRigidbody.CompareTag(_playerTag)))
|
||||
return;
|
||||
if (_targetArea == null || _isPlayerInside) return;
|
||||
|
||||
_isPlayerInside = true;
|
||||
s_InsideZones.Add(this);
|
||||
|
||||
// 没有激活的触发区域 → 立即切换
|
||||
// 已有激活的触发区域 → 等玩家离开后再接管(避免重叠区域间提前切换)
|
||||
if (s_ActiveZone == null)
|
||||
Activate(this);
|
||||
// Immediate:进入即评估切换。
|
||||
// ExitFirst:仅在当前无激活区域时才先先激活,否则等待玩家离开当前激活区域。
|
||||
if (_switchMode == CameraZoneSwitchMode.Immediate || s_ActiveZone == null)
|
||||
EvaluateAndSwitch();
|
||||
}
|
||||
|
||||
private void OnTriggerExit2D(Collider2D other)
|
||||
{
|
||||
if (!Application.isPlaying) return;
|
||||
if (!other.CompareTag(_playerTag)) return;
|
||||
if (!_isPlayerInside) return;
|
||||
if (!other.CompareTag(_playerTag) &&
|
||||
(other.attachedRigidbody == null || !other.attachedRigidbody.CompareTag(_playerTag)))
|
||||
return;
|
||||
|
||||
// 复合碰撞体场景:某个子碰撞体退出时,验证玩家根节点是否仍在区域内。
|
||||
// 若根节点还在区域内(其他碰撞体尚未退出),则忽略此次退出事件。
|
||||
Transform playerRoot = other.attachedRigidbody != null
|
||||
? other.attachedRigidbody.transform
|
||||
: other.transform;
|
||||
if (_collider != null && _collider.OverlapPoint(playerRoot.position)) return;
|
||||
|
||||
HandlePlayerExit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 玩家离开触发区域的统一处理(<see cref="OnTriggerExit2D"/>、
|
||||
/// <see cref="FixedUpdate"/> 边缘检测及 <see cref="OnDisable"/> 共同调用)。
|
||||
/// 带幂等保护,多次调用安全。
|
||||
/// </summary>
|
||||
private void HandlePlayerExit()
|
||||
{
|
||||
if (!_isPlayerInside) return; // 幂等保护:防止重复触发
|
||||
|
||||
_isPlayerInside = false;
|
||||
s_InsideZones.Remove(this);
|
||||
|
||||
if (s_ActiveZone == this)
|
||||
Deactivate(this);
|
||||
else
|
||||
ServiceLocator.GetOrDefault<ICameraService>()?.ReleaseArea(_targetArea, null);
|
||||
}
|
||||
|
||||
// ── 静态辅助方法 ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 评估 <see cref="s_InsideZones"/> 并在需要时切换激活区域。
|
||||
/// <list type="bullet">
|
||||
/// <item>无激活区域时:直接激活最后进入的区域。</item>
|
||||
/// <item>新 <c>SelectBest()</c> 与当前激活不同时:立即覆盖切换。
|
||||
/// 由于 <c>SelectBest</c> 使用 >= 规则,进入任何新区域都会触发切换。</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
private static void EvaluateAndSwitch()
|
||||
{
|
||||
if (s_ActiveZone == null)
|
||||
{
|
||||
Activate(s_InsideZones[s_InsideZones.Count - 1]);
|
||||
return;
|
||||
}
|
||||
|
||||
CameraTriggerZone best = SelectBest();
|
||||
if (best != s_ActiveZone)
|
||||
OverrideActive(best);
|
||||
}
|
||||
|
||||
private static void Activate(CameraTriggerZone zone)
|
||||
{
|
||||
s_ActiveZone = zone;
|
||||
ServiceLocator.GetOrDefault<ICameraService>()?.SwitchArea(zone._targetArea, zone._priority);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 不经过 Exit 事件,直接将激活区域切换为 <paramref name="newZone"/>。
|
||||
/// 旧区域保留在 <see cref="s_InsideZones"/> 中(玩家仍在其内部),
|
||||
/// 不立即释放旧区域——等玩家物理离开旧区域时由 <see cref="OnTriggerExit2D"/> 清理。
|
||||
/// </summary>
|
||||
private static void OverrideActive(CameraTriggerZone newZone)
|
||||
{
|
||||
s_ActiveZone = newZone;
|
||||
ServiceLocator.GetOrDefault<ICameraService>()?.SwitchArea(newZone._targetArea, newZone._priority);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 离开 <paramref name="leaving"/> 时的处理:
|
||||
/// 若还有其他触发区域,先激活最优者再释放 leaving(避免短暂回退到房间基线);
|
||||
@@ -140,12 +228,16 @@ namespace BaseGames.Camera
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>从 <see cref="s_InsideZones"/> 中选出优先级最高的区域。</summary>
|
||||
/// <summary>
|
||||
/// 从 <see cref="s_InsideZones"/> 中选出最优区域。
|
||||
/// 优先级高者优先;优先级相同时取最后进入的区域(后进入的胜出),
|
||||
/// 确保进入任何新区域时都会立即切换,而不是等待离开旧区域。
|
||||
/// </summary>
|
||||
private static CameraTriggerZone SelectBest()
|
||||
{
|
||||
CameraTriggerZone best = s_InsideZones[0];
|
||||
for (int i = 1; i < s_InsideZones.Count; i++)
|
||||
if (s_InsideZones[i]._priority > best._priority)
|
||||
if (s_InsideZones[i]._priority >= best._priority) // >= 使同优先级时后进入的胜出
|
||||
best = s_InsideZones[i];
|
||||
return best;
|
||||
}
|
||||
@@ -155,19 +247,19 @@ namespace BaseGames.Camera
|
||||
if (_collider == null) _collider = GetComponent<PolygonCollider2D>();
|
||||
if (_collider == null || _collider.pathCount == 0) return;
|
||||
|
||||
var pts = new System.Collections.Generic.List<Vector2>();
|
||||
_collider.GetPath(0, pts);
|
||||
if (pts.Count < 2) return;
|
||||
|
||||
Gizmos.matrix = transform.localToWorldMatrix;
|
||||
Vector2 off = _collider.offset;
|
||||
|
||||
// 多边形触发边界(进入检测外框)
|
||||
Gizmos.color = new Color(0.2f, 0.8f, 1f, 0.8f);
|
||||
var pts = new List<Vector2>();
|
||||
_collider.GetPath(0, pts);
|
||||
for (int i = 0; i < pts.Count; i++)
|
||||
{
|
||||
Vector2 a = pts[i] + off;
|
||||
Vector2 b = pts[(i + 1) % pts.Count] + off;
|
||||
Gizmos.DrawLine(new Vector3(a.x, a.y), new Vector3(b.x, b.y));
|
||||
Vector3 a = new Vector3(pts[i].x, pts[i].y, 0f);
|
||||
Vector3 b = new Vector3(pts[(i + 1) % pts.Count].x, pts[(i + 1) % pts.Count].y, 0f);
|
||||
Gizmos.DrawLine(a, b);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using UnityEngine;
|
||||
using Unity.Cinemachine;
|
||||
|
||||
namespace BaseGames.Camera
|
||||
{
|
||||
@@ -27,7 +28,7 @@ namespace BaseGames.Camera
|
||||
/// </summary>
|
||||
void ReleaseArea(CameraArea releasedArea, CameraArea fallback);
|
||||
|
||||
/// <summary>为全局双 VCam 设置跟随目标(Player/CameraFollowTarget)。</summary>
|
||||
/// <summary>运行时设置跟随目标(Player/CameraFollowTarget),激活区域时自动同步到专属 VCam。</summary>
|
||||
void SetFollowTarget(Transform followTarget);
|
||||
|
||||
/// <summary>触发屏幕抖动(指定速度矢量)。</summary>
|
||||
@@ -37,9 +38,28 @@ namespace BaseGames.Camera
|
||||
void TriggerImpulse(float strength = 0.3f);
|
||||
|
||||
/// <summary>
|
||||
/// 平滑过渡正交相机尺寸。<paramref name="duration"/> = 0 时瞬间切换。
|
||||
/// 平滑过渡视野尺寸(可视半高,世界单位)。<paramref name="duration"/> = 0 时瞬间切换。
|
||||
/// 透视相机下自动换算为 FOV,与正交相机的 OrthographicSize 语义等价。
|
||||
/// 适用于 Boss 战拉远、特殊演出室拉近等场景。
|
||||
/// </summary>
|
||||
void SetLensSize(float orthographicSize, float duration = 0f);
|
||||
void SetLensSize(float visibleHalfHeight, float duration = 0f);
|
||||
|
||||
/// <summary>
|
||||
/// 进入过场模式,激活指定 VCam(优先级高于所有区域相机),Brain 自动混合。
|
||||
/// VCam 的 Follow / LookAt / Lens 由设计者在 Inspector 中配置,接口不强制覆写。
|
||||
/// </summary>
|
||||
void EnterCutsceneMode(CinemachineCamera cutsceneCamera);
|
||||
|
||||
/// <summary>
|
||||
/// 退出过场模式,撤销过场 VCam 的优先级,Brain 自动混合回当前区域相机。
|
||||
/// </summary>
|
||||
void ExitCutsceneMode();
|
||||
|
||||
/// <summary>
|
||||
/// 通知相机系统玩家的面朝方向,使方向感知偏置立即响应。
|
||||
/// 由 PlayerController 在精灵翻转时调用(<c>OnFlip</c>)。
|
||||
/// direction: +1 = 朝右,-1 = 朝左,0 = 清除外部输入(回退到速度估算)。
|
||||
/// </summary>
|
||||
void SetPlayerFacing(int direction);
|
||||
}
|
||||
}
|
||||
|
||||
44
Assets/_Game/Scripts/Camera/SpritePixelSnapper.cs
Normal file
44
Assets/_Game/Scripts/Camera/SpritePixelSnapper.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Camera
|
||||
{
|
||||
/// <summary>
|
||||
/// 精灵像素对齐组件。
|
||||
///
|
||||
/// 挂载在含 <see cref="SpriteRenderer"/> 的 GameObject 上(可与 Rigidbody2D 同节点)。
|
||||
/// 每渲染帧在 LateUpdate(+1000,与 <see cref="CameraPixelSnapper"/> 同执行序)中,
|
||||
/// 将 <c>transform.position</c> 四舍五入到最近像素网格,消除精灵移动时的亚像素模糊。
|
||||
///
|
||||
/// <para>
|
||||
/// 与 <see cref="CameraPixelSnapper"/> 搭配使用时,须保持 <c>_pixelsPerUnit</c>
|
||||
/// 与相机侧一致,使精灵和相机吸附到同一像素格,从而消除二者相对偏移造成的抖动。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Rigidbody2D 内部物理位置(<c>rb.position</c>)不受影响;
|
||||
/// 仅 <c>transform.position</c>(用于渲染)被修正,最大偏差 ±0.5 / PPU。
|
||||
/// 子节点(HurtBox、GroundCheck 等)随父节点微小平移,但偏差在物理容差范围内,
|
||||
/// 不影响碰撞检测精度。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(1000)]
|
||||
[AddComponentMenu("BaseGames/Camera/Sprite Pixel Snapper")]
|
||||
public class SpritePixelSnapper : MonoBehaviour
|
||||
{
|
||||
[Tooltip("每世界单位像素数(PPU)。须与 CameraPixelSnapper 及精灵导入设置保持一致。\n" +
|
||||
"常用值:16(粗像素风格)、32(标准像素风格)。0 = 禁用对齐。")]
|
||||
[Min(0f)]
|
||||
[SerializeField] private float _pixelsPerUnit = 16f;
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (_pixelsPerUnit <= 0f) return;
|
||||
|
||||
float ppu = _pixelsPerUnit;
|
||||
Vector3 pos = transform.position;
|
||||
pos.x = Mathf.Round(pos.x * ppu) / ppu;
|
||||
pos.y = Mathf.Round(pos.y * ppu) / ppu;
|
||||
// Z 轴不参与像素对齐
|
||||
transform.position = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Camera/SpritePixelSnapper.cs.meta
Normal file
11
Assets/_Game/Scripts/Camera/SpritePixelSnapper.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 91f07a72af066da4684b9194b370a6f9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user