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(); } } // 捕获漂移 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; } /// /// 按分辨率微调相机:在设计基准上叠加 X 旋转增量、Y 高度增量、Z 位置增量。 /// 改的是漂移 pivot(不动相机本身),且同步 cameraDriftBaseLocalPosition 的 y/z, /// 与漂移/抖动/技能偏移/冲刺全部不冲突。可重复调用(总是相对设计基准,不累积)。 /// 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; /// /// 按分辨率**直接调整相机本体(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 基准。 /// 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(); 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(); if (shake == null) { shake = cachedShakeTarget.gameObject.AddComponent(); } 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(); if (controller == null) return; controller.CacheShakeTarget(); controller.UpdatePlayerSkillCameraOffset(); } public static void TryTriggerMultiNoteShake() { effectEventController controller = Instance != null ? Instance : SceneObjectLookupCache.FindAny(); if (controller == null) return; controller.TriggerMultiNoteShakeInternal(); } public static void TryTriggerTrackCrash(int trackIndex) { effectEventController controller = Instance != null ? Instance : SceneObjectLookupCache.FindAny(); 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(); if (shake == null) { shake = cachedShakeTarget.gameObject.AddComponent(); } 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); } }