更新
This commit is contained in:
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using Continentis.MainGame.Character;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
/// <summary>
|
||||
/// 命令静态工厂。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
/// <summary>
|
||||
/// 命令基类。子类重写 <see cref="ExecuteAsync"/> 实现具体逻辑。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
/// <summary>
|
||||
/// 指令上下文 (Command Context)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
/// <summary>
|
||||
/// 框架预定义的 CommandContext 键常量。
|
||||
|
||||
@@ -2,7 +2,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
/// <summary>命令组的执行模式。</summary>
|
||||
public enum ExecutionMode
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
/// <summary>
|
||||
/// 命令队列的优先级分桶。数值越小优先级越高。
|
||||
|
||||
@@ -2,7 +2,7 @@ using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局命令队列管理器。
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using Cysharp.Threading.Tasks;
|
||||
using SLSFramework.General;
|
||||
using SLSUtilities.General;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
/// <summary>从 outerContext 中读取一个变量并输出到控制台。</summary>
|
||||
public class Cmd_GetAndLogVariable : CommandBase
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using Cysharp.Threading.Tasks;
|
||||
using SLSFramework.General;
|
||||
using SLSUtilities.General;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
/// <summary>在 outerContext 中设置一个键值对。</summary>
|
||||
public class Cmd_SetVariable : CommandBase
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using SLSFramework.General;
|
||||
using SLSUtilities.General;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
/// <summary>等待指定秒数后在控制台输出一条信息。</summary>
|
||||
public class Cmd_WaitAndLog : CommandBase
|
||||
|
||||
8
Assets/Scripts/SLSUtilities/Feedback.meta
Normal file
8
Assets/Scripts/SLSUtilities/Feedback.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 90dde392a034bae49933eab85866c2cc
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
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
|
||||
@@ -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
|
||||
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:
|
||||
@@ -0,0 +1,698 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using Sirenix.OdinInspector;
|
||||
using Sirenix.OdinInspector.Editor;
|
||||
using UnityEditor.Callbacks;
|
||||
|
||||
namespace SLSUtilities.Feedback.Editor
|
||||
{
|
||||
public class FeedbackDataEditorWindow : OdinEditorWindow
|
||||
{
|
||||
// ─────────────── 常量 ───────────────
|
||||
|
||||
private const float RULER_HEIGHT = 22f;
|
||||
private const float TRACK_HEIGHT = 28f;
|
||||
private const float TRACK_LABEL_WIDTH = 140f;
|
||||
private const float TRACK_BUTTON_WIDTH = 40f;
|
||||
private const float DRAG_HANDLE_WIDTH = 6f;
|
||||
private const float MIN_TIMELINE_DURATION = 1.0f;
|
||||
private const float TIMELINE_PADDING_RATIO = 0.15f;
|
||||
private const float DEFAULT_SNAP_INTERVAL = 0.05f;
|
||||
private const float MIN_CLIP_DURATION = 0.01f;
|
||||
|
||||
// ─────────────── 序列化字段 ───────────────
|
||||
|
||||
[Title("Feedback Data Editor")]
|
||||
[ShowInInspector, AssetsOnly, PropertyOrder(-100)]
|
||||
[LabelText("Target Data")]
|
||||
[OnValueChanged("OnDataChanged")]
|
||||
public FeedbackData targetData;
|
||||
|
||||
[ShowInInspector, PropertyOrder(-99)]
|
||||
[HorizontalGroup("TimelineSettings", Width = 200)]
|
||||
[LabelText("View Duration")]
|
||||
[LabelWidth(90)]
|
||||
[MinValue(0.1f)]
|
||||
[OnValueChanged("OnViewDurationChanged")]
|
||||
public float viewDuration = MIN_TIMELINE_DURATION;
|
||||
|
||||
[ShowInInspector, PropertyOrder(-99)]
|
||||
[HorizontalGroup("TimelineSettings", Width = 160)]
|
||||
[LabelText("Snap")]
|
||||
[LabelWidth(35)]
|
||||
[MinValue(0.001f)]
|
||||
public float snapInterval = DEFAULT_SNAP_INTERVAL;
|
||||
|
||||
[ShowInInspector, PropertyOrder(-99)]
|
||||
[HorizontalGroup("TimelineSettings", Width = 120)]
|
||||
[LabelText("Auto Fit")]
|
||||
[LabelWidth(55)]
|
||||
public bool autoFitDuration = true;
|
||||
|
||||
[ShowInInspector, PropertyOrder(1)]
|
||||
[ShowIf("targetData")]
|
||||
[InlineEditor(Expanded = true, ObjectFieldMode = InlineEditorObjectFieldModes.Hidden)]
|
||||
public FeedbackData dataEditor;
|
||||
|
||||
// ─────────────── 拖拽状态 ───────────────
|
||||
|
||||
private enum DragMode { None, ClipMove, ClipLeft, ClipRight }
|
||||
|
||||
private DragMode _currentDragMode = DragMode.None;
|
||||
private int _dragTrackIndex = -1;
|
||||
private int _dragClipIndex = -1;
|
||||
private float _dragStartMouseTime;
|
||||
private float _dragStartClipStart;
|
||||
private float _dragStartClipDuration;
|
||||
private bool _isDirty;
|
||||
|
||||
// ─────────────── 选中状态 ───────────────
|
||||
|
||||
private int _selectedTrackIndex = -1;
|
||||
private int _selectedClipIndex = -1;
|
||||
|
||||
// ─────────────── 颜色缓存 ───────────────
|
||||
|
||||
private static readonly Dictionary<Type, Color> ActionColorCache = new Dictionary<Type, Color>();
|
||||
|
||||
private static readonly Color BackgroundColor = new Color(0.16f, 0.16f, 0.16f);
|
||||
private static readonly Color TrackBackgroundColor = new Color(0.22f, 0.22f, 0.22f);
|
||||
private static readonly Color TrackAltBackgroundColor = new Color(0.20f, 0.20f, 0.20f);
|
||||
private static readonly Color RulerBackgroundColor = new Color(0.14f, 0.14f, 0.14f);
|
||||
private static readonly Color SelectionOutlineColor = new Color(1f, 0.85f, 0.2f);
|
||||
private static readonly Color MutedOverlayColor = new Color(0.5f, 0.5f, 0.5f, 0.4f);
|
||||
private static readonly Color SoloIndicatorColor = new Color(1f, 0.85f, 0.2f);
|
||||
private static readonly Color DefaultClipColor = new Color(0.5f, 0.6f, 0.7f, 0.8f);
|
||||
private static readonly Color PlayheadColor = new Color(1f, 1f, 1f, 0.6f);
|
||||
|
||||
// ─────────────── 窗口入口 ───────────────
|
||||
|
||||
/// <summary>
|
||||
/// 通过菜单打开窗口。
|
||||
/// </summary>
|
||||
[MenuItem("Tools/SLS Utilities/Feedback Data Editor")]
|
||||
private static void OpenWindow()
|
||||
{
|
||||
var window = GetWindow<FeedbackDataEditorWindow>();
|
||||
window.titleContent = new GUIContent("Feedback Editor");
|
||||
window.Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 双击 FeedbackData 资产时自动打开编辑器。
|
||||
/// </summary>
|
||||
[OnOpenAsset(1)]
|
||||
public static bool OnOpenAsset(int instanceID, int line)
|
||||
{
|
||||
FeedbackData data = EditorUtility.InstanceIDToObject(instanceID) as FeedbackData;
|
||||
if (data == null) return false;
|
||||
|
||||
OpenWindow();
|
||||
var window = GetWindow<FeedbackDataEditorWindow>();
|
||||
window.targetData = data;
|
||||
window.OnDataChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─────────────── 数据变更回调 ───────────────
|
||||
|
||||
private void OnDataChanged()
|
||||
{
|
||||
dataEditor = targetData;
|
||||
if (autoFitDuration) FitViewDuration();
|
||||
_selectedTrackIndex = -1;
|
||||
_selectedClipIndex = -1;
|
||||
}
|
||||
|
||||
private void OnViewDurationChanged()
|
||||
{
|
||||
autoFitDuration = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自动适配 viewDuration 到数据实际长度。
|
||||
/// </summary>
|
||||
private void FitViewDuration()
|
||||
{
|
||||
if (targetData == null) return;
|
||||
float total = targetData.TotalDuration;
|
||||
viewDuration = Mathf.Max(total * (1f + TIMELINE_PADDING_RATIO), MIN_TIMELINE_DURATION);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 时间轴 GUI 主入口
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
[OnInspectorGUI]
|
||||
[PropertyOrder(-98)]
|
||||
[ShowIf("targetData")]
|
||||
private void DrawTimelineGUI()
|
||||
{
|
||||
if (targetData == null) return;
|
||||
if (autoFitDuration) FitViewDuration();
|
||||
|
||||
int numTracks = targetData.tracks != null ? targetData.tracks.Count : 0;
|
||||
float totalHeight = RULER_HEIGHT + Mathf.Max(numTracks, 1) * TRACK_HEIGHT;
|
||||
|
||||
Rect totalArea = GUILayoutUtility.GetRect(GUIContent.none, GUIStyle.none, GUILayout.Height(totalHeight));
|
||||
|
||||
Rect labelHeaderArea = new Rect(totalArea.x, totalArea.y, TRACK_LABEL_WIDTH, RULER_HEIGHT);
|
||||
Rect rulerArea = new Rect(
|
||||
totalArea.x + TRACK_LABEL_WIDTH,
|
||||
totalArea.y,
|
||||
totalArea.width - TRACK_LABEL_WIDTH,
|
||||
RULER_HEIGHT
|
||||
);
|
||||
Rect allTracksArea = new Rect(
|
||||
totalArea.x,
|
||||
totalArea.y + RULER_HEIGHT,
|
||||
totalArea.width,
|
||||
totalHeight - RULER_HEIGHT
|
||||
);
|
||||
|
||||
// 背景
|
||||
EditorGUI.DrawRect(totalArea, BackgroundColor);
|
||||
EditorGUI.DrawRect(labelHeaderArea, RulerBackgroundColor);
|
||||
EditorGUI.DrawRect(rulerArea, RulerBackgroundColor);
|
||||
|
||||
// 绘制内容
|
||||
DrawRuler(rulerArea, viewDuration);
|
||||
DrawTracks(allTracksArea, viewDuration);
|
||||
DrawTotalDurationLine(rulerArea, allTracksArea, viewDuration);
|
||||
|
||||
// 输入处理
|
||||
HandleMouseInput(totalArea, rulerArea, allTracksArea, viewDuration);
|
||||
|
||||
if (_isDirty)
|
||||
{
|
||||
Repaint();
|
||||
_isDirty = false;
|
||||
}
|
||||
|
||||
GUILayout.Space(8);
|
||||
}
|
||||
|
||||
// ─────────────── 标尺绘制 ───────────────
|
||||
|
||||
private void DrawRuler(Rect rulerArea, float duration)
|
||||
{
|
||||
if (duration <= 0) return;
|
||||
|
||||
// 自适应刻度间距
|
||||
float tickInterval = CalculateTickInterval(duration, rulerArea.width);
|
||||
|
||||
Handles.color = new Color(0.5f, 0.5f, 0.5f, 0.5f);
|
||||
GUIStyle tickLabelStyle = new GUIStyle(EditorStyles.miniLabel)
|
||||
{
|
||||
normal = { textColor = new Color(0.7f, 0.7f, 0.7f) },
|
||||
alignment = TextAnchor.UpperLeft
|
||||
};
|
||||
|
||||
int numTicks = Mathf.FloorToInt(duration / tickInterval);
|
||||
for (int i = 0; i <= numTicks; i++)
|
||||
{
|
||||
float time = i * tickInterval;
|
||||
float xPos = rulerArea.x + (time / duration) * rulerArea.width;
|
||||
|
||||
bool isMajor = Mathf.Approximately(time % (tickInterval * 2f), 0f) || i == 0;
|
||||
float lineHeight = isMajor ? rulerArea.height : rulerArea.height * 0.5f;
|
||||
|
||||
Handles.DrawLine(
|
||||
new Vector3(xPos, rulerArea.yMax - lineHeight),
|
||||
new Vector3(xPos, rulerArea.yMax)
|
||||
);
|
||||
|
||||
if (isMajor)
|
||||
{
|
||||
GUI.Label(new Rect(xPos + 2, rulerArea.y, 50, rulerArea.height), $"{time:F2}s", tickLabelStyle);
|
||||
}
|
||||
}
|
||||
|
||||
// 总时长标记
|
||||
GUIStyle totalStyle = new GUIStyle(EditorStyles.miniLabel)
|
||||
{
|
||||
normal = { textColor = new Color(0.9f, 0.9f, 0.5f) },
|
||||
alignment = TextAnchor.UpperRight
|
||||
};
|
||||
string totalLabel = $"Total: {targetData.TotalDuration:F2}s";
|
||||
GUI.Label(new Rect(rulerArea.xMax - 100, rulerArea.y, 98, rulerArea.height), totalLabel, totalStyle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据时间轴总时长和像素宽度计算合理的刻度间距。
|
||||
/// </summary>
|
||||
private float CalculateTickInterval(float duration, float width)
|
||||
{
|
||||
float targetPixelsPerTick = 60f;
|
||||
float idealInterval = duration * targetPixelsPerTick / width;
|
||||
|
||||
float[] candidates = { 0.01f, 0.02f, 0.05f, 0.1f, 0.2f, 0.25f, 0.5f, 1f, 2f, 5f, 10f };
|
||||
foreach (float c in candidates)
|
||||
{
|
||||
if (c >= idealInterval) return c;
|
||||
}
|
||||
return 10f;
|
||||
}
|
||||
|
||||
// ─────────────── TotalDuration 指示线 ───────────────
|
||||
|
||||
private void DrawTotalDurationLine(Rect rulerArea, Rect tracksArea, float duration)
|
||||
{
|
||||
float total = targetData.TotalDuration;
|
||||
if (total <= 0 || duration <= 0) return;
|
||||
|
||||
float xPos = rulerArea.x + (total / duration) * rulerArea.width;
|
||||
Handles.color = new Color(0.9f, 0.9f, 0.3f, 0.4f);
|
||||
Handles.DrawLine(
|
||||
new Vector3(xPos, rulerArea.y),
|
||||
new Vector3(xPos, tracksArea.yMax)
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────── 轨道绘制 ───────────────
|
||||
|
||||
private void DrawTracks(Rect allTracksArea, float duration)
|
||||
{
|
||||
if (targetData.tracks == null || targetData.tracks.Count == 0)
|
||||
{
|
||||
GUIStyle emptyStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel);
|
||||
GUI.Label(allTracksArea, "No tracks. Add tracks in the inspector below.", emptyStyle);
|
||||
return;
|
||||
}
|
||||
|
||||
float currentY = allTracksArea.y;
|
||||
|
||||
for (int trackIdx = 0; trackIdx < targetData.tracks.Count; trackIdx++)
|
||||
{
|
||||
FeedbackTrack track = targetData.tracks[trackIdx];
|
||||
Rect rowRect = new Rect(allTracksArea.x, currentY, allTracksArea.width, TRACK_HEIGHT);
|
||||
Rect labelRect = new Rect(rowRect.x, rowRect.y, TRACK_LABEL_WIDTH, rowRect.height - 1);
|
||||
Rect trackRect = new Rect(
|
||||
rowRect.x + TRACK_LABEL_WIDTH,
|
||||
rowRect.y,
|
||||
rowRect.width - TRACK_LABEL_WIDTH,
|
||||
rowRect.height - 1
|
||||
);
|
||||
|
||||
// 轨道背景(交替色)
|
||||
Color bgColor = (trackIdx % 2 == 0) ? TrackBackgroundColor : TrackAltBackgroundColor;
|
||||
EditorGUI.DrawRect(trackRect, bgColor);
|
||||
|
||||
// 轨道标签
|
||||
DrawTrackLabel(labelRect, track, trackIdx);
|
||||
|
||||
// 轨道中的 Clip
|
||||
if (track.clips != null)
|
||||
{
|
||||
for (int clipIdx = 0; clipIdx < track.clips.Count; clipIdx++)
|
||||
{
|
||||
DrawClip(trackRect, track.clips[clipIdx], trackIdx, clipIdx, duration);
|
||||
}
|
||||
}
|
||||
|
||||
// Mute 叠加层
|
||||
if (track.mute)
|
||||
{
|
||||
EditorGUI.DrawRect(trackRect, MutedOverlayColor);
|
||||
}
|
||||
|
||||
currentY += TRACK_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绘制轨道标签区域(名称 + mute/solo 指示)。
|
||||
/// </summary>
|
||||
private void DrawTrackLabel(Rect labelRect, FeedbackTrack track, int trackIdx)
|
||||
{
|
||||
EditorGUI.DrawRect(labelRect, RulerBackgroundColor);
|
||||
|
||||
// Solo 指示条
|
||||
if (track.solo)
|
||||
{
|
||||
Rect soloBar = new Rect(labelRect.x, labelRect.y, 3f, labelRect.height);
|
||||
EditorGUI.DrawRect(soloBar, SoloIndicatorColor);
|
||||
}
|
||||
|
||||
GUIStyle labelStyle = new GUIStyle(EditorStyles.label)
|
||||
{
|
||||
fontSize = 11,
|
||||
padding = new RectOffset(8, 0, 0, 0)
|
||||
};
|
||||
|
||||
string prefix = "";
|
||||
if (track.mute) prefix = "[M] ";
|
||||
if (track.solo) prefix = "[S] ";
|
||||
|
||||
string displayName = string.IsNullOrEmpty(track.trackName) ? $"Track {trackIdx}" : track.trackName;
|
||||
labelStyle.normal.textColor = track.mute
|
||||
? new Color(0.5f, 0.5f, 0.5f)
|
||||
: new Color(0.85f, 0.85f, 0.85f);
|
||||
|
||||
EditorGUI.LabelField(labelRect, $"{prefix}{displayName}", labelStyle);
|
||||
}
|
||||
|
||||
// ─────────────── Clip 绘制 ───────────────
|
||||
|
||||
private void DrawClip(Rect trackRect, FeedbackClip clip, int trackIdx, int clipIdx, float duration)
|
||||
{
|
||||
if (clip == null || duration <= 0) return;
|
||||
|
||||
float startNorm = clip.startTime / duration;
|
||||
float endNorm = clip.EndTime / duration;
|
||||
|
||||
float startX = trackRect.x + startNorm * trackRect.width;
|
||||
float endX = trackRect.x + endNorm * trackRect.width;
|
||||
float width = Mathf.Max(endX - startX, 2f);
|
||||
|
||||
Rect clipRect = new Rect(startX, trackRect.y + 2f, width, trackRect.height - 4f);
|
||||
|
||||
// Clip 填充色
|
||||
Color clipColor = GetActionColor(clip.action);
|
||||
bool isSelected = (trackIdx == _selectedTrackIndex && clipIdx == _selectedClipIndex);
|
||||
|
||||
if (isSelected)
|
||||
{
|
||||
clipColor = Color.Lerp(clipColor, Color.white, 0.2f);
|
||||
}
|
||||
|
||||
EditorGUI.DrawRect(clipRect, clipColor);
|
||||
|
||||
// 选中描边
|
||||
if (isSelected)
|
||||
{
|
||||
DrawRectOutline(clipRect, SelectionOutlineColor, 1f);
|
||||
}
|
||||
|
||||
// Clip 标签
|
||||
string clipLabel = clip.action != null ? clip.action.DisplayName : "(null)";
|
||||
GUIStyle clipLabelStyle = new GUIStyle(EditorStyles.miniLabel)
|
||||
{
|
||||
normal = { textColor = Color.white },
|
||||
clipping = TextClipping.Clip,
|
||||
alignment = TextAnchor.MiddleLeft,
|
||||
padding = new RectOffset(4, 2, 0, 0),
|
||||
fontSize = 10
|
||||
};
|
||||
|
||||
if (clipRect.width > 20f)
|
||||
{
|
||||
GUI.Label(clipRect, clipLabel, clipLabelStyle);
|
||||
}
|
||||
|
||||
// 左右拖拽把手的视觉提示
|
||||
if (clipRect.width > DRAG_HANDLE_WIDTH * 3)
|
||||
{
|
||||
Color handleColor = new Color(1f, 1f, 1f, 0.15f);
|
||||
Rect leftHandle = new Rect(clipRect.x, clipRect.y, DRAG_HANDLE_WIDTH, clipRect.height);
|
||||
Rect rightHandle = new Rect(clipRect.xMax - DRAG_HANDLE_WIDTH, clipRect.y, DRAG_HANDLE_WIDTH, clipRect.height);
|
||||
EditorGUI.DrawRect(leftHandle, handleColor);
|
||||
EditorGUI.DrawRect(rightHandle, handleColor);
|
||||
}
|
||||
|
||||
// Tooltip
|
||||
string tooltip = $"{clipLabel}\n{clip.startTime:F3}s - {clip.EndTime:F3}s (dur: {clip.duration:F3}s)";
|
||||
GUI.Label(clipRect, new GUIContent("", tooltip));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绘制矩形描边。
|
||||
/// </summary>
|
||||
private void DrawRectOutline(Rect rect, Color color, float thickness)
|
||||
{
|
||||
EditorGUI.DrawRect(new Rect(rect.x, rect.y, rect.width, thickness), color);
|
||||
EditorGUI.DrawRect(new Rect(rect.x, rect.yMax - thickness, rect.width, thickness), color);
|
||||
EditorGUI.DrawRect(new Rect(rect.x, rect.y, thickness, rect.height), color);
|
||||
EditorGUI.DrawRect(new Rect(rect.xMax - thickness, rect.y, thickness, rect.height), color);
|
||||
}
|
||||
|
||||
// ─────────────── Action 颜色 ───────────────
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Action 的时间轴显示颜色(优先从 FeedbackActionColorAttribute 读取,否则用类型哈希生成)。
|
||||
/// </summary>
|
||||
private Color GetActionColor(FeedbackActionBase action)
|
||||
{
|
||||
if (action == null) return DefaultClipColor;
|
||||
|
||||
Type type = action.GetType();
|
||||
if (ActionColorCache.TryGetValue(type, out Color cached)) return cached;
|
||||
|
||||
var attr = type.GetCustomAttribute<FeedbackActionColorAttribute>();
|
||||
Color color;
|
||||
if (attr != null)
|
||||
{
|
||||
color = attr.Color;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 根据类型名哈希生成确定性颜色
|
||||
int hash = type.FullName?.GetHashCode() ?? 0;
|
||||
float h = Mathf.Abs(hash % 360) / 360f;
|
||||
color = Color.HSVToRGB(h, 0.5f, 0.75f);
|
||||
color.a = 0.8f;
|
||||
}
|
||||
|
||||
ActionColorCache[type] = color;
|
||||
return color;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 鼠标输入处理
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
private void HandleMouseInput(Rect totalArea, Rect rulerArea, Rect allTracksArea, float duration)
|
||||
{
|
||||
Event e = Event.current;
|
||||
Vector2 mousePos = e.mousePosition;
|
||||
|
||||
if (!totalArea.Contains(mousePos))
|
||||
{
|
||||
if (e.type == EventType.MouseUp) ResetDrag();
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateMouseCursor(allTracksArea, mousePos, duration);
|
||||
|
||||
switch (e.type)
|
||||
{
|
||||
case EventType.MouseDown when e.button == 0:
|
||||
HandleMouseDown(allTracksArea, mousePos, duration, e);
|
||||
break;
|
||||
|
||||
case EventType.MouseDrag when _currentDragMode != DragMode.None:
|
||||
HandleMouseDrag(allTracksArea, mousePos, duration, e);
|
||||
break;
|
||||
|
||||
case EventType.MouseUp when e.button == 0:
|
||||
ResetDrag();
|
||||
_isDirty = true;
|
||||
e.Use();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleMouseDown(Rect allTracksArea, Vector2 mousePos, float duration, Event e)
|
||||
{
|
||||
if (!allTracksArea.Contains(mousePos)) return;
|
||||
|
||||
Undo.RecordObject(targetData, "Modify Feedback Timeline");
|
||||
|
||||
var hit = HitTestClip(allTracksArea, mousePos, duration);
|
||||
if (hit.trackIndex != -1)
|
||||
{
|
||||
_selectedTrackIndex = hit.trackIndex;
|
||||
_selectedClipIndex = hit.clipIndex;
|
||||
|
||||
Rect trackRect = GetTrackContentRect(allTracksArea, hit.trackIndex);
|
||||
float mouseTime = PixelToTime(mousePos.x, trackRect, duration);
|
||||
|
||||
FeedbackClip clip = targetData.tracks[hit.trackIndex].clips[hit.clipIndex];
|
||||
_dragStartMouseTime = mouseTime;
|
||||
_dragStartClipStart = clip.startTime;
|
||||
_dragStartClipDuration = clip.duration;
|
||||
|
||||
_currentDragMode = hit.dragMode;
|
||||
_dragTrackIndex = hit.trackIndex;
|
||||
_dragClipIndex = hit.clipIndex;
|
||||
|
||||
_isDirty = true;
|
||||
e.Use();
|
||||
return;
|
||||
}
|
||||
|
||||
// 点击空白 → 取消选中
|
||||
_selectedTrackIndex = -1;
|
||||
_selectedClipIndex = -1;
|
||||
_isDirty = true;
|
||||
e.Use();
|
||||
}
|
||||
|
||||
private void HandleMouseDrag(Rect allTracksArea, Vector2 mousePos, float duration, Event e)
|
||||
{
|
||||
if (_dragTrackIndex < 0 || _dragTrackIndex >= targetData.tracks.Count) return;
|
||||
var track = targetData.tracks[_dragTrackIndex];
|
||||
if (_dragClipIndex < 0 || _dragClipIndex >= track.clips.Count) return;
|
||||
|
||||
Rect trackRect = GetTrackContentRect(allTracksArea, _dragTrackIndex);
|
||||
float mouseTime = PixelToTime(mousePos.x, trackRect, duration);
|
||||
float timeDelta = mouseTime - _dragStartMouseTime;
|
||||
|
||||
FeedbackClip clip = track.clips[_dragClipIndex];
|
||||
|
||||
switch (_currentDragMode)
|
||||
{
|
||||
case DragMode.ClipMove:
|
||||
float newStart = SnapTime(_dragStartClipStart + timeDelta);
|
||||
newStart = Mathf.Max(0, newStart);
|
||||
clip.startTime = newStart;
|
||||
break;
|
||||
|
||||
case DragMode.ClipLeft:
|
||||
float newLeftStart = SnapTime(_dragStartClipStart + timeDelta);
|
||||
newLeftStart = Mathf.Max(0, newLeftStart);
|
||||
float maxLeft = _dragStartClipStart + _dragStartClipDuration - MIN_CLIP_DURATION;
|
||||
newLeftStart = Mathf.Min(newLeftStart, maxLeft);
|
||||
float endTime = _dragStartClipStart + _dragStartClipDuration;
|
||||
clip.startTime = newLeftStart;
|
||||
clip.duration = Mathf.Max(endTime - newLeftStart, MIN_CLIP_DURATION);
|
||||
break;
|
||||
|
||||
case DragMode.ClipRight:
|
||||
float newDuration = SnapTime(_dragStartClipDuration + timeDelta);
|
||||
clip.duration = Mathf.Max(newDuration, MIN_CLIP_DURATION);
|
||||
break;
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(targetData);
|
||||
_isDirty = true;
|
||||
e.Use();
|
||||
}
|
||||
|
||||
private void ResetDrag()
|
||||
{
|
||||
_currentDragMode = DragMode.None;
|
||||
_dragTrackIndex = -1;
|
||||
_dragClipIndex = -1;
|
||||
}
|
||||
|
||||
// ─────────────── Hit Testing ───────────────
|
||||
|
||||
private struct ClipHitResult
|
||||
{
|
||||
public int trackIndex;
|
||||
public int clipIndex;
|
||||
public DragMode dragMode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试鼠标位置是否命中某个 Clip,并返回拖拽模式。
|
||||
/// </summary>
|
||||
private ClipHitResult HitTestClip(Rect allTracksArea, Vector2 mousePos, float duration)
|
||||
{
|
||||
var result = new ClipHitResult { trackIndex = -1, clipIndex = -1, dragMode = DragMode.None };
|
||||
if (targetData.tracks == null || duration <= 0) return result;
|
||||
|
||||
// 确定鼠标所在轨道
|
||||
float yInTracks = mousePos.y - allTracksArea.y;
|
||||
int trackIndex = Mathf.FloorToInt(yInTracks / TRACK_HEIGHT);
|
||||
if (trackIndex < 0 || trackIndex >= targetData.tracks.Count) return result;
|
||||
|
||||
Rect trackRect = GetTrackContentRect(allTracksArea, trackIndex);
|
||||
if (!trackRect.Contains(mousePos)) return result;
|
||||
|
||||
var track = targetData.tracks[trackIndex];
|
||||
if (track.clips == null) return result;
|
||||
|
||||
// 从后往前检测(后绘制的优先)
|
||||
for (int i = track.clips.Count - 1; i >= 0; i--)
|
||||
{
|
||||
FeedbackClip clip = track.clips[i];
|
||||
if (clip == null) continue;
|
||||
|
||||
float startX = trackRect.x + (clip.startTime / duration) * trackRect.width;
|
||||
float endX = trackRect.x + (clip.EndTime / duration) * trackRect.width;
|
||||
|
||||
if (mousePos.x >= startX && mousePos.x <= endX)
|
||||
{
|
||||
result.trackIndex = trackIndex;
|
||||
result.clipIndex = i;
|
||||
|
||||
if (mousePos.x - startX <= DRAG_HANDLE_WIDTH)
|
||||
result.dragMode = DragMode.ClipLeft;
|
||||
else if (endX - mousePos.x <= DRAG_HANDLE_WIDTH)
|
||||
result.dragMode = DragMode.ClipRight;
|
||||
else
|
||||
result.dragMode = DragMode.ClipMove;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─────────────── 光标更新 ───────────────
|
||||
|
||||
private void UpdateMouseCursor(Rect allTracksArea, Vector2 mousePos, float duration)
|
||||
{
|
||||
if (!allTracksArea.Contains(mousePos)) return;
|
||||
|
||||
var hit = HitTestClip(allTracksArea, mousePos, duration);
|
||||
if (hit.trackIndex == -1)
|
||||
{
|
||||
EditorGUIUtility.AddCursorRect(allTracksArea, MouseCursor.Arrow);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (hit.dragMode)
|
||||
{
|
||||
case DragMode.ClipLeft:
|
||||
case DragMode.ClipRight:
|
||||
EditorGUIUtility.AddCursorRect(allTracksArea, MouseCursor.ResizeHorizontal);
|
||||
break;
|
||||
default:
|
||||
EditorGUIUtility.AddCursorRect(allTracksArea, MouseCursor.MoveArrow);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────── 工具方法 ───────────────
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定轨道的内容区域(不含标签列)。
|
||||
/// </summary>
|
||||
private Rect GetTrackContentRect(Rect allTracksArea, int trackIndex)
|
||||
{
|
||||
return new Rect(
|
||||
allTracksArea.x + TRACK_LABEL_WIDTH,
|
||||
allTracksArea.y + trackIndex * TRACK_HEIGHT,
|
||||
allTracksArea.width - TRACK_LABEL_WIDTH,
|
||||
TRACK_HEIGHT - 1
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 像素坐标转换为时间值。
|
||||
/// </summary>
|
||||
private float PixelToTime(float pixelX, Rect trackRect, float duration)
|
||||
{
|
||||
if (trackRect.width <= 0) return 0;
|
||||
return Mathf.Clamp((pixelX - trackRect.x) / trackRect.width * duration, 0, duration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将时间吸附到最近的 snapInterval 刻度。
|
||||
/// </summary>
|
||||
private float SnapTime(float time)
|
||||
{
|
||||
if (snapInterval <= 0) return Mathf.Max(0, time);
|
||||
return Mathf.Max(0, Mathf.Round(time / snapInterval) * snapInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 91af4d70c106a2147a6b469af19db689
|
||||
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
|
||||
@@ -4,7 +4,7 @@ using System.Linq;
|
||||
using SoftCircuits.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
public static class DictionaryExtension
|
||||
{
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using System;
|
||||
using SLSFramework.General;
|
||||
using SLSUtilities.General;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
public class PrioritizedAction : IPrioritized
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
/// <summary>
|
||||
/// 实现该接口的类可以根据优先级进行比较和排序。
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.Linq;
|
||||
using UnityEngine;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
public static partial class ListExtension
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
public static class SpaceConverter
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
public static class SpriteExtension
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using Continentis.MainGame.Card;
|
||||
using I2.Loc;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
public static class StringExtension
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
public static class TransformExtension
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ using NaughtyAttributes;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace SLSFramework.General.LeanPoolAssistance
|
||||
namespace SLSUtilities.General.LeanPoolAssistance
|
||||
{
|
||||
public class PooledObject : MonoBehaviour, IPoolable
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using UnityEditor;
|
||||
#endif
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
[System.Serializable]
|
||||
public class InterfaceHolder<T> where T : class
|
||||
|
||||
@@ -2,7 +2,7 @@ using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
[System.Serializable]
|
||||
public class SerializableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, ISerializationCallbackReceiver
|
||||
|
||||
@@ -6,7 +6,7 @@ using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
[CustomPropertyDrawer(typeof(SerializableDictionary<,>), true)]
|
||||
public class SerializableDictionaryDrawer : PropertyDrawer
|
||||
@@ -410,7 +410,7 @@ namespace SLSFramework.General
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于指定 SerializableDictionary 抽屉中 Key 区域的宽度占比。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.General
|
||||
namespace SLSUtilities.General
|
||||
{
|
||||
[System.Serializable]
|
||||
public class UnityObjectWrapper<T> where T : class
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using SLSFramework.StorySystem.Dialog;
|
||||
using SLSUtilities.StorySystem.Dialog;
|
||||
using UnityEditor.Experimental.GraphView;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.StorySystem
|
||||
namespace SLSUtilities.StorySystem
|
||||
{
|
||||
// 自定义节点的基础类,用于存储对应的数据
|
||||
public abstract class BaseGraphNode : Node
|
||||
|
||||
@@ -2,7 +2,7 @@ using UnityEditor.Experimental.GraphView;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace SLSFramework.StorySystem
|
||||
namespace SLSUtilities.StorySystem
|
||||
{
|
||||
public class ConditionGraphNode : BaseGraphNode
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ using UnityEditor.Experimental.GraphView;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace SLSFramework.StorySystem
|
||||
namespace SLSUtilities.StorySystem
|
||||
{
|
||||
public class EndGraphNode : BaseGraphNode
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ using UnityEditor.Experimental.GraphView;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace SLSFramework.StorySystem.Dialog
|
||||
namespace SLSUtilities.StorySystem.Dialog
|
||||
{
|
||||
public class EventGraphNode : BaseGraphNode
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ using UnityEditor.Experimental.GraphView;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace SLSFramework.StorySystem
|
||||
namespace SLSUtilities.StorySystem
|
||||
{
|
||||
public class StartGraphNode : BaseGraphNode
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.StorySystem
|
||||
namespace SLSUtilities.StorySystem
|
||||
{
|
||||
public class EditorWindowBase : EditorWindow
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ using UnityEditor.Experimental.GraphView;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace SLSFramework.StorySystem
|
||||
namespace SLSUtilities.StorySystem
|
||||
{
|
||||
public partial class GraphViewBase : GraphView
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace SLSFramework.StorySystem.Dialog
|
||||
namespace SLSUtilities.StorySystem.Dialog
|
||||
{
|
||||
public class ChoiceGraphNode : BaseGraphNode
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace SLSFramework.StorySystem.Dialog
|
||||
namespace SLSUtilities.StorySystem.Dialog
|
||||
{
|
||||
public class CompoundDialogNode : BaseGraphNode
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace SLSFramework.StorySystem.Dialog
|
||||
namespace SLSUtilities.StorySystem.Dialog
|
||||
{
|
||||
public class DialogGraphNode : BaseGraphNode
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ using UnityEngine;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace SLSFramework.StorySystem.Dialog
|
||||
namespace SLSUtilities.StorySystem.Dialog
|
||||
{
|
||||
public class DialogGraphEditorWindow : EditorWindowBase
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ using UnityEditor.Experimental.GraphView;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace SLSFramework.StorySystem.Dialog
|
||||
namespace SLSUtilities.StorySystem.Dialog
|
||||
{
|
||||
public partial class DialogGraphView : GraphViewBase
|
||||
{
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using SLSFramework.StorySystem.Dialog;
|
||||
using SLSUtilities.StorySystem.Dialog;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Experimental.GraphView;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace SLSFramework.StorySystem.Storyline
|
||||
namespace SLSUtilities.StorySystem.Storyline
|
||||
{
|
||||
public class StorylineDialogNode : BaseGraphNode
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace SLSFramework.StorySystem.Storyline
|
||||
namespace SLSUtilities.StorySystem.Storyline
|
||||
{
|
||||
public class StorylineGraphEditorWindow : EditorWindowBase
|
||||
{
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using SLSFramework.StorySystem.Dialog;
|
||||
using SLSUtilities.StorySystem.Dialog;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Experimental.GraphView;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace SLSFramework.StorySystem.Storyline
|
||||
namespace SLSUtilities.StorySystem.Storyline
|
||||
{
|
||||
public class StorylineGraphView : GraphViewBase
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.StorySystem.Dialog
|
||||
namespace SLSUtilities.StorySystem.Dialog
|
||||
{
|
||||
[CreateAssetMenu(fileName = "NewDialogueGraph", menuName = "StorySystem/Dialog/Dialog Graph")]
|
||||
public class DialogGraph : GraphBase
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace SLSFramework.StorySystem.Dialog
|
||||
namespace SLSUtilities.StorySystem.Dialog
|
||||
{
|
||||
// ----------------------------------------------------------------------
|
||||
// 各种具体节点的数据
|
||||
|
||||
@@ -2,7 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.StorySystem
|
||||
namespace SLSUtilities.StorySystem
|
||||
{
|
||||
[CreateAssetMenu(fileName = "NewCharacterData", menuName = "StorySystem/Character Data")]
|
||||
public partial class StoryCharacterData : ScriptableObject
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.StorySystem
|
||||
namespace SLSUtilities.StorySystem
|
||||
{
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.StorySystem.Storyline
|
||||
namespace SLSUtilities.StorySystem.Storyline
|
||||
{
|
||||
[CreateAssetMenu(fileName = "NewStorylineGraph", menuName = "StorySystem/Storyline/Storyline Graph")]
|
||||
public class StorylineGraph : GraphBase
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using SLSFramework.StorySystem.Dialog;
|
||||
using SLSUtilities.StorySystem.Dialog;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.StorySystem.Storyline
|
||||
namespace SLSUtilities.StorySystem.Storyline
|
||||
{
|
||||
[Serializable]
|
||||
public class StorylineDialogNodeData : BaseNodeData
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.StorySystem
|
||||
namespace SLSUtilities.StorySystem
|
||||
{
|
||||
public abstract class GraphBase : ScriptableObject
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace SLSFramework.UModAssistance
|
||||
namespace SLSUtilities.UModAssistance
|
||||
{
|
||||
#region List<string>选择器,通过类型查找资产,将其名称存储在列表中
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ using Continentis.MainGame.Equipment;
|
||||
using Continentis.MainGame.Rules;
|
||||
using Continentis.Mods;
|
||||
using I2.Loc;
|
||||
using SLSFramework.General;
|
||||
using SLSUtilities.General;
|
||||
using UMod;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace SLSFramework.UModAssistance
|
||||
namespace SLSUtilities.UModAssistance
|
||||
{
|
||||
public partial class ModBrowser : MonoBehaviour
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ using UMod;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace SLSFramework.UModAssistance
|
||||
namespace SLSUtilities.UModAssistance
|
||||
{
|
||||
public class ModLoadTab : MonoBehaviour
|
||||
{
|
||||
|
||||
@@ -4,13 +4,13 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using SLSFramework.General;
|
||||
using SLSUtilities.General;
|
||||
using UMod;
|
||||
using UMod.Scripting;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace SLSFramework.UModAssistance
|
||||
namespace SLSUtilities.UModAssistance
|
||||
{
|
||||
public static partial class ModManager
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user