using System; using UnityEngine; using UnityEngine.UI; using UnityEngine.Rendering; using UnityEngine.Playables; using UnityEngine.Timeline; namespace AibisDream.FixSystem { [RequireComponent(typeof(Slider))] public class MemorySlider : MonoBehaviour { [SerializeField] private float defaultValue = 0f; [SerializeField] private float sliderMaxValue = 6f; [SerializeField] private float lockThreshold = 0.08f; [SerializeField] private float nearThreshold = 0.24f; [SerializeField, Range(0f, 1f)] private float unresolvedVisualFloor = 0.12f; [SerializeField] private TimelineAsset openAsset; [SerializeField] private TimelineAsset closeAsset; private Slider slider; private PlayableDirector playableDirector; private Volume _volume; private bool driveVolume = true; /// 视觉 Lock/Unlock(Timeline);true = 收起未开,false = 已打开。 private bool isLocked; public float TargetValue { get; set; } public float StartValue => defaultValue; public float MaxValue => sliderMaxValue; public bool IsFound { get; private set; } public event Action OnFound; public event Action OnLost; public event Action OnTuningChanged; public void Init(Volume volume) { slider = GetComponent(); playableDirector = GetComponent(); _volume = volume; driveVolume = true; IsFound = false; slider.onValueChanged.RemoveAllListeners(); slider.onValueChanged.AddListener(HandleValueChanged); slider.maxValue = sliderMaxValue; slider.value = defaultValue; UpdateTuningOutput(slider.value); isLocked = true; SetInteractable(false); } /// 仅控制是否可拖动,与 / 无关。 public void SetInteractable(bool interactable) => slider.interactable = interactable; public void SetVolumeOutputEnabled(bool enabled) { driveVolume = enabled; if (!driveVolume && _volume != null) _volume.weight = 0f; } /// /// 将拉杆视觉复位到默认位置并清零调谐输出(不播 Timeline、不触发搜索判定)。 /// 用于记忆播放结束后回到待机界面。 /// public void ResetVisualToDefault() { IsFound = false; if (slider == null) slider = GetComponent(); slider.SetValueWithoutNotify(defaultValue); if (_volume != null && driveVolume) _volume.weight = 0f; OnTuningChanged?.Invoke(0f); } /// /// 新一轮搜索前复位滑条与 IsFound(不播 Timeline、不改变视觉锁状态)。 /// 需先设置好 。 /// public void PrepareForSearch() { slider.value = defaultValue; HandleValueChanged(slider.value); } /// 仅播放打开 Timeline(若有),不碰 interactable。 public void UnlockSlider(Action onComplete = null) { if (!isLocked) { onComplete?.Invoke(); return; } PlayTimeline(openAsset, () => { isLocked = false; onComplete?.Invoke(); }); } /// 仅播放收起 Timeline(若有),不碰 interactable。 public void LockSlider(Action onComplete = null) { if (isLocked) { onComplete?.Invoke(); return; } slider.value = 0f; UpdateTuningOutput(0f); PlayTimeline(closeAsset, () => { isLocked = true; onComplete?.Invoke(); }); } private void HandleValueChanged(float value) { float t = UpdateTuningOutput(value); bool wasFound = IsFound; IsFound = t < lockThreshold; if (IsFound && !wasFound) OnFound?.Invoke(); else if (!IsFound && wasFound) OnLost?.Invoke(); } private float UpdateTuningOutput(float value) { float t = GetNormalizedDistance(value); float visualStrength = GetVisualStrength(t); if (_volume != null && driveVolume) _volume.weight = Mathf.Lerp(0, 1, visualStrength); OnTuningChanged?.Invoke(visualStrength); return t; } private float GetNormalizedDistance(float value) { float distance = Mathf.Abs(value - TargetValue); float maxDistance = Mathf.Max( Mathf.Abs(slider.maxValue - TargetValue), Mathf.Abs(slider.minValue - TargetValue) ); return Mathf.Clamp01(distance / Mathf.Max(0.0001f, maxDistance)); } private float GetVisualStrength(float t) { float lockBand = Mathf.Clamp01(lockThreshold); float nearBand = Mathf.Clamp(Mathf.Max(nearThreshold, lockBand + 0.001f), lockBand + 0.001f, 1f); if (t <= lockBand) return 0f; if (t < nearBand) { float nearProgress = Mathf.InverseLerp(lockBand, nearBand, t); float easedProgress = Mathf.SmoothStep(0f, 1f, nearProgress); return Mathf.Lerp(unresolvedVisualFloor, nearBand, easedProgress); } return t; } private void PlayTimeline(TimelineAsset asset, Action onComplete) { if (asset == null || playableDirector == null) { onComplete?.Invoke(); return; } playableDirector.Stop(); playableDirector.playableAsset = asset; playableDirector.time = 0; playableDirector.extrapolationMode = DirectorWrapMode.None; Action callback = null; callback = pd => { playableDirector.stopped -= callback; onComplete?.Invoke(); }; playableDirector.stopped += callback; playableDirector.Play(); } } }