This commit is contained in:
SoulliesOfficial
2026-04-17 12:01:50 -04:00
parent dd2657573a
commit ac98ec3aef
438 changed files with 4505 additions and 428 deletions

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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 460e3071059bce248806e5bb6f81d27e

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