新Feedback系统

This commit is contained in:
SoulliesOfficial
2026-04-12 02:11:15 -04:00
parent f26f9fd374
commit 41140a2017
171 changed files with 296190 additions and 219527 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1ad1a9604de9797448b5779a5a190c32
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,63 @@
using System;
using Sirenix.OdinInspector;
using UnityEngine;
namespace SLSUtilities.Feedback
{
/// <summary>
/// 通用的"按曲线震动数值"基类,提供曲线采样、初始值记录/复位等通用逻辑。
/// RadialBlur、ChromaticAberration、Vignette 等后处理效果均继承此类。
/// </summary>
[Serializable]
public abstract class CurveShakeAction : FeedbackActionBase
{
/// <summary>
/// 震动曲线X 轴为归一化时间 [0,1]Y 轴为震动强度 [0,1]。
/// </summary>
[Title("Curve Shake")]
[LabelText("Shake Curve")]
public AnimationCurve shakeCurve = new AnimationCurve(
new Keyframe(0f, 0f),
new Keyframe(0.5f, 1f),
new Keyframe(1f, 0f)
);
/// <summary>
/// 曲线值 0 对应的实际数值。
/// </summary>
[LabelText("Remap Min")]
public float remapMin;
/// <summary>
/// 曲线值 1 对应的实际数值。
/// </summary>
[LabelText("Remap Max")]
public float remapMax = 1f;
/// <summary>
/// 是否在初始值上叠加(而非替换)。
/// </summary>
[LabelText("Relative to Initial")]
public bool relativeToInitial;
/// <summary>
/// 根据归一化时间采样曲线并映射到实际值范围。
/// 如果 relativeToInitial 为 true结果会叠加在 initialValue 上。
/// </summary>
/// <param name="normalizedTime">归一化时间 [0,1]</param>
/// <param name="initialValue">初始值OnStart 时记录)</param>
/// <returns>映射后的最终数值</returns>
protected float EvaluateShake(float normalizedTime, float initialValue)
{
float curveValue = shakeCurve.Evaluate(normalizedTime);
float remappedValue = Mathf.LerpUnclamped(remapMin, remapMax, curveValue);
if (relativeToInitial)
{
return initialValue + remappedValue;
}
return remappedValue;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 26b145321a43fe44d899db3f2178cb0e

View File

@@ -0,0 +1,85 @@
using System;
using Sirenix.OdinInspector;
using UnityEngine;
namespace SLSUtilities.Feedback
{
/// <summary>
/// 反馈播放上下文,传递给每个 FeedbackAction 的生命周期回调。
/// </summary>
public struct FeedbackContext
{
/// <summary>
/// 当前播放器实例。
/// </summary>
public FeedbackPlayer player;
/// <summary>
/// 触发者的 Transform可选
/// </summary>
public Transform owner;
/// <summary>
/// 当前帧经过时间缩放处理后的 deltaTime。
/// </summary>
public float deltaTime;
/// <summary>
/// Clip 已播放时间(秒)。
/// </summary>
public float elapsedTime;
/// <summary>
/// Clip 总时长(秒)。
/// </summary>
public float duration;
}
/// <summary>
/// 所有反馈动作的抽象基类,定义生命周期回调。
/// 使用 Odin 的序列化路径实现多态序列化SerializedScriptableObject
/// Odin 会自动为此抽象类型的字段显示多态类型选择器。
/// </summary>
[Serializable]
public abstract class FeedbackActionBase
{
/// <summary>
/// Inspector 中显示的名称。
/// </summary>
public virtual string DisplayName => GetType().Name;
/// <summary>
/// 初始化FeedbackPlayer 开始播放此 Clip 时调用。
/// </summary>
public virtual void OnStart(FeedbackContext context) { }
/// <summary>
/// 每帧更新normalizedTime 为 Clip 内的归一化进度 [0,1]。
/// </summary>
public virtual void OnUpdate(FeedbackContext context, float normalizedTime) { }
/// <summary>
/// Clip 自然结束时调用。
/// </summary>
public virtual void OnEnd(FeedbackContext context) { }
/// <summary>
/// 被打断时调用,负责立即复位到初始状态。
/// </summary>
public virtual void OnInterrupt(FeedbackContext context) { }
/// <summary>
/// 用于验证配置是否正确Editor 环境)。
/// </summary>
public virtual bool Validate(out string error)
{
error = null;
return true;
}
/// <summary>
/// 用于 Editor 预览Runtime 也可用)。
/// </summary>
public virtual void Preview() { }
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: eea5c9e5f4713f2438de3ca97bf00cba

View File

@@ -0,0 +1,49 @@
using System;
using Sirenix.OdinInspector;
using UnityEngine;
namespace SLSUtilities.Feedback
{
/// <summary>
/// 轨道上的一个时间片段,包含一个 FeedbackAction 及其时间参数。
/// 曲线控制完全下放到具体 Action 中Clip 只负责时间调度。
/// </summary>
[Serializable]
public class FeedbackClip
{
/// <summary>
/// 片段开始时间(秒)。
/// </summary>
[MinValue(0f)]
public float startTime;
/// <summary>
/// 片段持续时间(秒)。
/// </summary>
[MinValue(0.01f)]
public float duration = 0.1f;
/// <summary>
/// 片段结束时间(秒)。
/// </summary>
public float EndTime => startTime + duration;
/// <summary>
/// 是否覆盖 FeedbackData 的时间设置。
/// </summary>
[Title("Time Override")]
public bool overrideTimeSettings;
/// <summary>
/// 覆盖用的时间设置,仅在 overrideTimeSettings 为 true 时生效。
/// </summary>
[ShowIf("overrideTimeSettings")]
public FeedbackTimeSettings timeSettings = new FeedbackTimeSettings();
/// <summary>
/// 具体反馈动作Odin 自动显示多态类型选择器。
/// </summary>
[Title("Action"), SerializeReference]
public FeedbackActionBase action;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 05ae22a20d69f0e4fa3009d721f2331d

View File

@@ -0,0 +1,70 @@
using System.Collections.Generic;
using System.Linq;
using Sirenix.OdinInspector;
using UnityEngine;
namespace SLSUtilities.Feedback
{
/// <summary>
/// 单个反馈序列的完整数据定义,是 Feedback 系统的核心 ScriptableObject。
/// 包含多条轨道Track每条轨道包含按时间排列的片段Clip
/// </summary>
[CreateAssetMenu(fileName = "NewFeedbackData", menuName = "SLS/Feedback/FeedbackData")]
public class FeedbackData : SerializedScriptableObject
{
/// <summary>
/// 父级集合引用,由 FeedbackDataCollection 自动维护。
/// </summary>
[ReadOnly, ShowInInspector]
public FeedbackDataCollection parentCollection;
/// <summary>
/// 用于字典索引的名称,在 FeedbackDataCollection 中按此名称查找。
/// </summary>
[Title("Editor Settings")]
public string feedbackName;
/// <summary>
/// 全局默认的时间设置。Clip 可选择覆盖此设置。
/// </summary>
[Title("Time Settings (Default)")]
public FeedbackTimeSettings defaultTimeSettings = new FeedbackTimeSettings();
/// <summary>
/// 反馈轨道列表,多条轨道天然并行播放。
/// </summary>
[Title("Feedback Tracks")]
[ListDrawerSettings(ShowFoldout = true, ListElementLabelName = "trackName")]
public List<FeedbackTrack> tracks = new List<FeedbackTrack>();
/// <summary>
/// 所有轨道的最大时长。
/// </summary>
public float TotalDuration => tracks.Count > 0 ? tracks.Max(t => t.TotalDuration) : 0f;
/// <summary>
/// 运行时预览:通过 FeedbackManager 播放此反馈。
/// 仅在 Play 模式下可用。
/// </summary>
[Button("Preview", ButtonSizes.Medium)]
[EnableIf("@UnityEngine.Application.isPlaying")]
public void Preview()
{
if (!Application.isPlaying)
{
Debug.LogWarning("[FeedbackData] Preview is only available in Play mode.");
return;
}
if (FeedbackManager.Instance == null)
{
Debug.LogWarning("[FeedbackData] Preview failed: FeedbackManager not found in scene. " +
"Add a GameObject with FeedbackManager component.");
return;
}
FeedbackPlayer player = FeedbackManager.Instance.Play(this);
Debug.Log($"[FeedbackData] Previewing '{feedbackName}' (Duration: {TotalDuration:F2}s)");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 87e69f21423d3c746ae55ea47c545ba6

View File

@@ -0,0 +1,115 @@
using System.Collections.Generic;
using System.Linq;
using Sirenix.OdinInspector;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace SLSUtilities.Feedback
{
/// <summary>
/// FeedbackData 的容器,供策划在武器 Prefab 上配置多个反馈序列。
/// 支持按 feedbackName 索引查找。
/// </summary>
[CreateAssetMenu(fileName = "FeedbackDataCollection", menuName = "SLS/Feedback/FeedbackDataCollection")]
public class FeedbackDataCollection : SerializedScriptableObject
{
/// <summary>
/// 反馈数据列表。
/// </summary>
[Searchable]
[ListDrawerSettings(ShowFoldout = true, CustomRemoveIndexFunction = "OnRemoveItem")]
[OnValueChanged("OnListChanged", true)]
public List<FeedbackData> feedbackDataList = new List<FeedbackData>();
/// <summary>
/// 按 feedbackName 索引查找 FeedbackData。
/// </summary>
public FeedbackData this[string name]
{
get
{
if (feedbackDataList == null || string.IsNullOrEmpty(name)) return null;
return feedbackDataList.FirstOrDefault(d => d != null && d.feedbackName == name);
}
}
/// <summary>
/// 尝试按名称获取 FeedbackData。
/// </summary>
public bool TryGet(string name, out FeedbackData data)
{
data = this[name];
return data != null;
}
/// <summary>
/// 当列表发生任何变化(添加、拖入、重新排序)时调用,维护父子引用。
/// </summary>
private void OnListChanged()
{
if (feedbackDataList == null) return;
foreach (var data in feedbackDataList)
{
if (data != null && data.parentCollection != this)
{
data.parentCollection = this;
#if UNITY_EDITOR
EditorUtility.SetDirty(data);
#endif
}
}
}
/// <summary>
/// 自定义删除逻辑:先解绑父级引用,再从列表中移除。
/// </summary>
private void OnRemoveItem(int index)
{
if (index < 0 || index >= feedbackDataList.Count) return;
FeedbackData dataToRemove = feedbackDataList[index];
if (dataToRemove != null)
{
if (dataToRemove.parentCollection == this)
{
dataToRemove.parentCollection = null;
#if UNITY_EDITOR
EditorUtility.SetDirty(dataToRemove);
#endif
}
}
feedbackDataList.RemoveAt(index);
#if UNITY_EDITOR
EditorUtility.SetDirty(this);
#endif
}
/// <summary>
/// 运行时按名称预览指定的 FeedbackData。
/// </summary>
[Button("Preview by Name")]
[EnableIf("@UnityEngine.Application.isPlaying")]
public void PreviewByName(string name)
{
if (!Application.isPlaying)
{
Debug.LogWarning("[FeedbackDataCollection] Preview is only available in Play mode.");
return;
}
if (TryGet(name, out FeedbackData data))
{
data.Preview();
}
else
{
Debug.LogWarning($"[FeedbackDataCollection] FeedbackData with name '{name}' not found.");
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 693e7631325261949bb90a5df6789240

View File

@@ -0,0 +1,34 @@
using System;
using UnityEngine;
namespace SLSUtilities.Feedback
{
/// <summary>
/// 时间设置数据结构,控制 Feedback 的时间缩放来源。
/// 同时存在于 FeedbackData全局默认和 FeedbackClip单元覆盖两个层级。
/// Clip 级设置如果 useTimeScale = true 则覆盖 Data 级设置。
/// </summary>
[Serializable]
public class FeedbackTimeSettings
{
/// <summary>
/// 是否使用时间缩放。默认 false 表示不受任何时间缩放影响。
/// </summary>
public bool useTimeScale;
/// <summary>
/// 受 TimeManager.globalTimeScale 影响。
/// </summary>
public bool affectedByGlobalTimeScale;
/// <summary>
/// 受 TimeManager 的分组时间影响player/enemy 等)。
/// </summary>
public bool affectedByGroupTimeScale;
/// <summary>
/// 受角色本地 localTimeScale 影响。
/// </summary>
public bool affectedByLocalTimeScale;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b3b96365b54adee4887c91d228fcf159

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Sirenix.OdinInspector;
namespace SLSUtilities.Feedback
{
/// <summary>
/// 一条反馈轨道,包含按时间排列的 Clip 序列。
/// 多个 Track 天然并行播放Track 内的 Clip 按时间顺序排列,不重叠。
/// </summary>
[Serializable]
public class FeedbackTrack
{
/// <summary>
/// 轨道名称,用于调试和 Inspector 显示。
/// </summary>
[LabelText("Track Name")]
public string trackName = "New Track";
/// <summary>
/// 静音此轨道,播放时跳过。
/// </summary>
[HorizontalGroup("Flags", Width = 60)]
[LabelWidth(40)]
public bool mute;
/// <summary>
/// 独奏此轨道,仅播放标记为 Solo 的轨道。
/// </summary>
[HorizontalGroup("Flags", Width = 50)]
[LabelWidth(35)]
public bool solo;
/// <summary>
/// 轨道上的片段列表。
/// </summary>
[ListDrawerSettings(ShowFoldout = true)]
public List<FeedbackClip> clips = new List<FeedbackClip>();
/// <summary>
/// 该轨道的总时长,取所有 Clip 中最大的 EndTime。
/// </summary>
public float TotalDuration => clips.Count > 0 ? clips.Max(c => c.EndTime) : 0f;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8010f4b35c62b4c44b20ee97778cb33a

View File

@@ -0,0 +1,29 @@
namespace SLSUtilities.Feedback
{
/// <summary>
/// 时间提供者接口,框架层不依赖具体的 TimeManager 实现。
/// 游戏层通过 SelfTimeSubmodule 或适配器类实现此接口,注入到 FeedbackPlayer 中。
/// </summary>
public interface IFeedbackTimeProvider
{
/// <summary>
/// 全局时间缩放值。
/// </summary>
float GlobalTimeScale { get; }
/// <summary>
/// 分组时间缩放值,由具体实现根据 Fraction 返回对应值。
/// </summary>
float GroupTimeScale { get; }
/// <summary>
/// 角色本地时间缩放值。
/// </summary>
float LocalTimeScale { get; }
/// <summary>
/// 根据时间设置计算实际 deltaTime。
/// </summary>
float GetDeltaTime(FeedbackTimeSettings settings);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8c53b0168a5ad5f4daae818aab208d20

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e04ed28b2e1c6ef4a90b9574d94efc60
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c442c21f1abe1f943bc6f68f0a4bb394
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b5afafd522603d740a166204893eaad1

View 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
};
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6f6ececc7f732a54092e62154febce90