485 lines
17 KiB
C#
485 lines
17 KiB
C#
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;
|
||
|
||
private void Update()
|
||
{
|
||
if (enableCameraDrift)
|
||
{
|
||
_driftResetDone = false;
|
||
UpdateCameraDrift();
|
||
}
|
||
else if (!_driftResetDone)
|
||
{
|
||
// 关闭瞬间归位一次,之后不再每帧空跑。
|
||
UpdateCameraDrift();
|
||
_driftResetDone = true;
|
||
}
|
||
|
||
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 _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)
|
||
{
|
||
if (cachedShakeTarget == null)
|
||
{
|
||
CacheShakeTarget();
|
||
if (cachedShakeTarget == null) return false; // 相机(Camera.main)尚未就绪,稍后重试
|
||
}
|
||
t = Mathf.Clamp01(t);
|
||
|
||
if (!_cameraSelfBaseCaptured)
|
||
{
|
||
_cameraSelfBaseLocalZ = cachedShakeTarget.localPosition.z;
|
||
_cameraSelfBaseLocalRotX = NormalizeSignedAngle(cachedShakeTarget.localEulerAngles.x);
|
||
_cameraSelfBaseCaptured = true;
|
||
}
|
||
|
||
float z = Mathf.Lerp(_cameraSelfBaseLocalZ, targetLocalZ, t);
|
||
float rotX = Mathf.Lerp(_cameraSelfBaseLocalRotX, targetLocalRotX, t);
|
||
|
||
Vector3 lp = cachedShakeTarget.localPosition;
|
||
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;
|
||
|
||
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);
|
||
|
||
shake.shakers = new[] { shake.positionShake, shake.rotationShake };
|
||
shake.sum = new Vector3[2];
|
||
shake.StartShake();
|
||
}
|
||
|
||
private void TriggerTrackCrashInternal(int trackIndex)
|
||
{
|
||
CacheTrackCrashController();
|
||
if (trackCrashController == null)
|
||
return;
|
||
|
||
trackCrashController.TriggerTrackCrash(trackIndex);
|
||
}
|
||
}
|