Files
2026-07-30 23:15:58 +08:00

507 lines
19 KiB
C#
Raw Permalink 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 SmoothShakeFree;
using UnityEngine;
public class effectEventController : MonoBehaviour
{
public enum CameraOffsetAxis
{
X,
Y,
Z
}
public static effectEventController Instance { get; private set; }
[Header("Multi Note Screen Shake")]
public bool enableMultiNoteScreenShake = true;
public float multiNoteScreenShakeAmplitude = 0.08f;
[Header("Camera Drift")]
public bool enableCameraDrift = false;
public Vector2 cameraDriftRange = new Vector2(0.15f, 0.08f);
public float cameraDriftSpeed = 0.3f;
[Header("Player Skill 10 Camera Offset")]
public float playerSkillCameraOffsetAmount = 0.6f;
public CameraOffsetAxis playerSkillCameraOffsetAxis = CameraOffsetAxis.Y;
[Header("Track Crash")]
[SerializeField] private TrackCrashController trackCrashController;
private Transform cachedShakeTarget;
private Transform cachedCameraDriftPivot;
private Transform cachedCameraSkillPivot;
private Vector3 cameraDriftBaseLocalPosition;
private Vector3 cameraSkillBaseLocalPosition;
private float cameraDriftSeedX;
private float cameraDriftSeedY;
// 分辨率相机微调:改的是"漂移 pivot"(顶层节点,承载 -40°/-8 基准),不碰相机自身,
// 因此不与 SmoothShake(写相机 local)、技能偏移(写技能 pivot)、冲刺(写相机 z)冲突。
// 改 pivot 的 X 旋转无任何系统覆盖;改 pivot 的 Y/Z 必须同步 cameraDriftBaseLocalPosition
// 否则 UpdateCameraDrift/ResetCameraDriftIfNeeded 会用旧基准把 Y/Z 拉回。
private bool _cameraBaseCaptured;
private float _designPivotRotX; // pivot 初始 X 旋转(如 -40)
private float _designPivotY; // pivot 初始 Y(如 -6,相机高度)
private float _designPivotZ; // pivot 初始 Z(如 -8)
private void Awake()
{
if (Instance == null)
{
Instance = this;
cameraDriftSeedX = Random.Range(0f, 1000f);
cameraDriftSeedY = Random.Range(1000f, 2000f);
CacheShakeTarget();
return;
}
if (Instance != this)
{
Destroy(gameObject);
}
}
private void OnEnable()
{
CacheShakeTarget();
ResetCameraDriftIfNeeded();
CacheTrackCrashController();
}
// 门控:漂移关闭且已归位后,跳过每帧的 Perlin/缓存查找;开关切换时才归位一次。
private bool _driftResetDone;
// Last PlayerSkillService.SelectionVersion this controller applied its camera offset for.
// While it matches the current version (and the pivot is already resolved), the offset
// cannot change, so we skip the per-frame IsSkillEnabled traversal + localPosition write.
private int _appliedSkillSelectionVersion = -1;
private void Update()
{
if (enableCameraDrift)
{
_driftResetDone = false;
UpdateCameraDrift();
}
else if (!_driftResetDone)
{
// 关闭瞬间归位一次,之后不再每帧空跑。
UpdateCameraDrift();
_driftResetDone = true;
}
// The camera offset only depends on the selected skill. Re-evaluate it once per
// selection change; while the version is unchanged and the pivot is resolved, the
// result is identical, so we can skip the work entirely.
if (cachedCameraSkillPivot == null || _appliedSkillSelectionVersion != PlayerSkillService.SelectionVersion)
{
UpdatePlayerSkillCameraOffset();
}
}
private void CacheShakeTarget()
{
if (Camera.main != null)
{
cachedShakeTarget = Camera.main.transform;
PrewarmShakeComponent();
EnsureCameraDriftPivot();
EnsureCameraSkillPivot();
}
}
private void CacheTrackCrashController()
{
if (trackCrashController == null)
{
trackCrashController = SceneObjectLookupCache.FindAny<TrackCrashController>();
}
}
// 捕获漂移 pivot 的设计基准(X旋转/Z)一次。pivot 建好后调用即安全。
private void CaptureCameraBaseIfNeeded()
{
if (_cameraBaseCaptured) return;
if (cachedCameraDriftPivot == null)
{
CacheShakeTarget(); // 确保 pivot 已建立
if (cachedCameraDriftPivot == null) return;
}
_designPivotRotX = cachedCameraDriftPivot.localEulerAngles.x;
_designPivotY = cachedCameraDriftPivot.localPosition.y;
_designPivotZ = cachedCameraDriftPivot.localPosition.z;
_cameraBaseCaptured = true;
}
/// <summary>
/// 按分辨率微调相机:在设计基准上叠加 X 旋转增量、Y 高度增量、Z 位置增量。
/// 改的是漂移 pivot(不动相机本身),且同步 cameraDriftBaseLocalPosition 的 y/z
/// 与漂移/抖动/技能偏移/冲刺全部不冲突。可重复调用(总是相对设计基准,不累积)。
/// </summary>
public void SetResolutionCameraAdjust(float extraRotX, float extraZ, float extraY = 0f)
{
CaptureCameraBaseIfNeeded();
if (cachedCameraDriftPivot == null) return;
// X 旋转:设计值 + 增量。pivot 的 rotation 无系统写入,直接设安全。
Vector3 euler = cachedCameraDriftPivot.localEulerAngles;
euler.x = _designPivotRotX + extraRotX;
cachedCameraDriftPivot.localEulerAngles = euler;
// Y(高度)与 Z:设计值 + 增量。漂移每帧写 base + (perlinX, perlinY, 0)
// 故必须同步 cameraDriftBaseLocalPosition 的 y/z,否则下一帧漂移会用旧基准拉回。
float targetY = _designPivotY + extraY;
float targetZ = _designPivotZ + extraZ;
Vector3 lp = cachedCameraDriftPivot.localPosition;
lp.y = targetY;
lp.z = targetZ;
cachedCameraDriftPivot.localPosition = lp;
cameraDriftBaseLocalPosition.y = targetY; // 关键:否则会被 UpdateCameraDrift/Reset 拉回
cameraDriftBaseLocalPosition.z = targetZ;
}
// 相机本体(Main Camera)分辨率适配的 local 基准。运行时相机被重挂到 skill_pivot 之下、
// local 位姿被清零(见 EnsureCameraDriftPivot/EnsureCameraSkillPivot),故基准 z≈0、X≈0。
// 首次调用 SetResolutionCameraSelf 时惰性捕获,供插值 t=0 回到该基准。
private bool _cameraSelfBaseCaptured;
private float _cameraSelfBaseLocalY;
private float _cameraSelfBaseLocalZ;
private float _cameraSelfBaseLocalRotX;
/// <summary>
/// 按分辨率**直接调整相机本体(Main Camera 子物体)**的 local z 高度与 local X 旋转,
/// 不动其父/祖父 pivot。t=0 → 回到相机 local 设计基准(运行时为 0/identity,与未调整一致)
/// t=1 → 相机 local 到达 (targetLocalRotX, targetLocalZ)。始终相对基准插值,不累积。
///
/// 冲突处理:相机本体的 local 由 SmoothShake(抖动时每帧写)与冲刺(动画 local z)共享。
/// - 空闲时二者不写相机(SmoothShake 无活动协程即不 ApplySum),本次写入持久生效;
/// - 抖动/冲刺触发时会以"当前 local"为基准重新捕获(SaveDefaultValues / CacheCameraTransform)
/// 故结束后回到本方法设定的值。为兼容"抖动进行中改分辨率"的罕见情形,
/// 若相机上已有 SmoothShake 且正在抖动,同步其 startPosition/startRotation 基准。
/// </summary>
public bool SetResolutionCameraSelf(float t, float targetLocalRotX, float targetLocalZ, float targetLocalY = 0f)
{
if (cachedShakeTarget == null)
{
CacheShakeTarget();
if (cachedShakeTarget == null) return false; // 相机(Camera.main)尚未就绪,稍后重试
}
t = Mathf.Clamp01(t);
if (!_cameraSelfBaseCaptured)
{
_cameraSelfBaseLocalY = cachedShakeTarget.localPosition.y;
_cameraSelfBaseLocalZ = cachedShakeTarget.localPosition.z;
_cameraSelfBaseLocalRotX = NormalizeSignedAngle(cachedShakeTarget.localEulerAngles.x);
_cameraSelfBaseCaptured = true;
}
float y = Mathf.Lerp(_cameraSelfBaseLocalY, targetLocalY, t);
float z = Mathf.Lerp(_cameraSelfBaseLocalZ, targetLocalZ, t);
float rotX = Mathf.Lerp(_cameraSelfBaseLocalRotX, targetLocalRotX, t);
Vector3 lp = cachedShakeTarget.localPosition;
lp.y = y;
lp.z = z;
cachedShakeTarget.localPosition = lp;
Vector3 euler = cachedShakeTarget.localEulerAngles;
euler.x = rotX;
cachedShakeTarget.localEulerAngles = euler;
SyncShakeBaseIfActive(lp, euler);
return true;
}
// 若相机本体上的 SmoothShake 正在抖动,同步其 start 基准,避免抖动进行中改分辨率被回弹覆盖。
private void SyncShakeBaseIfActive(Vector3 localPos, Vector3 localEuler)
{
if (cachedShakeTarget == null) return;
var shake = cachedShakeTarget.GetComponent<SmoothShakeFree.SmoothShake>();
if (shake == null) return;
if (shake.activeShakeRoutines == null || shake.activeShakeRoutines.Count == 0) return;
var type = typeof(SmoothShakeFree.SmoothShake);
const System.Reflection.BindingFlags flags =
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance;
var startPosField = type.GetField("startPosition", flags);
var startRotField = type.GetField("startRotation", flags);
if (startPosField != null) startPosField.SetValue(shake, localPos);
if (startRotField != null) startRotField.SetValue(shake, localEuler);
}
// Unity eulerAngles.x 存 [0,360)-40 存成 320;归一到 [-180,180) 便于插值(负=朝上)。
private static float NormalizeSignedAngle(float angle)
{
angle %= 360f;
if (angle > 180f) angle -= 360f;
else if (angle < -180f) angle += 360f;
return angle;
}
private void PrewarmShakeComponent()
{
if (cachedShakeTarget == null)
return;
SmoothShake shake = cachedShakeTarget.GetComponent<SmoothShake>();
if (shake == null)
{
shake = cachedShakeTarget.gameObject.AddComponent<SmoothShake>();
}
if (shake.positionShake == null) shake.positionShake = new Shaker();
if (shake.rotationShake == null) shake.rotationShake = new Shaker();
if (shake.shakers == null || shake.shakers.Length != 2)
shake.shakers = new[] { shake.positionShake, shake.rotationShake };
if (shake.sum == null || shake.sum.Length != 2)
shake.sum = new Vector3[2];
}
private void EnsureCameraDriftPivot()
{
if (cachedShakeTarget == null)
return;
if (cachedCameraDriftPivot != null)
return;
Transform currentParent = cachedShakeTarget.parent;
if (currentParent != null && currentParent.name == "__effect_event_camera_drift_pivot")
{
cachedCameraDriftPivot = currentParent;
cameraDriftBaseLocalPosition = cachedCameraDriftPivot.localPosition;
return;
}
GameObject pivotObject = new GameObject("__effect_event_camera_drift_pivot");
Transform pivot = pivotObject.transform;
if (currentParent != null)
{
pivot.SetParent(currentParent, false);
pivot.localPosition = cachedShakeTarget.localPosition;
pivot.localRotation = cachedShakeTarget.localRotation;
pivot.localScale = Vector3.one;
}
else
{
pivot.position = cachedShakeTarget.position;
pivot.rotation = cachedShakeTarget.rotation;
}
cachedShakeTarget.SetParent(pivot, true);
cachedShakeTarget.localRotation = Quaternion.identity;
cachedCameraDriftPivot = pivot;
cameraDriftBaseLocalPosition = cachedCameraDriftPivot.localPosition;
}
private void EnsureCameraSkillPivot()
{
if (cachedShakeTarget == null)
return;
if (cachedCameraSkillPivot != null)
return;
Transform currentParent = cachedShakeTarget.parent;
if (currentParent != null && currentParent.name == "__effect_event_camera_skill_pivot")
{
cachedCameraSkillPivot = currentParent;
cameraSkillBaseLocalPosition = cachedCameraSkillPivot.localPosition;
return;
}
GameObject pivotObject = new GameObject("__effect_event_camera_skill_pivot");
Transform pivot = pivotObject.transform;
if (currentParent != null)
{
pivot.SetParent(currentParent, false);
pivot.localPosition = cachedShakeTarget.localPosition;
pivot.localRotation = cachedShakeTarget.localRotation;
pivot.localScale = Vector3.one;
}
else
{
pivot.position = cachedShakeTarget.position;
pivot.rotation = cachedShakeTarget.rotation;
pivot.localScale = Vector3.one;
}
cachedShakeTarget.SetParent(pivot, true);
cachedShakeTarget.localPosition = Vector3.zero;
cachedShakeTarget.localRotation = Quaternion.identity;
cachedCameraSkillPivot = pivot;
cameraSkillBaseLocalPosition = cachedCameraSkillPivot.localPosition;
}
private void UpdateCameraDrift()
{
if (cachedShakeTarget == null)
{
CacheShakeTarget();
}
if (cachedShakeTarget == null)
return;
EnsureCameraDriftPivot();
if (cachedCameraDriftPivot == null)
return;
if (!enableCameraDrift)
{
ResetCameraDriftIfNeeded();
return;
}
float speed = Mathf.Max(0f, cameraDriftSpeed);
float time = GameplayClock.NowSongTime * speed;
float offsetX = (Mathf.PerlinNoise(cameraDriftSeedX, time) - 0.5f) * 2f * Mathf.Abs(cameraDriftRange.x);
float offsetY = (Mathf.PerlinNoise(cameraDriftSeedY, time) - 0.5f) * 2f * Mathf.Abs(cameraDriftRange.y);
cachedCameraDriftPivot.localPosition = cameraDriftBaseLocalPosition + new Vector3(offsetX, offsetY, 0f);
}
private void ResetCameraDriftIfNeeded()
{
if (cachedCameraDriftPivot == null)
return;
cachedCameraDriftPivot.localPosition = cameraDriftBaseLocalPosition;
}
private void UpdatePlayerSkillCameraOffset()
{
if (cachedShakeTarget == null)
{
CacheShakeTarget();
}
if (cachedShakeTarget == null)
return;
EnsureCameraSkillPivot();
if (cachedCameraSkillPivot == null)
return;
// Pivot is resolved; mark this selection version as applied so Update can skip
// subsequent frames until the selection changes again.
_appliedSkillSelectionVersion = PlayerSkillService.SelectionVersion;
Vector3 targetLocalPosition = cameraSkillBaseLocalPosition;
if (PlayerSkillService.IsSkillEnabled(10))
{
float offset = playerSkillCameraOffsetAmount;
switch (playerSkillCameraOffsetAxis)
{
case CameraOffsetAxis.X:
targetLocalPosition.x += offset;
break;
case CameraOffsetAxis.Y:
targetLocalPosition.y += offset;
break;
case CameraOffsetAxis.Z:
targetLocalPosition.z += offset;
break;
}
}
if (cachedCameraSkillPivot.localPosition != targetLocalPosition)
{
cachedCameraSkillPivot.localPosition = targetLocalPosition;
}
}
public static void RefreshPlayerSkillCameraOffset()
{
effectEventController controller = Instance != null ? Instance : SceneObjectLookupCache.FindAny<effectEventController>();
if (controller == null)
return;
controller.CacheShakeTarget();
controller.UpdatePlayerSkillCameraOffset();
}
public static void TryTriggerMultiNoteShake()
{
effectEventController controller = Instance != null ? Instance : SceneObjectLookupCache.FindAny<effectEventController>();
if (controller == null)
return;
controller.TriggerMultiNoteShakeInternal();
}
public static void TryTriggerTrackCrash(int trackIndex)
{
effectEventController controller = Instance != null ? Instance : SceneObjectLookupCache.FindAny<effectEventController>();
if (controller == null)
return;
controller.TriggerTrackCrashInternal(trackIndex);
}
private void TriggerMultiNoteShakeInternal()
{
if (!enableMultiNoteScreenShake)
return;
if (cachedShakeTarget == null)
{
CacheShakeTarget();
}
if (cachedShakeTarget == null)
return;
float amplitudeValue = Mathf.Max(0f, multiNoteScreenShakeAmplitude);
if (amplitudeValue <= 0f)
return;
SmoothShake shake = cachedShakeTarget.GetComponent<SmoothShake>();
if (shake == null)
{
shake = cachedShakeTarget.gameObject.AddComponent<SmoothShake>();
}
if (shake.positionShake == null) shake.positionShake = new Shaker();
if (shake.rotationShake == null) shake.rotationShake = new Shaker();
shake.positionShake.noiseType = Shaker.NoiseType.SineWave;
shake.positionShake.amplitude = new Vector3(amplitudeValue, amplitudeValue, 0f);
shake.positionShake.frequency = new Vector3(20f, 20f, 0f);
shake.rotationShake.amplitude = Vector3.zero;
shake.rotationShake.frequency = Vector3.zero;
shake.timeSettings.constantShake = false;
shake.timeSettings.fadeInDuration = 0.03f;
shake.timeSettings.holdDuration = 0.04f;
shake.timeSettings.fadeOutDuration = 0.05f;
if (shake.timeSettings.fadeInCurve == null || shake.timeSettings.fadeInCurve.length == 0)
shake.timeSettings.fadeInCurve = AnimationCurve.Linear(0f, 0f, 1f, 1f);
if (shake.timeSettings.fadeOutCurve == null || shake.timeSettings.fadeOutCurve.length == 0)
shake.timeSettings.fadeOutCurve = AnimationCurve.Linear(0f, 1f, 1f, 0f);
// 复用已同步的内部数组(通常 PrewarmShakeComponent 已建好),仅在缺失/尺寸不符时重建,
// 避免每次多押命中都分配两个数组。
if (shake.shakers == null || shake.shakers.Length != 2)
shake.shakers = new[] { shake.positionShake, shake.rotationShake };
if (shake.sum == null || shake.sum.Length != 2)
shake.sum = new Vector3[2];
shake.StartShake();
}
private void TriggerTrackCrashInternal(int trackIndex)
{
CacheTrackCrashController();
if (trackCrashController == null)
return;
trackCrashController.TriggerTrackCrash(trackIndex);
}
}