2026-04-18 13:57:19 -04:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using Cielonos.MainGame;
|
2026-07-18 03:16:20 -04:00
|
|
|
|
using SLSUtilities.Feedback;
|
2026-04-18 13:57:19 -04:00
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Cielonos.MainGame.Effects.Feedback
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 时间缩放通道数据,用于事件传输。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[Serializable]
|
|
|
|
|
|
public struct TimeScaleChannelData
|
|
|
|
|
|
{
|
|
|
|
|
|
public bool active;
|
|
|
|
|
|
public TimeScaleMode mode;
|
|
|
|
|
|
public float fixedValue;
|
|
|
|
|
|
public AnimationCurve curve;
|
|
|
|
|
|
public float remapZero;
|
|
|
|
|
|
public float remapOne;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 根据归一化进度计算当前通道的时间缩放值。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public float Evaluate(float normalizedTime)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!active) return 1f;
|
|
|
|
|
|
|
|
|
|
|
|
if (mode == TimeScaleMode.Fixed)
|
|
|
|
|
|
{
|
|
|
|
|
|
return fixedValue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
float curveValue = curve != null ? curve.Evaluate(normalizedTime) : 0f;
|
|
|
|
|
|
return Mathf.LerpUnclamped(remapZero, remapOne, curveValue);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 时间缩放震动事件。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public struct TimeScaleShakeEvent
|
|
|
|
|
|
{
|
|
|
|
|
|
private static event ShakeDelegate OnEvent;
|
|
|
|
|
|
|
|
|
|
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
|
|
|
|
|
private static void RuntimeInitialization() { OnEvent = null; }
|
|
|
|
|
|
|
|
|
|
|
|
public delegate void ShakeDelegate(
|
2026-07-18 03:16:20 -04:00
|
|
|
|
FeedbackContext feedbackContext,
|
2026-04-18 13:57:19 -04:00
|
|
|
|
float duration,
|
2026-07-18 03:16:20 -04:00
|
|
|
|
int priority,
|
2026-04-18 13:57:19 -04:00
|
|
|
|
TimeScaleChannelData global,
|
|
|
|
|
|
TimeScaleChannelData player,
|
|
|
|
|
|
TimeScaleChannelData enemy,
|
|
|
|
|
|
TimeScaleChannelData allied,
|
|
|
|
|
|
TimeScaleChannelData nonPlayer,
|
|
|
|
|
|
bool stop
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 注册震动监听。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static void Register(ShakeDelegate callback) { OnEvent += callback; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 取消震动监听。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static void Unregister(ShakeDelegate callback) { OnEvent -= callback; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 触发时间缩放震动事件。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static void Trigger(
|
2026-07-18 03:16:20 -04:00
|
|
|
|
FeedbackContext feedbackContext,
|
2026-04-18 13:57:19 -04:00
|
|
|
|
float duration,
|
2026-07-18 03:16:20 -04:00
|
|
|
|
int priority = 0,
|
2026-04-18 13:57:19 -04:00
|
|
|
|
TimeScaleChannelData global = default,
|
|
|
|
|
|
TimeScaleChannelData player = default,
|
|
|
|
|
|
TimeScaleChannelData enemy = default,
|
|
|
|
|
|
TimeScaleChannelData allied = default,
|
|
|
|
|
|
TimeScaleChannelData nonPlayer = default,
|
|
|
|
|
|
bool stop = false)
|
|
|
|
|
|
{
|
2026-07-18 03:16:20 -04:00
|
|
|
|
OnEvent?.Invoke(feedbackContext, duration, priority, global, player, enemy, allied, nonPlayer, stop);
|
2026-04-18 13:57:19 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 时间缩放震动实例。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class TimeScaleShakeInstance
|
|
|
|
|
|
{
|
|
|
|
|
|
public readonly float Duration;
|
|
|
|
|
|
public readonly TimeScaleChannelData Global;
|
|
|
|
|
|
public readonly TimeScaleChannelData Player;
|
|
|
|
|
|
public readonly TimeScaleChannelData Enemy;
|
|
|
|
|
|
public readonly TimeScaleChannelData Allied;
|
|
|
|
|
|
public readonly TimeScaleChannelData NonPlayer;
|
2026-07-18 03:16:20 -04:00
|
|
|
|
public readonly int Priority;
|
|
|
|
|
|
public readonly int Sequence;
|
|
|
|
|
|
public readonly FeedbackPlayer PlayerOwner;
|
|
|
|
|
|
public readonly int RuntimeFeedbackId;
|
|
|
|
|
|
public readonly int TrackIndex;
|
|
|
|
|
|
public readonly int ClipIndex;
|
2026-04-18 13:57:19 -04:00
|
|
|
|
|
|
|
|
|
|
public float Timer;
|
|
|
|
|
|
|
|
|
|
|
|
public TimeScaleShakeInstance(
|
2026-07-18 03:16:20 -04:00
|
|
|
|
FeedbackContext feedbackContext,
|
2026-04-18 13:57:19 -04:00
|
|
|
|
float duration,
|
2026-07-18 03:16:20 -04:00
|
|
|
|
int priority,
|
|
|
|
|
|
int sequence,
|
2026-04-18 13:57:19 -04:00
|
|
|
|
TimeScaleChannelData global,
|
|
|
|
|
|
TimeScaleChannelData player,
|
|
|
|
|
|
TimeScaleChannelData enemy,
|
|
|
|
|
|
TimeScaleChannelData allied,
|
|
|
|
|
|
TimeScaleChannelData nonPlayer)
|
|
|
|
|
|
{
|
|
|
|
|
|
Duration = duration;
|
|
|
|
|
|
Global = global;
|
|
|
|
|
|
Player = player;
|
|
|
|
|
|
Enemy = enemy;
|
|
|
|
|
|
Allied = allied;
|
|
|
|
|
|
NonPlayer = nonPlayer;
|
2026-07-18 03:16:20 -04:00
|
|
|
|
Priority = priority;
|
|
|
|
|
|
Sequence = sequence;
|
|
|
|
|
|
PlayerOwner = feedbackContext.player;
|
|
|
|
|
|
RuntimeFeedbackId = feedbackContext.runtimeFeedbackId;
|
|
|
|
|
|
TrackIndex = feedbackContext.trackIndex;
|
|
|
|
|
|
ClipIndex = feedbackContext.clipIndex;
|
2026-04-18 13:57:19 -04:00
|
|
|
|
Timer = 0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool IsFinished => Timer >= Duration;
|
2026-07-18 03:16:20 -04:00
|
|
|
|
|
|
|
|
|
|
public bool Matches(FeedbackContext feedbackContext)
|
|
|
|
|
|
{
|
|
|
|
|
|
return PlayerOwner == feedbackContext.player &&
|
|
|
|
|
|
RuntimeFeedbackId == feedbackContext.runtimeFeedbackId &&
|
|
|
|
|
|
TrackIndex == feedbackContext.trackIndex &&
|
|
|
|
|
|
ClipIndex == feedbackContext.clipIndex;
|
|
|
|
|
|
}
|
2026-04-18 13:57:19 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// TimeManager 的时间缩放震动聚合器。
|
|
|
|
|
|
/// 管理多个并发时间缩放实例。
|
|
|
|
|
|
/// 当有活跃实例时,各通道取"最后激活"实例的值(last wins)。
|
|
|
|
|
|
/// 全部结束后恢复初始值。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[AddComponentMenu("SLS Utilities/Feedback Shakers/Time Scale Shaker")]
|
|
|
|
|
|
public class TimeScaleShaker : MonoBehaviour
|
|
|
|
|
|
{
|
|
|
|
|
|
private float _initGlobal;
|
|
|
|
|
|
private float _initPlayer;
|
|
|
|
|
|
private float _initEnemy;
|
|
|
|
|
|
private float _initAllied;
|
|
|
|
|
|
private float _initNonPlayer;
|
|
|
|
|
|
private bool _resolved;
|
2026-07-18 03:16:20 -04:00
|
|
|
|
private int _nextSequence;
|
2026-04-18 13:57:19 -04:00
|
|
|
|
|
|
|
|
|
|
private readonly List<TimeScaleShakeInstance> _activeShakes =
|
|
|
|
|
|
new List<TimeScaleShakeInstance>();
|
|
|
|
|
|
|
|
|
|
|
|
private void Awake()
|
|
|
|
|
|
{
|
|
|
|
|
|
_resolved = TryResolve();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnEnable()
|
|
|
|
|
|
{
|
|
|
|
|
|
TimeScaleShakeEvent.Register(OnShakeEvent);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnDisable()
|
|
|
|
|
|
{
|
|
|
|
|
|
TimeScaleShakeEvent.Unregister(OnShakeEvent);
|
|
|
|
|
|
StopAll();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void Update()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!_resolved || _activeShakes.Count == 0) return;
|
|
|
|
|
|
if (TimeManager.Instance == null) return;
|
|
|
|
|
|
|
2026-07-18 03:16:20 -04:00
|
|
|
|
ApplyActiveShakes(Time.deltaTime);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void ApplyActiveShakes(float deltaTime)
|
|
|
|
|
|
{
|
2026-04-18 13:57:19 -04:00
|
|
|
|
|
|
|
|
|
|
// 各通道取最后激活实例的值
|
|
|
|
|
|
float globalVal = _initGlobal;
|
|
|
|
|
|
float playerVal = _initPlayer;
|
|
|
|
|
|
float enemyVal = _initEnemy;
|
|
|
|
|
|
float alliedVal = _initAllied;
|
|
|
|
|
|
float nonPlayerVal = _initNonPlayer;
|
|
|
|
|
|
|
2026-07-18 03:16:20 -04:00
|
|
|
|
TimeScaleShakeInstance globalWinner = null;
|
|
|
|
|
|
TimeScaleShakeInstance playerWinner = null;
|
|
|
|
|
|
TimeScaleShakeInstance enemyWinner = null;
|
|
|
|
|
|
TimeScaleShakeInstance alliedWinner = null;
|
|
|
|
|
|
TimeScaleShakeInstance nonPlayerWinner = null;
|
2026-04-18 13:57:19 -04:00
|
|
|
|
|
|
|
|
|
|
for (int i = _activeShakes.Count - 1; i >= 0; i--)
|
|
|
|
|
|
{
|
|
|
|
|
|
TimeScaleShakeInstance shake = _activeShakes[i];
|
2026-07-18 03:16:20 -04:00
|
|
|
|
shake.Timer += deltaTime;
|
2026-04-18 13:57:19 -04:00
|
|
|
|
|
|
|
|
|
|
if (shake.IsFinished)
|
|
|
|
|
|
{
|
|
|
|
|
|
_activeShakes.RemoveAt(i);
|
2026-07-18 03:16:20 -04:00
|
|
|
|
continue;
|
2026-04-18 13:57:19 -04:00
|
|
|
|
}
|
2026-07-18 03:16:20 -04:00
|
|
|
|
|
|
|
|
|
|
if (shake.Global.active && HasHigherOrder(shake, globalWinner)) globalWinner = shake;
|
|
|
|
|
|
if (shake.Player.active && HasHigherOrder(shake, playerWinner)) playerWinner = shake;
|
|
|
|
|
|
if (shake.Enemy.active && HasHigherOrder(shake, enemyWinner)) enemyWinner = shake;
|
|
|
|
|
|
if (shake.Allied.active && HasHigherOrder(shake, alliedWinner)) alliedWinner = shake;
|
|
|
|
|
|
if (shake.NonPlayer.active && HasHigherOrder(shake, nonPlayerWinner)) nonPlayerWinner = shake;
|
2026-04-18 13:57:19 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-18 03:16:20 -04:00
|
|
|
|
if (globalWinner != null) globalVal = globalWinner.Global.Evaluate(GetNormalizedTime(globalWinner));
|
|
|
|
|
|
if (playerWinner != null) playerVal = playerWinner.Player.Evaluate(GetNormalizedTime(playerWinner));
|
|
|
|
|
|
if (enemyWinner != null) enemyVal = enemyWinner.Enemy.Evaluate(GetNormalizedTime(enemyWinner));
|
|
|
|
|
|
if (alliedWinner != null) alliedVal = alliedWinner.Allied.Evaluate(GetNormalizedTime(alliedWinner));
|
|
|
|
|
|
if (nonPlayerWinner != null) nonPlayerVal = nonPlayerWinner.NonPlayer.Evaluate(GetNormalizedTime(nonPlayerWinner));
|
|
|
|
|
|
|
|
|
|
|
|
TimeManager.Instance.globalTimeScale.Value = globalVal;
|
|
|
|
|
|
TimeManager.Instance.playerTimeScale.Value = playerVal;
|
|
|
|
|
|
TimeManager.Instance.enemyTimeScale.Value = enemyVal;
|
|
|
|
|
|
TimeManager.Instance.alliedMinionTimeScale.Value = alliedVal;
|
|
|
|
|
|
TimeManager.Instance.nonPlayerTimeScale.Value = nonPlayerVal;
|
2026-04-18 13:57:19 -04:00
|
|
|
|
|
|
|
|
|
|
if (_activeShakes.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
Restore();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnShakeEvent(
|
2026-07-18 03:16:20 -04:00
|
|
|
|
FeedbackContext feedbackContext,
|
2026-04-18 13:57:19 -04:00
|
|
|
|
float duration,
|
2026-07-18 03:16:20 -04:00
|
|
|
|
int priority,
|
2026-04-18 13:57:19 -04:00
|
|
|
|
TimeScaleChannelData global,
|
|
|
|
|
|
TimeScaleChannelData player,
|
|
|
|
|
|
TimeScaleChannelData enemy,
|
|
|
|
|
|
TimeScaleChannelData allied,
|
|
|
|
|
|
TimeScaleChannelData nonPlayer,
|
|
|
|
|
|
bool stop)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (stop)
|
|
|
|
|
|
{
|
2026-07-18 03:16:20 -04:00
|
|
|
|
_activeShakes.RemoveAll(instance => instance != null && instance.Matches(feedbackContext));
|
|
|
|
|
|
if (_activeShakes.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
Restore();
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (_resolved && TimeManager.Instance != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
ApplyActiveShakes(0f);
|
|
|
|
|
|
}
|
2026-04-18 13:57:19 -04:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!_resolved) _resolved = TryResolve();
|
|
|
|
|
|
if (!_resolved) return;
|
|
|
|
|
|
|
|
|
|
|
|
_activeShakes.Add(new TimeScaleShakeInstance(
|
2026-07-18 03:16:20 -04:00
|
|
|
|
feedbackContext, duration, priority, ++_nextSequence, global, player, enemy, allied, nonPlayer
|
2026-04-18 13:57:19 -04:00
|
|
|
|
));
|
2026-07-18 03:16:20 -04:00
|
|
|
|
|
|
|
|
|
|
ApplyActiveShakes(0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static bool HasHigherOrder(TimeScaleShakeInstance candidate, TimeScaleShakeInstance current)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (current == null) return true;
|
|
|
|
|
|
if (candidate.Priority != current.Priority) return candidate.Priority > current.Priority;
|
|
|
|
|
|
return candidate.Sequence > current.Sequence;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static float GetNormalizedTime(TimeScaleShakeInstance shake)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Mathf.Clamp01(shake.Timer / Mathf.Max(shake.Duration, 0.001f));
|
2026-04-18 13:57:19 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool TryResolve()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (TimeManager.Instance == null) return false;
|
|
|
|
|
|
|
|
|
|
|
|
_initGlobal = TimeManager.Instance.globalTimeScale.Value;
|
|
|
|
|
|
_initPlayer = TimeManager.Instance.playerTimeScale.Value;
|
|
|
|
|
|
_initEnemy = TimeManager.Instance.enemyTimeScale.Value;
|
|
|
|
|
|
_initAllied = TimeManager.Instance.alliedMinionTimeScale.Value;
|
|
|
|
|
|
_initNonPlayer = TimeManager.Instance.nonPlayerTimeScale.Value;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void Restore()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (TimeManager.Instance == null) return;
|
|
|
|
|
|
TimeManager.Instance.globalTimeScale.Value = _initGlobal;
|
|
|
|
|
|
TimeManager.Instance.playerTimeScale.Value = _initPlayer;
|
|
|
|
|
|
TimeManager.Instance.enemyTimeScale.Value = _initEnemy;
|
|
|
|
|
|
TimeManager.Instance.alliedMinionTimeScale.Value = _initAllied;
|
|
|
|
|
|
TimeManager.Instance.nonPlayerTimeScale.Value = _initNonPlayer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void StopAll()
|
|
|
|
|
|
{
|
|
|
|
|
|
_activeShakes.Clear();
|
|
|
|
|
|
Restore();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|