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