新Feedback系统
This commit is contained in:
8
Assets/Scripts/SLSUtilities/Feedback/Base.meta
Normal file
8
Assets/Scripts/SLSUtilities/Feedback/Base.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1ad1a9604de9797448b5779a5a190c32
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 26b145321a43fe44d899db3f2178cb0e
|
||||
@@ -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() { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eea5c9e5f4713f2438de3ca97bf00cba
|
||||
49
Assets/Scripts/SLSUtilities/Feedback/Base/FeedbackClip.cs
Normal file
49
Assets/Scripts/SLSUtilities/Feedback/Base/FeedbackClip.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 05ae22a20d69f0e4fa3009d721f2331d
|
||||
70
Assets/Scripts/SLSUtilities/Feedback/Base/FeedbackData.cs
Normal file
70
Assets/Scripts/SLSUtilities/Feedback/Base/FeedbackData.cs
Normal 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)");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 87e69f21423d3c746ae55ea47c545ba6
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 693e7631325261949bb90a5df6789240
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3b96365b54adee4887c91d228fcf159
|
||||
46
Assets/Scripts/SLSUtilities/Feedback/Base/FeedbackTrack.cs
Normal file
46
Assets/Scripts/SLSUtilities/Feedback/Base/FeedbackTrack.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8010f4b35c62b4c44b20ee97778cb33a
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8c53b0168a5ad5f4daae818aab208d20
|
||||
8
Assets/Scripts/SLSUtilities/Feedback/Editor.meta
Normal file
8
Assets/Scripts/SLSUtilities/Feedback/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e04ed28b2e1c6ef4a90b9574d94efc60
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/SLSUtilities/Feedback/Runtime.meta
Normal file
8
Assets/Scripts/SLSUtilities/Feedback/Runtime.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c442c21f1abe1f943bc6f68f0a4bb394
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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