更新
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
using System.Collections.Generic;
|
||||
using SLSUtilities.General;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSUtilities.Feedback
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局 Feedback 播放管理器(Singleton)。
|
||||
/// 集中驱动所有注册的 FeedbackPlayer,提供 Play / Stop 快捷 API。
|
||||
/// 适用于不绑定特定角色时间缩放的"全局反馈"(如后处理效果、UI 反馈等)。
|
||||
/// 需要角色级时间缩放的反馈仍由 FeedbackSubcontroller 手动驱动。
|
||||
/// </summary>
|
||||
public class FeedbackManager : Singleton<FeedbackManager>
|
||||
{
|
||||
private const int INITIAL_CAPACITY = 64;
|
||||
|
||||
private readonly List<FeedbackPlayer> _activePlayers = new List<FeedbackPlayer>(INITIAL_CAPACITY);
|
||||
|
||||
/// <summary>
|
||||
/// 当前活跃的 Player 数量。
|
||||
/// </summary>
|
||||
public int ActiveCount => _activePlayers.Count;
|
||||
|
||||
/// <summary>
|
||||
/// 以全局方式播放一个 FeedbackData,不绑定任何 owner 或 timeProvider。
|
||||
/// </summary>
|
||||
public FeedbackPlayer Play(FeedbackData data)
|
||||
{
|
||||
return Play(data, null, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 播放一个 FeedbackData,指定时间提供者和 owner。
|
||||
/// </summary>
|
||||
public FeedbackPlayer Play(FeedbackData data, IFeedbackTimeProvider timeProvider, Transform owner)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
Debug.LogWarning("[FeedbackManager] Cannot play: FeedbackData is null.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var player = new FeedbackPlayer(data, timeProvider, owner);
|
||||
player.Play();
|
||||
_activePlayers.Add(player);
|
||||
return player;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册一个已有的 FeedbackPlayer 由管理器驱动。
|
||||
/// 适用于外部创建 Player 后需要交给管理器集中管理的场景。
|
||||
/// </summary>
|
||||
public void Register(FeedbackPlayer player)
|
||||
{
|
||||
if (player == null || _activePlayers.Contains(player)) return;
|
||||
_activePlayers.Add(player);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止并移除指定的 FeedbackPlayer。
|
||||
/// </summary>
|
||||
public void Stop(FeedbackPlayer player)
|
||||
{
|
||||
if (player == null) return;
|
||||
|
||||
player.Stop();
|
||||
_activePlayers.Remove(player);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止所有活跃的 FeedbackPlayer。
|
||||
/// </summary>
|
||||
public void StopAll()
|
||||
{
|
||||
for (int i = _activePlayers.Count - 1; i >= 0; i--)
|
||||
{
|
||||
_activePlayers[i].Stop();
|
||||
}
|
||||
_activePlayers.Clear();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
float dt = Time.unscaledDeltaTime;
|
||||
|
||||
for (int i = _activePlayers.Count - 1; i >= 0; i--)
|
||||
{
|
||||
FeedbackPlayer player = _activePlayers[i];
|
||||
player.Tick(dt);
|
||||
|
||||
if (player.IsCompleted || !player.IsActive)
|
||||
{
|
||||
_activePlayers.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5afafd522603d740a166204893eaad1
|
||||
329
Assets/Scripts/SLSUtilities/Feedback/Runtime/FeedbackPlayer.cs
Normal file
329
Assets/Scripts/SLSUtilities/Feedback/Runtime/FeedbackPlayer.cs
Normal file
@@ -0,0 +1,329 @@
|
||||
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 unscaledDeltaTime)
|
||||
{
|
||||
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 clipDeltaTime = ComputeClipDeltaTime(clip, unscaledDeltaTime);
|
||||
ProcessClip(trackIdx, clipIdx, clip, clipDeltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
_currentTime += unscaledDeltaTime;
|
||||
|
||||
if (_currentTime >= totalDuration)
|
||||
{
|
||||
_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 的时间设置计算实际 deltaTime。
|
||||
/// </summary>
|
||||
private float ComputeClipDeltaTime(FeedbackClip clip, float unscaledDeltaTime)
|
||||
{
|
||||
if (_timeProvider == null) return unscaledDeltaTime;
|
||||
|
||||
FeedbackTimeSettings settings = clip.overrideTimeSettings
|
||||
? clip.timeSettings
|
||||
: _data.defaultTimeSettings;
|
||||
|
||||
if (settings == null || !settings.useTimeScale) return unscaledDeltaTime;
|
||||
|
||||
return _timeProvider.GetDeltaTime(settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理单个 Clip 的生命周期状态转换和回调调用。
|
||||
/// </summary>
|
||||
private void ProcessClip(int trackIdx, int clipIdx, FeedbackClip clip, float deltaTime)
|
||||
{
|
||||
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);
|
||||
clip.action.OnStart(ctx);
|
||||
|
||||
float normalizedTime = Mathf.Clamp01(elapsed / safeDuration);
|
||||
clip.action.OnUpdate(CreateContext(deltaTime, elapsed, safeDuration), normalizedTime);
|
||||
}
|
||||
break;
|
||||
|
||||
case ClipState.Active:
|
||||
elapsed += deltaTime;
|
||||
|
||||
if (elapsed >= safeDuration)
|
||||
{
|
||||
elapsed = safeDuration;
|
||||
clipState = ClipState.Finished;
|
||||
|
||||
FeedbackContext ctx = CreateContext(deltaTime, elapsed, safeDuration);
|
||||
clip.action.OnUpdate(ctx, 1f);
|
||||
clip.action.OnEnd(ctx);
|
||||
}
|
||||
else
|
||||
{
|
||||
float normalizedTime = Mathf.Clamp01(elapsed / safeDuration);
|
||||
FeedbackContext ctx = CreateContext(deltaTime, elapsed, safeDuration);
|
||||
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);
|
||||
clip.action.OnInterrupt(ctx);
|
||||
}
|
||||
|
||||
_clipStates[trackIdx, clipIdx] = ClipState.Finished;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 FeedbackContext 实例。
|
||||
/// </summary>
|
||||
private FeedbackContext CreateContext(float deltaTime, float elapsedTime, float duration)
|
||||
{
|
||||
return new FeedbackContext
|
||||
{
|
||||
player = this,
|
||||
owner = _ownerTransform,
|
||||
deltaTime = deltaTime,
|
||||
elapsedTime = elapsedTime,
|
||||
duration = duration
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f6ececc7f732a54092e62154febce90
|
||||
Reference in New Issue
Block a user