using System; using System.Collections; using System.Collections.Generic; using System.Reflection; using Sirenix.OdinInspector; using UnityEngine; namespace SLSUtilities.Feedback { /// /// 反馈播放上下文,传递给每个 FeedbackAction 的生命周期回调。 /// public struct FeedbackContext { /// /// 当前播放器实例。 /// public FeedbackPlayer player; public int runtimeFeedbackId; public int trackIndex; public int clipIndex; /// /// 触发者的 Transform(可选)。 /// public Transform owner; /// /// 当前帧经过时间缩放处理后的 deltaTime。 /// public float deltaTime; /// /// Clip 已播放时间(秒)。 /// public float elapsedTime; /// /// Clip 总时长(秒)。 /// public float duration; /// /// 当前 Clip 的综合时间缩放系数(含 Global/Group/Local),由 FeedbackPlayer 每帧动态计算。 /// public float timeScale; /// /// 当前 Clip 是否动态获取当前的时间缩放 /// public FeedbackTimeSettings timeSettings; } /// /// 所有反馈动作的抽象基类,定义生命周期回调。 /// 使用 Odin 的序列化路径实现多态序列化(SerializedScriptableObject)。 /// Odin 会自动为此抽象类型的字段显示多态类型选择器。 /// [Serializable] public abstract class FeedbackActionBase { /// /// Inspector 中显示的名称。 /// public virtual string DisplayName => GetType().Name; /// /// 是否忽略时间缩放。如果为true,此Action将使用原始deltaTime,不受TimeScale影响。 /// public virtual bool IgnoreTimeScale => false; /// /// 初始化,FeedbackPlayer 开始播放此 Clip 时调用。 /// public virtual void OnStart(FeedbackContext context) { } /// /// 每帧更新,normalizedTime 为 Clip 内的归一化进度 [0,1]。 /// public virtual void OnUpdate(FeedbackContext context, float normalizedTime) { } /// /// Clip 自然结束时调用。 /// public virtual void OnEnd(FeedbackContext context) { } /// /// 被打断时调用,负责立即复位到初始状态。 /// public virtual void OnInterrupt(FeedbackContext context) { } /// /// 用于验证配置是否正确(Editor 环境)。 /// public virtual bool Validate(out string error) { error = null; return true; } /// /// 用于 Editor 预览(Runtime 也可用)。 /// public virtual void Preview() { } public virtual FeedbackActionBase CreateRuntimeCopy() { return FeedbackRuntimeCloneUtility.CloneSerialized(this); } /// /// 根据归一化时间采样曲线并映射到实际值范围。 /// 如果 relativeToInitial 为 true,结果会叠加在 initialValue 上。 /// /// 震动曲线,X 轴为归一化时间 [0,1],Y 轴为震动强度 [0,1]。 /// 曲线值 0 对应的实际数值。 /// 曲线值 1 对应的实际数值。 /// 是否在初始值上叠加(而非替换)。 /// 归一化时间 [0,1] /// 初始值(OnStart 时记录) /// 映射后的最终数值 protected virtual float EvaluateShake(AnimationCurve shakeCurve, float remapMin, float remapMax, bool relativeToInitial, float normalizedTime, float initialValue) { float curveValue = shakeCurve.Evaluate(normalizedTime); float remappedValue = Mathf.LerpUnclamped(remapMin, remapMax, curveValue); if (relativeToInitial) { return initialValue + remappedValue; } return remappedValue; } } internal static class FeedbackRuntimeCloneUtility { public static T CloneSerialized(T source) { return (T)CloneValue(source); } private static object CloneValue(object source) { if (source == null) return null; Type type = source.GetType(); if (IsDirectReference(type)) { return source; } if (source is AnimationCurve animationCurve) { return CloneAnimationCurve(animationCurve); } if (source is Gradient gradient) { return CloneGradient(gradient); } if (source is UnityEngine.Object) { return source; } if (type.IsArray) { return CloneArray((Array)source, type.GetElementType()); } if (source is IList list) { return CloneList(list, type); } if (type.IsValueType) { object boxed = source; CopySerializedFields(source, boxed, type); return boxed; } object clone; try { clone = Activator.CreateInstance(type); } catch (Exception exception) { Debug.LogWarning($"[FeedbackRuntimeCloneUtility] Failed to clone '{type.Name}'. Runtime playback will reuse the original reference. {exception.Message}"); return source; } CopySerializedFields(source, clone, type); return clone; } private static bool IsDirectReference(Type type) { return type.IsPrimitive || type.IsEnum || type.Namespace != null && type.Namespace.StartsWith("AK.Wwise", StringComparison.Ordinal) || type == typeof(string) || type == typeof(decimal) || type == typeof(Vector2) || type == typeof(Vector3) || type == typeof(Vector4) || type == typeof(Quaternion) || type == typeof(Color) || type == typeof(Color32) || type == typeof(Rect) || type == typeof(Bounds) || type == typeof(LayerMask); } private static AnimationCurve CloneAnimationCurve(AnimationCurve source) { AnimationCurve clone = new AnimationCurve(source.keys) { preWrapMode = source.preWrapMode, postWrapMode = source.postWrapMode }; return clone; } private static Gradient CloneGradient(Gradient source) { Gradient clone = new Gradient { mode = source.mode }; clone.SetKeys(source.colorKeys, source.alphaKeys); return clone; } private static Array CloneArray(Array source, Type elementType) { Array clone = Array.CreateInstance(elementType, source.Length); for (int i = 0; i < source.Length; i++) { clone.SetValue(CloneValue(source.GetValue(i)), i); } return clone; } private static object CloneList(IList source, Type listType) { Type elementType = typeof(object); if (listType.IsGenericType) { elementType = listType.GetGenericArguments()[0]; } IList clone = CreateListInstance(listType, elementType); foreach (object value in source) { clone.Add(CloneValue(value)); } return clone; } private static IList CreateListInstance(Type listType, Type elementType) { if (!listType.IsInterface && !listType.IsAbstract) { try { return (IList)Activator.CreateInstance(listType); } catch { // Fall back to List below. } } Type concreteType = typeof(List<>).MakeGenericType(elementType); return (IList)Activator.CreateInstance(concreteType); } private static void CopySerializedFields(object source, object target, Type type) { while (type != null && type != typeof(object)) { FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly); for (int i = 0; i < fields.Length; i++) { FieldInfo field = fields[i]; if (!ShouldCopyField(field)) continue; object value = field.GetValue(source); field.SetValue(target, CloneValue(value)); } type = type.BaseType; } } private static bool ShouldCopyField(FieldInfo field) { if (field.IsStatic) return false; if (field.IsInitOnly) return false; if (field.IsNotSerialized) return false; return field.IsPublic || Attribute.IsDefined(field, typeof(SerializeField)) || Attribute.IsDefined(field, typeof(SerializeReference)); } } }