角色能力,存档

This commit is contained in:
2026-05-19 11:50:21 +08:00
parent d25f237e76
commit 2dcb7a961a
136 changed files with 36035 additions and 27551 deletions

View File

@@ -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("全局双 VCamPersistent 场景中放置两台通用虚拟相机)")]
[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 = A1 = 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; // 降到待机但仍 > 0Brain 可在混合期间读取其状态
_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; }
}