using System; using System.Collections.Generic; using UnityEngine; namespace SLSUtilities.Feedback { public enum FeedbackPlayerState { Idle, Playing, Paused } /// /// 运行时反馈播放器。播放器只读取 RuntimeFeedback,TemplateData 仅用于来源识别和停止匹配。 /// public class FeedbackPlayer { private const float MIN_DURATION = 0.001f; private readonly RuntimeFeedback _runtimeFeedback; private readonly IFeedbackTimeProvider _timeProvider; private readonly Transform _ownerTransform; private FeedbackPlayerState _state; private float _currentTime; private bool _isCompleted; private enum ClipState { Pending, Active, Finished } private ClipState[,] _clipStates; private float[,] _clipElapsedTimes; private bool _hasSoloTracks; public FeedbackData Data => TemplateData; public FeedbackData TemplateData => _runtimeFeedback?.templateData; public RuntimeFeedback RuntimeData => _runtimeFeedback; public FeedbackPlayerState State => _state; public float CurrentTime => _currentTime; public IFeedbackTimeProvider TimeProvider => _timeProvider; public Transform OwnerTransform => _ownerTransform; public bool IsCompleted => _isCompleted; public bool IsActive => _state == FeedbackPlayerState.Playing || _state == FeedbackPlayerState.Paused; public event Action OnComplete; public event Action OnInterrupt; public FeedbackPlayer(FeedbackData data, IFeedbackTimeProvider timeProvider, Transform ownerTransform) : this(data?.CreateRuntimeFeedback(), timeProvider, ownerTransform) { } public FeedbackPlayer(RuntimeFeedback runtimeFeedback, IFeedbackTimeProvider timeProvider, Transform ownerTransform) { _runtimeFeedback = runtimeFeedback; _timeProvider = timeProvider ?? DefaultFeedbackTimeProvider.Instance; _ownerTransform = ownerTransform; _state = FeedbackPlayerState.Idle; _currentTime = 0f; _isCompleted = false; } public void Play() { if (_runtimeFeedback == null) { Debug.LogWarning("[FeedbackPlayer] Cannot play: RuntimeFeedback is null."); return; } _currentTime = 0f; _isCompleted = false; _state = FeedbackPlayerState.Playing; InitializeClipStates(); } public void Pause() { if (_state == FeedbackPlayerState.Playing) { _state = FeedbackPlayerState.Paused; } } public void Resume() { if (_state == FeedbackPlayerState.Paused) { _state = FeedbackPlayerState.Playing; } } public void Stop() { if (_state == FeedbackPlayerState.Idle) return; InterruptAllActiveClips(); _state = FeedbackPlayerState.Idle; OnInterrupt?.Invoke(); ClearEvents(); } public void Tick(float deltaTime) { if (_state != FeedbackPlayerState.Playing) return; if (_runtimeFeedback?.tracks == null) return; float totalDuration = _runtimeFeedback.TotalDuration; if (totalDuration <= 0f) { _state = FeedbackPlayerState.Idle; _isCompleted = true; OnComplete?.Invoke(); ClearEvents(); return; } List tracks = _runtimeFeedback.tracks; for (int trackIdx = 0; trackIdx < tracks.Count; trackIdx++) { FeedbackTrack track = tracks[trackIdx]; if (!ShouldPlayTrack(track)) continue; for (int clipIdx = 0; clipIdx < track.clips.Count; clipIdx++) { FeedbackClip clip = track.clips[clipIdx]; if (clip?.action == null) continue; float clipTimeScale = ComputeClipTimeScale(clip); float clipDeltaTime = deltaTime * clipTimeScale; ProcessClip(trackIdx, clipIdx, clip, clipDeltaTime, clipTimeScale); } } _currentTime += deltaTime; if (_currentTime >= totalDuration && AllClipsFinished()) { _state = FeedbackPlayerState.Idle; _isCompleted = true; OnComplete?.Invoke(); ClearEvents(); } } private void ClearEvents() { OnComplete = null; OnInterrupt = null; } private void InitializeClipStates() { List tracks = _runtimeFeedback.tracks; if (tracks == null || tracks.Count == 0) return; int maxClips = 0; _hasSoloTracks = false; for (int i = 0; i < tracks.Count; i++) { if (tracks[i].clips != null && tracks[i].clips.Count > maxClips) maxClips = tracks[i].clips.Count; if (tracks[i].solo) _hasSoloTracks = true; } _clipStates = new ClipState[tracks.Count, maxClips]; _clipElapsedTimes = new float[tracks.Count, maxClips]; } private bool ShouldPlayTrack(FeedbackTrack track) { if (track == null) return false; if (track.mute) return false; if (_hasSoloTracks && !track.solo) return false; return true; } private float ComputeClipTimeScale(FeedbackClip clip) { if (_timeProvider == null) return 1f; if (clip?.action == null || clip.action.IgnoreTimeScale) return 1f; FeedbackTimeSettings settings = clip.overrideTimeSettings ? clip.timeSettings : _runtimeFeedback.defaultTimeSettings; if (settings == null || settings.timeScaleType == FeedbackTimeSettings.TimeScaleType.Unscaled) return 1f; return _timeProvider.GetTimeScale(settings); } private void ProcessClip(int trackIdx, int clipIdx, FeedbackClip clip, float deltaTime, float timeScale) { ref ClipState clipState = ref _clipStates[trackIdx, clipIdx]; ref float elapsed = ref _clipElapsedTimes[trackIdx, clipIdx]; float safeDuration = Mathf.Max(clip.duration, MIN_DURATION); switch (clipState) { case ClipState.Pending: if (_currentTime >= clip.startTime) { clipState = ClipState.Active; elapsed = _currentTime - clip.startTime; FeedbackContext ctx = CreateContext(trackIdx, clipIdx, deltaTime, elapsed, safeDuration, timeScale, GetTimeSettings(clip)); clip.action.OnStart(ctx); float normalizedTime = Mathf.Clamp01(elapsed / safeDuration); clip.action.OnUpdate(ctx, normalizedTime); } break; case ClipState.Active: FeedbackTimeSettings settings = GetTimeSettings(clip); float currentTimeScale = timeScale; if (settings != null && settings.applyDynamicTimeScale) { currentTimeScale = ComputeClipTimeScale(clip); } float adjustedDeltaTime = deltaTime * currentTimeScale; elapsed += adjustedDeltaTime; if (elapsed >= safeDuration) { elapsed = safeDuration; clipState = ClipState.Finished; FeedbackContext ctx = CreateContext(trackIdx, clipIdx, deltaTime, elapsed, safeDuration, currentTimeScale, settings); clip.action.OnUpdate(ctx, 1f); clip.action.OnEnd(ctx); } else { float normalizedTime = Mathf.Clamp01(elapsed / safeDuration); FeedbackContext ctx = CreateContext(trackIdx, clipIdx, deltaTime, elapsed, safeDuration, currentTimeScale, settings); clip.action.OnUpdate(ctx, normalizedTime); } break; } } private FeedbackTimeSettings GetTimeSettings(FeedbackClip clip) { return clip.overrideTimeSettings ? clip.timeSettings : _runtimeFeedback.defaultTimeSettings; } private void InterruptAllActiveClips() { if (_runtimeFeedback?.tracks == null || _clipStates == null) return; List tracks = _runtimeFeedback.tracks; for (int trackIdx = 0; trackIdx < tracks.Count; trackIdx++) { if (tracks[trackIdx].clips == null) continue; for (int clipIdx = 0; clipIdx < tracks[trackIdx].clips.Count; clipIdx++) { if (_clipStates[trackIdx, clipIdx] == ClipState.Active) { FeedbackClip clip = tracks[trackIdx].clips[clipIdx]; if (clip?.action != null) { float elapsed = _clipElapsedTimes[trackIdx, clipIdx]; float safeDuration = Mathf.Max(clip.duration, MIN_DURATION); FeedbackContext ctx = CreateContext(trackIdx, clipIdx, 0f, elapsed, safeDuration, 1f, GetTimeSettings(clip)); clip.action.OnInterrupt(ctx); } } _clipStates[trackIdx, clipIdx] = ClipState.Finished; } } } private bool AllClipsFinished() { if (_clipStates == null) return true; List tracks = _runtimeFeedback.tracks; for (int trackIdx = 0; trackIdx < tracks.Count; trackIdx++) { FeedbackTrack track = tracks[trackIdx]; if (!ShouldPlayTrack(track)) continue; if (track.clips == null) continue; for (int clipIdx = 0; clipIdx < track.clips.Count; clipIdx++) { if (_clipStates[trackIdx, clipIdx] == ClipState.Active) return false; } } return true; } private FeedbackContext CreateContext(int trackIdx, int clipIdx, float deltaTime, float elapsedTime, float duration, float timeScale, FeedbackTimeSettings timeSettings) { return new FeedbackContext { player = this, runtimeFeedbackId = _runtimeFeedback.runtimeId, trackIndex = trackIdx, clipIndex = clipIdx, owner = _ownerTransform, deltaTime = deltaTime, elapsedTime = elapsedTime, duration = duration, timeScale = timeScale, timeSettings = timeSettings }; } } }