Files
Cielonos/Assets/Scripts/SLSUtilities/Feedback/Runtime/FeedbackPlayer.cs
2026-04-18 13:57:19 -04:00

371 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using UnityEngine;
namespace SLSUtilities.Feedback
{
/// <summary>
/// 反馈播放器状态枚举。
/// </summary>
public enum FeedbackPlayerState
{
Idle,
Playing,
Paused
}
/// <summary>
/// 运行时反馈播放器(纯 C# 类,非 MonoBehaviour管理一个 FeedbackData 的播放生命周期。
/// 由 FeedbackManager 的 Update 集中驱动,也可由外部(如 FeedbackSubcontroller手动驱动。
/// </summary>
public class FeedbackPlayer
{
private const float MIN_DURATION = 0.001f;
private FeedbackData _data;
private FeedbackPlayerState _state;
private float _currentTime;
private IFeedbackTimeProvider _timeProvider;
private Transform _ownerTransform;
private bool _isCompleted;
// 每个 Clip 的运行时状态
private enum ClipState { Pending, Active, Finished }
private ClipState[,] _clipStates; // [trackIndex, clipIndex]
private float[,] _clipElapsedTimes; // [trackIndex, clipIndex]
private bool _hasSoloTracks;
public FeedbackData Data => _data;
public FeedbackPlayerState State => _state;
public float CurrentTime => _currentTime;
public IFeedbackTimeProvider TimeProvider => _timeProvider;
public Transform OwnerTransform => _ownerTransform;
/// <summary>
/// 播放完毕事件。
/// </summary>
public event Action OnComplete;
/// <summary>
/// 被打断事件。
/// </summary>
public event Action OnInterrupt;
/// <summary>
/// 是否已播放完毕(自然结束)。
/// </summary>
public bool IsCompleted => _isCompleted;
/// <summary>
/// 是否处于活跃状态Playing 或 Paused
/// </summary>
public bool IsActive => _state == FeedbackPlayerState.Playing || _state == FeedbackPlayerState.Paused;
public FeedbackPlayer(FeedbackData data, IFeedbackTimeProvider timeProvider, Transform ownerTransform)
{
_data = data;
_timeProvider = timeProvider;
_ownerTransform = ownerTransform;
_state = FeedbackPlayerState.Idle;
_currentTime = 0f;
_isCompleted = false;
}
/// <summary>
/// 开始播放。
/// </summary>
public void Play()
{
if (_data == null)
{
Debug.LogWarning("[FeedbackPlayer] Cannot play: FeedbackData is null.");
return;
}
_currentTime = 0f;
_isCompleted = false;
_state = FeedbackPlayerState.Playing;
InitializeClipStates();
}
/// <summary>
/// 暂停播放。
/// </summary>
public void Pause()
{
if (_state == FeedbackPlayerState.Playing)
{
_state = FeedbackPlayerState.Paused;
}
}
/// <summary>
/// 恢复播放。
/// </summary>
public void Resume()
{
if (_state == FeedbackPlayerState.Paused)
{
_state = FeedbackPlayerState.Playing;
}
}
/// <summary>
/// 立即停止并复位所有已启动的 Action。
/// </summary>
public void Stop()
{
if (_state == FeedbackPlayerState.Idle) return;
InterruptAllActiveClips();
_state = FeedbackPlayerState.Idle;
OnInterrupt?.Invoke();
ClearEvents();
}
/// <summary>
/// 每帧由外部驱动调用FeedbackManager 或 Subcontroller
/// </summary>
public void Tick(float deltaTime)
{
if (_state != FeedbackPlayerState.Playing) return;
if (_data == null || _data.tracks == null) return;
float totalDuration = _data.TotalDuration;
// 处理 TotalDuration 为 0 的边界情况
if (totalDuration <= 0f)
{
_state = FeedbackPlayerState.Idle;
_isCompleted = true;
OnComplete?.Invoke();
ClearEvents();
return;
}
List<FeedbackTrack> tracks = _data.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;
// 仅当时间线游标超过总时长 且 所有 Clip 已结束时才完成。
// 这避免了因时间缩放导致 Clip 尚在播放就被提前完成的问题。
if (_currentTime >= totalDuration && AllClipsFinished())
{
_state = FeedbackPlayerState.Idle;
_isCompleted = true;
OnComplete?.Invoke();
ClearEvents();
}
}
/// <summary>
/// 清除所有事件订阅,防止完成/停止后残留引用。
/// </summary>
private void ClearEvents()
{
OnComplete = null;
OnInterrupt = null;
}
/// <summary>
/// 初始化所有 Clip 的运行时状态数组。
/// </summary>
private void InitializeClipStates()
{
List<FeedbackTrack> tracks = _data.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];
}
/// <summary>
/// 判断轨道是否应该播放:处理 Mute 和 Solo 逻辑。
/// </summary>
private bool ShouldPlayTrack(FeedbackTrack track)
{
if (track.mute) return false;
if (_hasSoloTracks && !track.solo) return false;
return true;
}
/// <summary>
/// 根据 Clip 的时间设置计算综合时间缩放系数。
/// </summary>
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 : _data.defaultTimeSettings;
if (settings == null || settings.timeScaleType == FeedbackTimeSettings.TimeScaleType.Unscaled) return 1f;
return _timeProvider.GetTimeScale(settings);
}
/// <summary>
/// 处理单个 Clip 的生命周期状态转换和回调调用。
/// </summary>
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(deltaTime, elapsed, safeDuration, timeScale, clip.timeSettings);
clip.action.OnStart(ctx);
float normalizedTime = Mathf.Clamp01(elapsed / safeDuration);
clip.action.OnUpdate(CreateContext(deltaTime, elapsed, safeDuration, timeScale, clip.timeSettings), normalizedTime);
}
break;
case ClipState.Active:
// 如果启用动态时间缩放,每帧重新获取当前的时间缩放
FeedbackTimeSettings settings = clip.overrideTimeSettings ? clip.timeSettings : _data.defaultTimeSettings;
float currentTimeScale = timeScale;
if (settings.applyDynamicTimeScale)
{
currentTimeScale = ComputeClipTimeScale(clip);
}
// 使用调整后的deltaTime进行累加
float adjustedDeltaTime = deltaTime * currentTimeScale;
elapsed += adjustedDeltaTime;
if (elapsed >= safeDuration)
{
elapsed = safeDuration;
clipState = ClipState.Finished;
FeedbackContext ctx = CreateContext(deltaTime, elapsed, safeDuration, currentTimeScale, clip.timeSettings);
clip.action.OnUpdate(ctx, 1f);
clip.action.OnEnd(ctx);
}
else
{
float normalizedTime = Mathf.Clamp01(elapsed / safeDuration);
FeedbackContext ctx = CreateContext(deltaTime, elapsed, safeDuration, currentTimeScale, clip.timeSettings);
clip.action.OnUpdate(ctx, normalizedTime);
}
break;
case ClipState.Finished:
break;
}
}
/// <summary>
/// 打断所有已激活的 Clip调用其 OnInterrupt 进行复位。
/// </summary>
private void InterruptAllActiveClips()
{
if (_data?.tracks == null || _clipStates == null) return;
List<FeedbackTrack> tracks = _data.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) continue;
float elapsed = _clipElapsedTimes[trackIdx, clipIdx];
float safeDuration = Mathf.Max(clip.duration, MIN_DURATION);
FeedbackContext ctx = CreateContext(0f, elapsed, safeDuration, 1f, clip.timeSettings);
clip.action.OnInterrupt(ctx);
}
_clipStates[trackIdx, clipIdx] = ClipState.Finished;
}
}
}
/// <summary>
/// 检查所有应播放的 Clip 是否都已完成。
/// 用于时间缩放场景下,避免 Clip 尚在播放就提前完成整个 Feedback。
/// </summary>
private bool AllClipsFinished()
{
if (_clipStates == null) return true;
List<FeedbackTrack> tracks = _data.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;
}
/// <summary>
/// 创建 FeedbackContext 实例。
/// </summary>
private FeedbackContext CreateContext(float deltaTime, float elapsedTime,
float duration, float timeScale, FeedbackTimeSettings timeSettings)
{
return new FeedbackContext
{
player = this,
owner = _ownerTransform,
deltaTime = deltaTime,
elapsedTime = elapsedTime,
duration = duration,
timeScale = timeScale,
timeSettings = timeSettings
};
}
}
}