更新
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSUtilities.Feedback
|
||||
{
|
||||
/// <summary>
|
||||
/// 为 FeedbackActionBase 子类指定在时间轴编辑器中的显示颜色。
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class FeedbackActionColorAttribute : Attribute
|
||||
{
|
||||
public Color Color { get; }
|
||||
|
||||
public FeedbackActionColorAttribute(float r, float g, float b, float a = 0.8f)
|
||||
{
|
||||
Color = new Color(r, g, b, a);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 460e3071059bce248806e5bb6f81d27e
|
||||
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
|
||||
Reference in New Issue
Block a user