319 lines
10 KiB
C#
319 lines
10 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.Reflection;
|
||
using Sirenix.OdinInspector;
|
||
using UnityEngine;
|
||
|
||
namespace SLSUtilities.Feedback
|
||
{
|
||
/// <summary>
|
||
/// 反馈播放上下文,传递给每个 FeedbackAction 的生命周期回调。
|
||
/// </summary>
|
||
public struct FeedbackContext
|
||
{
|
||
/// <summary>
|
||
/// 当前播放器实例。
|
||
/// </summary>
|
||
public FeedbackPlayer player;
|
||
|
||
public int runtimeFeedbackId;
|
||
|
||
public int trackIndex;
|
||
|
||
public int clipIndex;
|
||
|
||
/// <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>
|
||
/// 当前 Clip 的综合时间缩放系数(含 Global/Group/Local),由 FeedbackPlayer 每帧动态计算。
|
||
/// </summary>
|
||
public float timeScale;
|
||
|
||
/// <summary>
|
||
/// 当前 Clip 是否动态获取当前的时间缩放
|
||
/// </summary>
|
||
public FeedbackTimeSettings timeSettings;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 所有反馈动作的抽象基类,定义生命周期回调。
|
||
/// 使用 Odin 的序列化路径实现多态序列化(SerializedScriptableObject)。
|
||
/// Odin 会自动为此抽象类型的字段显示多态类型选择器。
|
||
/// </summary>
|
||
[Serializable]
|
||
public abstract class FeedbackActionBase
|
||
{
|
||
/// <summary>
|
||
/// Inspector 中显示的名称。
|
||
/// </summary>
|
||
public virtual string DisplayName => GetType().Name;
|
||
|
||
/// <summary>
|
||
/// 是否忽略时间缩放。如果为true,此Action将使用原始deltaTime,不受TimeScale影响。
|
||
/// </summary>
|
||
public virtual bool IgnoreTimeScale => false;
|
||
|
||
/// <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() { }
|
||
|
||
public virtual FeedbackActionBase CreateRuntimeCopy()
|
||
{
|
||
return FeedbackRuntimeCloneUtility.CloneSerialized(this);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据归一化时间采样曲线并映射到实际值范围。
|
||
/// 如果 relativeToInitial 为 true,结果会叠加在 initialValue 上。
|
||
/// </summary>
|
||
/// <param name="shakeCurve">震动曲线,X 轴为归一化时间 [0,1],Y 轴为震动强度 [0,1]。</param>
|
||
/// <param name="remapMin">曲线值 0 对应的实际数值。</param>
|
||
/// <param name="remapMax">曲线值 1 对应的实际数值。</param>
|
||
/// <param name="relativeToInitial">是否在初始值上叠加(而非替换)。</param>
|
||
/// <param name="normalizedTime">归一化时间 [0,1]</param>
|
||
/// <param name="initialValue">初始值(OnStart 时记录)</param>
|
||
/// <returns>映射后的最终数值</returns>
|
||
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>(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<T> 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));
|
||
}
|
||
}
|
||
}
|