狗屎Minimax坏我代码
This commit is contained in:
@@ -1,103 +0,0 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using Lean.Pool;
|
||||
using Sirenix.OdinInspector;
|
||||
|
||||
namespace SLSUtilities.Effects
|
||||
{
|
||||
public class LightGradient : MonoBehaviour, IPoolable
|
||||
{
|
||||
[Required] public Light targetLight;
|
||||
|
||||
[Tooltip("在特效生成时是否立刻启用渐变,注意,必须通过对象池生成。\n为true时,渐变效果从targetLight的默认intensity开始,否则需要调用EnableFade方法启用渐变")]
|
||||
public bool playOnSpawn = true;
|
||||
|
||||
[Tooltip("光源渐变的生命周期")] public float life = 1f;
|
||||
|
||||
[Tooltip("是否应用强度变化")] public bool applyIntensityFade = true;
|
||||
[Tooltip("初始强度")] [ShowIf("applyIntensityFade")] public float initialIntensity = 1;
|
||||
[Tooltip("渐变曲线")] [ShowIf("applyIntensityFade")] public AnimationCurve intensityFadeCurve;
|
||||
|
||||
[Tooltip("是否应用范围变化")] public bool applyRangeFade = false;
|
||||
[Tooltip("初始范围")][ShowIf("applyRangeFade")] public float initialRange = 1;
|
||||
[Tooltip("范围曲线")][ShowIf("applyRangeFade")] public AnimationCurve rangeFadeCurve;
|
||||
|
||||
[Tooltip("是否使用光源颜色渐变")] public bool useLightColorGradient = false;
|
||||
[Tooltip("光源颜色渐变")][ShowIf("useLightColorGradient")] public Gradient lightColorGradient;
|
||||
|
||||
[HideInEditorMode]
|
||||
[SerializeField]
|
||||
private bool isFading;
|
||||
[HideInEditorMode]
|
||||
[SerializeField]
|
||||
private float time;
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
targetLight = GetComponent<Light>();
|
||||
intensityFadeCurve = AnimationCurve.EaseInOut(0, 1, 1, 0);
|
||||
rangeFadeCurve = AnimationCurve.EaseInOut(0, 1, 1, 0);
|
||||
}
|
||||
|
||||
public void OnSpawn()
|
||||
{
|
||||
if (playOnSpawn && !isFading)
|
||||
{
|
||||
EnableFade(initialIntensity, initialRange);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnDespawn()
|
||||
{
|
||||
isFading = false;
|
||||
}
|
||||
|
||||
public void EnableFade(float intensity, float initialRange)
|
||||
{
|
||||
if (targetLight == null) throw new NullReferenceException("Target Light is null");
|
||||
|
||||
time = 0;
|
||||
isFading = true;
|
||||
if (applyIntensityFade)
|
||||
{
|
||||
this.initialIntensity = intensity;
|
||||
targetLight.intensity = intensity;
|
||||
}
|
||||
|
||||
if (applyRangeFade)
|
||||
{
|
||||
this.initialRange = initialRange;
|
||||
targetLight.range = initialRange;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!isFading) return;
|
||||
|
||||
time += Time.deltaTime;
|
||||
|
||||
if (applyIntensityFade)
|
||||
{
|
||||
targetLight.intensity = initialIntensity * intensityFadeCurve.Evaluate(time / life);
|
||||
}
|
||||
|
||||
if (applyRangeFade)
|
||||
{
|
||||
targetLight.range = initialRange * rangeFadeCurve.Evaluate(time / life);
|
||||
}
|
||||
|
||||
if (useLightColorGradient)
|
||||
{
|
||||
targetLight.color = lightColorGradient.Evaluate(time / life);
|
||||
}
|
||||
|
||||
if (time > life)
|
||||
{
|
||||
time = 0;
|
||||
isFading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 24ff3199a15f84e469258023a00cc54f
|
||||
timeCreated: 1493668034
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,138 +0,0 @@
|
||||
using System;
|
||||
using Lean.Pool;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace SLSUtilities.Effects
|
||||
{
|
||||
public class TransformAdjustment : MonoBehaviour, IPoolable
|
||||
{
|
||||
[Required] public Transform targetTransform;
|
||||
|
||||
[Tooltip("在特效生成时是否立刻启用渐变,注意,必须通过对象池生成。\n为true时,渐变效果从默认transform开始,否则需要调用EnableFade方法启用渐变")]
|
||||
public bool playOnSpawn = true;
|
||||
|
||||
[Tooltip("生命周期")] public float life = 1f;
|
||||
|
||||
[Tooltip("是否应用本地位置变化")] public bool applyLocalPosition = true;
|
||||
[Tooltip("初始位置")] [ShowIf("applyLocalPosition")] public Vector3 initialLocalPosition;
|
||||
[Tooltip("目标位置")] [ShowIf("applyLocalPosition")] public Vector3 targetLocalPosition = Vector3.zero;
|
||||
[Tooltip("X位置曲线")] [ShowIf("applyLocalPosition")] public AnimationCurve positionCurveX;
|
||||
[Tooltip("Y位置曲线")] [ShowIf("applyLocalPosition")] public AnimationCurve positionCurveY;
|
||||
[Tooltip("Z位置曲线")] [ShowIf("applyLocalPosition")] public AnimationCurve positionCurveZ;
|
||||
|
||||
[Tooltip("是否应用本地旋转变化")] public bool applyLocalRotation = false;
|
||||
[Tooltip("初始旋转")][ShowIf("applyLocalRotation")]public Vector3 initialEulerAngles;
|
||||
[Tooltip("初始旋转")][ShowIf("applyLocalRotation")] public Vector3 targetEulerAngles = Vector3.zero;
|
||||
[Tooltip("X旋转曲线")][ShowIf("applyLocalRotation")] public AnimationCurve rotationCurveX;
|
||||
[Tooltip("Y旋转曲线")][ShowIf("applyLocalRotation")] public AnimationCurve rotationCurveY;
|
||||
[Tooltip("Z旋转曲线")][ShowIf("applyLocalRotation")] public AnimationCurve rotationCurveZ;
|
||||
|
||||
[Tooltip("是否应用本地缩放变化")] public bool applyLocalScale = false;
|
||||
[Tooltip("初始缩放")][ShowIf("applyLocalScale")] public Vector3 initialLocalScale;
|
||||
[Tooltip("目标缩放")][ShowIf("applyLocalScale")] public Vector3 targetLocalScale = Vector3.one;
|
||||
[Tooltip("X缩放曲线")][ShowIf("applyLocalScale")] public AnimationCurve scaleCurveX;
|
||||
[Tooltip("Y缩放曲线")][ShowIf("applyLocalScale")] public AnimationCurve scaleCurveY;
|
||||
[Tooltip("Z缩放曲线")][ShowIf("applyLocalScale")] public AnimationCurve scaleCurveZ;
|
||||
|
||||
[HideInEditorMode]
|
||||
[SerializeField]
|
||||
private bool isFading;
|
||||
[HideInEditorMode]
|
||||
[SerializeField]
|
||||
private float time;
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
targetTransform = transform;
|
||||
positionCurveX = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
positionCurveY = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
positionCurveZ = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
rotationCurveX = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
rotationCurveY = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
rotationCurveZ = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
scaleCurveX = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
scaleCurveY = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
scaleCurveZ = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
}
|
||||
|
||||
public void OnSpawn()
|
||||
{
|
||||
if (playOnSpawn && !isFading)
|
||||
{
|
||||
Enable(targetLocalPosition, targetEulerAngles, targetLocalScale);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnDespawn()
|
||||
{
|
||||
isFading = false;
|
||||
}
|
||||
|
||||
public void Enable(Vector3 position, Vector3 eulerAngles, Vector3 scale)
|
||||
{
|
||||
if (targetTransform == null) throw new NullReferenceException("Target Transform is null");
|
||||
|
||||
time = 0;
|
||||
isFading = true;
|
||||
if (applyLocalPosition)
|
||||
{
|
||||
this.targetLocalPosition = position;
|
||||
targetTransform.localPosition = initialLocalPosition;
|
||||
}
|
||||
|
||||
if (applyLocalRotation)
|
||||
{
|
||||
this.targetEulerAngles = eulerAngles;
|
||||
targetTransform.localEulerAngles = initialEulerAngles;
|
||||
}
|
||||
|
||||
if (applyLocalScale)
|
||||
{
|
||||
this.targetLocalScale = scale;
|
||||
targetTransform.localScale = initialLocalScale;
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!isFading) return;
|
||||
|
||||
time += Time.deltaTime;
|
||||
|
||||
if (applyLocalPosition)
|
||||
{
|
||||
targetTransform.localPosition = initialLocalPosition + new Vector3(
|
||||
(targetLocalPosition.x - initialLocalPosition.x) * positionCurveX.Evaluate(time / life),
|
||||
(targetLocalPosition.y - initialLocalPosition.y) * positionCurveY.Evaluate(time / life),
|
||||
(targetLocalPosition.z - initialLocalPosition.z) * positionCurveZ.Evaluate(time / life)
|
||||
);
|
||||
}
|
||||
|
||||
if (applyLocalRotation)
|
||||
{
|
||||
targetTransform.localEulerAngles = initialEulerAngles + new Vector3(
|
||||
(targetEulerAngles.x - initialEulerAngles.x) * rotationCurveX.Evaluate(time / life),
|
||||
(targetEulerAngles.y - initialEulerAngles.y) * rotationCurveY.Evaluate(time / life),
|
||||
(targetEulerAngles.z - initialEulerAngles.z) * rotationCurveZ.Evaluate(time / life)
|
||||
);
|
||||
}
|
||||
|
||||
if (applyLocalScale)
|
||||
{
|
||||
targetTransform.localScale = initialLocalScale + new Vector3(
|
||||
(targetLocalScale.x - initialLocalScale.x) * scaleCurveX.Evaluate(time / life),
|
||||
(targetLocalScale.y - initialLocalScale.y) * scaleCurveY.Evaluate(time / life),
|
||||
(targetLocalScale.z - initialLocalScale.z) * scaleCurveZ.Evaluate(time / life)
|
||||
);
|
||||
}
|
||||
|
||||
if (time > life)
|
||||
{
|
||||
time = 0;
|
||||
isFading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7723a076048a1c4e99bd7a77fdc664a
|
||||
@@ -1,140 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Lean.Pool;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSUtilities.Effects
|
||||
{
|
||||
public class VFXRaycastInteraction : MonoBehaviour, IPoolable
|
||||
{
|
||||
[Header("核心设置")]
|
||||
[Tooltip("在特效生成时是否立刻启用渐变,注意,必须通过对象池生成。\n为true时,渐变效果从targetLight的默认intensity开始,否则需要调用EnableFade方法启用渐变")]
|
||||
public bool playOnSpawn = true;
|
||||
public float life = 1f;
|
||||
public List<float> checkPoints;
|
||||
public int currentCheckPointIndex = 0;
|
||||
public bool isEnabling = true;
|
||||
public Transform startPoint; // 刀尖的位置(用于发射射线)
|
||||
public LayerMask collisionLayers; // 地面图层
|
||||
public float rayLength = 0.5f; // 射线长度(稍微比刀刃离地距离长一点)
|
||||
|
||||
[Header("特效资源")]
|
||||
public GameObject sparkPrefab; // 火星特效 Prefab
|
||||
public GameObject decalPrefab; // 划痕 Decal Prefab
|
||||
|
||||
[HideInEditorMode]
|
||||
[SerializeField]
|
||||
private bool isFading;
|
||||
[HideInEditorMode]
|
||||
[SerializeField]
|
||||
private float time;
|
||||
private Vector3 _lastTipPosition;
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
startPoint = transform;
|
||||
collisionLayers = LayerMask.GetMask("Wall", "Ground");
|
||||
}
|
||||
|
||||
public void OnSpawn()
|
||||
{
|
||||
if (playOnSpawn && !isEnabling)
|
||||
{
|
||||
isEnabling = true;
|
||||
time = 0f;
|
||||
currentCheckPointIndex = 0;
|
||||
}
|
||||
|
||||
_lastTipPosition = startPoint.position;
|
||||
}
|
||||
|
||||
public void OnDespawn()
|
||||
{
|
||||
isEnabling = false;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
time += Time.deltaTime;
|
||||
if (time >= life)
|
||||
{
|
||||
isEnabling = false;
|
||||
}
|
||||
|
||||
if (!isEnabling || Time.timeScale == 0 || checkPoints.Count == 0) return;
|
||||
|
||||
// 1. 计算刀尖速度方向(用于决定划痕朝向)
|
||||
Vector3 velocity = (startPoint.position - _lastTipPosition) / Time.deltaTime;
|
||||
_lastTipPosition = startPoint.position;
|
||||
|
||||
//1. 检查是否到达下一个检查点
|
||||
if (currentCheckPointIndex < checkPoints.Count)
|
||||
{
|
||||
if (time >= checkPoints[currentCheckPointIndex])
|
||||
{
|
||||
currentCheckPointIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
return; // 未到达检查点,跳过本次更新
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return; // 所有检查点已处理完,跳过本次更新
|
||||
}
|
||||
|
||||
|
||||
// 2. 发射射线检测地面
|
||||
// 这里我们向下发射射线。如果你的游戏支持墙壁划痕,可以改为 velocity.normalized
|
||||
|
||||
RaycastHit hit;
|
||||
if (Physics.Raycast(startPoint.position, startPoint.forward, out hit, rayLength, collisionLayers))
|
||||
{
|
||||
SpawnEffects(hit, velocity);
|
||||
}
|
||||
}
|
||||
|
||||
void SpawnEffects(RaycastHit hit, Vector3 slashVelocity)
|
||||
{
|
||||
Vector3 projectedSlashDir = Vector3.ProjectOnPlane(slashVelocity, hit.normal).normalized;
|
||||
|
||||
// --- 处理火星 (Spark) ---
|
||||
if (sparkPrefab != null)
|
||||
{
|
||||
// 关键点:使用 Quaternion.LookRotation(hit.normal)
|
||||
// 这会让火星特效的 Z轴(发射方向)严格对准地面法线(垂直向上),right方向和挥动方向对齐
|
||||
LeanPool.Spawn(sparkPrefab, hit.point, Quaternion.LookRotation(hit.normal, Vector3.Cross(projectedSlashDir, hit.normal)));
|
||||
}
|
||||
|
||||
// --- 处理划痕 (Decal) ---
|
||||
if (decalPrefab != null)
|
||||
{
|
||||
// 计算划痕的朝向:
|
||||
// 我们希望划痕贴在地面上(法线对齐 hit.normal)
|
||||
// 同时划痕的延伸方向要对齐刀的挥动方向(slashVelocity)
|
||||
if (projectedSlashDir != Vector3.zero)
|
||||
{
|
||||
// 计算 Decal 的旋转,Decal的forward和地面法线对齐,right方向和挥动方向对齐
|
||||
Quaternion decalRotation = Quaternion.LookRotation(hit.normal, Vector3.Cross(projectedSlashDir, hit.normal));
|
||||
|
||||
// 生成 Decal,稍微抬高一点点避免 Z-Fighting
|
||||
GameObject decal = LeanPool.Spawn(decalPrefab, hit.point + hit.normal * 0.01f, decalRotation);
|
||||
|
||||
// 记得在 Prefab 里设置自动销毁,或者在这里写 Destroy
|
||||
LeanPool.Despawn(decal, 5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 用于在 Scene 窗口调试射线,方便你调整 rayLength
|
||||
void OnDrawGizmos()
|
||||
{
|
||||
if (startPoint != null)
|
||||
{
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawLine(startPoint.position, startPoint.position + transform.forward * rayLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0217139a1a991f947ac6135f8ea20a0c
|
||||
@@ -16,6 +16,7 @@ namespace SLSUtilities.Feedback
|
||||
/// </summary>
|
||||
[Title("Curve Shake")]
|
||||
[LabelText("Shake Curve")]
|
||||
[ShakeCurvePreset]
|
||||
public AnimationCurve shakeCurve = new AnimationCurve(
|
||||
new Keyframe(0f, 0f),
|
||||
new Keyframe(0.5f, 1f),
|
||||
@@ -40,24 +41,10 @@ namespace SLSUtilities.Feedback
|
||||
[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)
|
||||
|
||||
protected virtual 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;
|
||||
return base.EvaluateShake(shakeCurve, remapMin, remapMax, relativeToInitial, normalizedTime, initialValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,16 @@ namespace SLSUtilities.Feedback
|
||||
/// Clip 总时长(秒)。
|
||||
/// </summary>
|
||||
public float duration;
|
||||
|
||||
/// <summary>
|
||||
/// 当前 Clip 的综合时间缩放系数(含 Global/Group/Local),由 FeedbackPlayer 每帧动态计算。
|
||||
/// </summary>
|
||||
public float timeScale;
|
||||
|
||||
/// <summary>
|
||||
/// 当前 Clip 是否动态获取当前的时间缩放
|
||||
/// </summary>
|
||||
public FeedbackTimeSettings timeSettings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -48,6 +58,11 @@ namespace SLSUtilities.Feedback
|
||||
/// </summary>
|
||||
public virtual string DisplayName => GetType().Name;
|
||||
|
||||
/// <summary>
|
||||
/// 是否忽略时间缩放。如果为true,此Action将使用原始deltaTime,不受TimeScale影响。
|
||||
/// </summary>
|
||||
public virtual bool IgnoreTimeScale => false;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化,FeedbackPlayer 开始播放此 Clip 时调用。
|
||||
/// </summary>
|
||||
@@ -81,5 +96,30 @@ namespace SLSUtilities.Feedback
|
||||
/// 用于 Editor 预览(Runtime 也可用)。
|
||||
/// </summary>
|
||||
public virtual void Preview() { }
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -11,17 +11,21 @@ namespace SLSUtilities.Feedback
|
||||
[Serializable]
|
||||
public class FeedbackClip
|
||||
{
|
||||
public string clipName;
|
||||
|
||||
/// <summary>
|
||||
/// 片段开始时间(秒)。
|
||||
/// </summary>
|
||||
[MinValue(0f)]
|
||||
[HorizontalGroup("Time"), LabelWidth(60)]
|
||||
public float startTime;
|
||||
|
||||
/// <summary>
|
||||
/// 片段持续时间(秒)。
|
||||
/// </summary>
|
||||
[MinValue(0.01f)]
|
||||
public float duration = 0.1f;
|
||||
[HorizontalGroup("Time"), LabelWidth(60)]
|
||||
public float duration = 0.2f;
|
||||
|
||||
/// <summary>
|
||||
/// 片段结束时间(秒)。
|
||||
@@ -31,7 +35,7 @@ namespace SLSUtilities.Feedback
|
||||
/// <summary>
|
||||
/// 是否覆盖 FeedbackData 的时间设置。
|
||||
/// </summary>
|
||||
[Title("Time Override")]
|
||||
[LabelWidth(150)]
|
||||
public bool overrideTimeSettings;
|
||||
|
||||
/// <summary>
|
||||
@@ -43,7 +47,7 @@ namespace SLSUtilities.Feedback
|
||||
/// <summary>
|
||||
/// 具体反馈动作,Odin 自动显示多态类型选择器。
|
||||
/// </summary>
|
||||
[Title("Action"), SerializeReference]
|
||||
[SerializeReference]
|
||||
public FeedbackActionBase action;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Sirenix.OdinInspector;
|
||||
using UniRx;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSUtilities.Feedback
|
||||
@@ -10,7 +12,7 @@ namespace SLSUtilities.Feedback
|
||||
/// 包含多条轨道(Track),每条轨道包含按时间排列的片段(Clip)。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "NewFeedbackData", menuName = "SLS/Feedback/FeedbackData")]
|
||||
public class FeedbackData : SerializedScriptableObject
|
||||
public partial class FeedbackData : SerializedScriptableObject
|
||||
{
|
||||
/// <summary>
|
||||
/// 父级集合引用,由 FeedbackDataCollection 自动维护。
|
||||
@@ -27,7 +29,6 @@ namespace SLSUtilities.Feedback
|
||||
/// <summary>
|
||||
/// 全局默认的时间设置。Clip 可选择覆盖此设置。
|
||||
/// </summary>
|
||||
[Title("Time Settings (Default)")]
|
||||
public FeedbackTimeSettings defaultTimeSettings = new FeedbackTimeSettings();
|
||||
|
||||
/// <summary>
|
||||
@@ -67,4 +68,47 @@ namespace SLSUtilities.Feedback
|
||||
Debug.Log($"[FeedbackData] Previewing '{feedbackName}' (Duration: {TotalDuration:F2}s)");
|
||||
}
|
||||
}
|
||||
|
||||
public partial class FeedbackData
|
||||
{
|
||||
public FeedbackTrack Track(string name)
|
||||
{
|
||||
FeedbackTrack track = tracks.FirstOrDefault(t => t.trackName == name);
|
||||
if (track == null)
|
||||
{
|
||||
Debug.LogWarning($"[FeedbackData] Track '{name}' not found in FeedbackData '{feedbackName}'.");
|
||||
}
|
||||
return track;
|
||||
}
|
||||
|
||||
public FeedbackClip Clip(string trackName, Func<FeedbackClip, bool> predicate)
|
||||
{
|
||||
FeedbackTrack track = Track(trackName);
|
||||
if (track == null) return null;
|
||||
|
||||
FeedbackClip clip = track.clips.FirstOrDefault(predicate);
|
||||
if (clip == null)
|
||||
{
|
||||
Debug.LogWarning($"[FeedbackData] Clip matching predicate not found in Track '{trackName}' of FeedbackData '{feedbackName}'.");
|
||||
}
|
||||
return clip;
|
||||
}
|
||||
|
||||
public FeedbackClip Clip<T>(string trackName) where T : FeedbackActionBase
|
||||
{
|
||||
return Clip(trackName, c => c.action.GetType() == typeof(T));
|
||||
}
|
||||
|
||||
public FeedbackClip Clip(string trackName, string clipName)
|
||||
{
|
||||
return Clip(trackName, c => c.clipName == clipName);
|
||||
}
|
||||
|
||||
public T Action<T>(string trackName) where T : FeedbackActionBase
|
||||
{
|
||||
FeedbackTrack track = Track(trackName);
|
||||
FeedbackClip clip = track?.clips.FirstOrDefault(c => c.action.GetType() == typeof(T));
|
||||
return clip?.action as T;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace SLSUtilities.Feedback
|
||||
{
|
||||
@@ -9,26 +11,26 @@ namespace SLSUtilities.Feedback
|
||||
/// Clip 级设置如果 useTimeScale = true 则覆盖 Data 级设置。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class FeedbackTimeSettings
|
||||
public partial class FeedbackTimeSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否使用时间缩放。默认 false 表示不受任何时间缩放影响。
|
||||
/// </summary>
|
||||
public bool useTimeScale;
|
||||
[EnumButtons]
|
||||
public TimeScaleType timeScaleType = TimeScaleType.Global;
|
||||
|
||||
[FormerlySerializedAs("useDynamicTimeScale")]
|
||||
[Tooltip("是否动态获取当前的时间缩放。启用后,FeedbackPlayer 每帧计算当前综合时间缩放系数并传递给 FeedbackAction。")]
|
||||
[HideIf("timeScaleType", TimeScaleType.Unscaled)]
|
||||
[LabelText("Apply Dynamic")]
|
||||
public bool applyDynamicTimeScale = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 受 TimeManager.globalTimeScale 影响。
|
||||
/// </summary>
|
||||
public bool affectedByGlobalTimeScale;
|
||||
|
||||
/// <summary>
|
||||
/// 受 TimeManager 的分组时间影响(player/enemy 等)。
|
||||
/// </summary>
|
||||
public bool affectedByGroupTimeScale;
|
||||
|
||||
/// <summary>
|
||||
/// 受角色本地 localTimeScale 影响。
|
||||
/// </summary>
|
||||
public bool affectedByLocalTimeScale;
|
||||
public partial class FeedbackTimeSettings
|
||||
{
|
||||
public enum TimeScaleType
|
||||
{
|
||||
Unscaled = 0,
|
||||
Global = 1,
|
||||
Group = 2,
|
||||
Local = 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,13 @@ namespace SLSUtilities.Feedback
|
||||
/// 多个 Track 天然并行播放,Track 内的 Clip 按时间顺序排列,不重叠。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class FeedbackTrack
|
||||
public partial class FeedbackTrack
|
||||
{
|
||||
/// <summary>
|
||||
/// 轨道名称,用于调试和 Inspector 显示。
|
||||
/// </summary>
|
||||
[LabelText("Track Name")]
|
||||
[ValueDropdown("GetTrackNamesList")]
|
||||
public string trackName = "New Track";
|
||||
|
||||
/// <summary>
|
||||
@@ -43,4 +44,15 @@ namespace SLSUtilities.Feedback
|
||||
/// </summary>
|
||||
public float TotalDuration => clips.Count > 0 ? clips.Max(c => c.EndTime) : 0f;
|
||||
}
|
||||
|
||||
public partial class FeedbackTrack
|
||||
{
|
||||
private List<string> GetTrackNamesList()
|
||||
{
|
||||
return new List<string>()
|
||||
{
|
||||
"Camera", "Time", "Postprocessing", "Audio"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,14 @@ namespace SLSUtilities.Feedback
|
||||
float LocalTimeScale { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 根据时间设置计算实际 deltaTime。
|
||||
/// 根据时间设置计算实际 deltaTime(秒)。
|
||||
/// </summary>
|
||||
float GetDeltaTime(FeedbackTimeSettings settings);
|
||||
|
||||
/// <summary>
|
||||
/// 根据时间设置计算综合时间缩放系数(无 deltaTime 乘入)。
|
||||
/// 返回 1.0 表示正常速度。
|
||||
/// </summary>
|
||||
float GetTimeScale(FeedbackTimeSettings settings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace SLSUtilities.Feedback
|
||||
{
|
||||
/// <summary>
|
||||
/// 标记 AnimationCurve 字段,使其在 Inspector 中显示震动曲线预设按钮组。
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
|
||||
public class ShakeCurvePresetAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d647fdd1e522b249b302cc16ccc365d
|
||||
177
Assets/Scripts/SLSUtilities/Feedback/Base/ShakeCurvePresets.cs
Normal file
177
Assets/Scripts/SLSUtilities/Feedback/Base/ShakeCurvePresets.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSUtilities.Feedback
|
||||
{
|
||||
/// <summary>
|
||||
/// 震动曲线预设集合。每个方法返回一条全新的 AnimationCurve 实例。
|
||||
/// 所有曲线的 X 轴为归一化时间 [0, 1],Y 轴为强度系数。
|
||||
/// </summary>
|
||||
public static class ShakeCurvePresets
|
||||
{
|
||||
/// <summary>
|
||||
/// 预设名 → 工厂方法。
|
||||
/// </summary>
|
||||
public static readonly (string name, System.Func<AnimationCurve> factory)[] All = new[]
|
||||
{
|
||||
("Impact", (System.Func<AnimationCurve>)QuickImpact),
|
||||
("Punch", (System.Func<AnimationCurve>)Punch),
|
||||
("Bump", (System.Func<AnimationCurve>)SmoothBump),
|
||||
("Fade Out", (System.Func<AnimationCurve>)FadeOut),
|
||||
("Oscillate", (System.Func<AnimationCurve>)Oscillate),
|
||||
("Dense Osc.", (System.Func<AnimationCurve>)DenseOscillate),
|
||||
("Anticipation", (System.Func<AnimationCurve>)Anticipation),
|
||||
("Recoil", (System.Func<AnimationCurve>)Recoil),
|
||||
("Double Hit", (System.Func<AnimationCurve>)DoubleHit),
|
||||
("Flash", (System.Func<AnimationCurve>)Flash),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 快速冲击 — 峰值出现在 10~15%,之后快速衰减至零。
|
||||
/// 适合:打击确认、子弹命中、轻攻击。
|
||||
/// </summary>
|
||||
public static AnimationCurve QuickImpact()
|
||||
{
|
||||
return new AnimationCurve(
|
||||
new Keyframe(0f, 0f, 0f, 8f),
|
||||
new Keyframe(0.12f, 1f, 0f, 0f),
|
||||
new Keyframe(0.5f, 0.12f, -0.6f, -0.3f),
|
||||
new Keyframe(1f, 0f, -0.1f, 0f)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重拳 — 第 1 帧即达峰值,线性衰减至零。
|
||||
/// 适合:重击、格挡冲击、爆炸瞬间。
|
||||
/// </summary>
|
||||
public static AnimationCurve Punch()
|
||||
{
|
||||
return new AnimationCurve(
|
||||
new Keyframe(0f, 0f, 0f, float.PositiveInfinity),
|
||||
new Keyframe(0.02f, 1f, 0f, -1.02f),
|
||||
new Keyframe(1f, 0f, -1.02f, 0f)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 平滑铃形 — 对称的升/降曲线(EaseInOut)。
|
||||
/// 适合:脚步震动、跳跃落地、节奏性效果。
|
||||
/// </summary>
|
||||
public static AnimationCurve SmoothBump()
|
||||
{
|
||||
return new AnimationCurve(
|
||||
new Keyframe(0f, 0f, 0f, 0f),
|
||||
new Keyframe(0.5f, 1f, 0f, 0f),
|
||||
new Keyframe(1f, 0f, 0f, 0f)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 渐弱 — 起始即为峰值,EaseOut 衰减至零。
|
||||
/// 适合:爆炸余波、效果消散、技能冷却过渡。
|
||||
/// </summary>
|
||||
public static AnimationCurve FadeOut()
|
||||
{
|
||||
return new AnimationCurve(
|
||||
new Keyframe(0f, 1f, 0f, 0f),
|
||||
new Keyframe(1f, 0f, -2f, 0f)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 衰减振荡 — 2~3 个波峰逐渐衰减。
|
||||
/// 适合:爆炸余震、碰撞后振荡、弹跳。
|
||||
/// </summary>
|
||||
public static AnimationCurve Oscillate()
|
||||
{
|
||||
return new AnimationCurve(
|
||||
new Keyframe(0f, 0f, 0f, 12f),
|
||||
new Keyframe(0.08f, 1f, 0f, 0f),
|
||||
new Keyframe(0.24f, -0.55f, 0f, 0f),
|
||||
new Keyframe(0.42f, 0.3f, 0f, 0f),
|
||||
new Keyframe(0.62f, -0.12f, 0f, 0f),
|
||||
new Keyframe(1f, 0f, 0f, 0f)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 密集衰减振荡 — 5~6 个波峰,频率更高,适合更猛烈的效果。
|
||||
/// 适合:大型爆炸持续余震、引擎振动、电击。
|
||||
/// </summary>
|
||||
public static AnimationCurve DenseOscillate()
|
||||
{
|
||||
return new AnimationCurve(
|
||||
new Keyframe(0f, 0f, 0f, 20f),
|
||||
new Keyframe(0.05f, 1f, 0f, 0f),
|
||||
new Keyframe(0.13f, -0.72f, 0f, 0f),
|
||||
new Keyframe(0.21f, 0.52f, 0f, 0f),
|
||||
new Keyframe(0.29f, -0.36f, 0f, 0f),
|
||||
new Keyframe(0.38f, 0.24f, 0f, 0f),
|
||||
new Keyframe(0.48f, -0.15f, 0f, 0f),
|
||||
new Keyframe(0.60f, 0.08f, 0f, 0f),
|
||||
new Keyframe(0.75f, -0.03f, 0f, 0f),
|
||||
new Keyframe(1f, 0f, 0f, 0f)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 预兆 + 冲击 — 先小幅反向蓄力,再大幅正向爆发。
|
||||
/// 适合:重型近战预备 + 冲击,与动画预备帧配合。
|
||||
/// </summary>
|
||||
public static AnimationCurve Anticipation()
|
||||
{
|
||||
return new AnimationCurve(
|
||||
new Keyframe(0f, 0f, 0f, 0f),
|
||||
new Keyframe(0.18f, -0.2f, 0f, 0f),
|
||||
new Keyframe(0.32f, 0f, 0f, 4f),
|
||||
new Keyframe(0.5f, 1f, 0f, 0f),
|
||||
new Keyframe(1f, 0f, -1.2f, 0f)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 后坐/反弹 — 快速达到峰值后超调回弹,逐渐收敛。
|
||||
/// 适合:枪械后坐力、刀击挥出后的回摆。
|
||||
/// </summary>
|
||||
public static AnimationCurve Recoil()
|
||||
{
|
||||
return new AnimationCurve(
|
||||
new Keyframe(0f, 0f, 0f, 10f),
|
||||
new Keyframe(0.1f, 1f, 0f, 0f),
|
||||
new Keyframe(0.32f, -0.3f, 0f, 0f),
|
||||
new Keyframe(0.52f, 0.12f, 0f, 0f),
|
||||
new Keyframe(0.72f, -0.04f, 0f, 0f),
|
||||
new Keyframe(1f, 0f, 0f, 0f)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 双击峰 — 两个依次递减的波峰。
|
||||
/// 适合:连击、二段攻击、双弹命中。
|
||||
/// </summary>
|
||||
public static AnimationCurve DoubleHit()
|
||||
{
|
||||
return new AnimationCurve(
|
||||
new Keyframe(0f, 0f, 0f, 8f),
|
||||
new Keyframe(0.12f, 1f, 0f, 0f),
|
||||
new Keyframe(0.32f, 0.1f, 0f, 0f),
|
||||
new Keyframe(0.52f, 0.7f, 0f, 0f),
|
||||
new Keyframe(1f, 0f, -0.6f, 0f)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 方波闪光 — 近乎即时上升/下降,中间保持峰值。
|
||||
/// 适合:描边闪光、即时视觉强调、全屏闪白。
|
||||
/// </summary>
|
||||
public static AnimationCurve Flash()
|
||||
{
|
||||
// 使用极陡切线模拟方波:在极短时间内完成升/降
|
||||
return new AnimationCurve(
|
||||
new Keyframe(0f, 0f, 0f, float.PositiveInfinity),
|
||||
new Keyframe(0.02f, 1f, float.PositiveInfinity, 0f),
|
||||
new Keyframe(0.98f, 1f, 0f, float.NegativeInfinity),
|
||||
new Keyframe(1f, 0f, float.NegativeInfinity, 0f)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 260284d08797fb74d9922b06fd61db55
|
||||
@@ -0,0 +1,34 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSUtilities.Feedback
|
||||
{
|
||||
/// <summary>
|
||||
/// 后处理震动实例的运行时状态。
|
||||
/// 由 Shaker 维护,支持多个实例的叠加混合。
|
||||
/// </summary>
|
||||
public class ShakeInstanceBase
|
||||
{
|
||||
public FeedbackTimeSettings timeSettings;
|
||||
public IFeedbackTimeProvider timeProvider;
|
||||
public float timer;
|
||||
public float duration;
|
||||
|
||||
public ShakeInstanceBase(FeedbackTimeSettings timeSettings, IFeedbackTimeProvider timeProvider, float duration)
|
||||
{
|
||||
this.timeSettings = timeSettings;
|
||||
this.timeProvider = timeProvider;
|
||||
this.duration = duration;
|
||||
timer = 0f;
|
||||
}
|
||||
|
||||
public void Tick()
|
||||
{
|
||||
timer += timeProvider.GetDeltaTime(timeSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前震动是否已结束。
|
||||
/// </summary>
|
||||
public bool IsFinished => timer >= duration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0e5361f026a60804a9f494f514e810cb
|
||||
@@ -0,0 +1,956 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using Sirenix.OdinInspector;
|
||||
using Sirenix.OdinInspector.Editor;
|
||||
using Sirenix.Utilities.Editor;
|
||||
using UnityEditor.Callbacks;
|
||||
|
||||
namespace SLSUtilities.Feedback.Editor
|
||||
{
|
||||
public class FeedbackDataEditorWindow : OdinEditorWindow
|
||||
{
|
||||
// ─────────────── 常量 ───────────────
|
||||
|
||||
private const float RULER_HEIGHT = 22f;
|
||||
private const float LANE_HEIGHT = 24f;
|
||||
private const float TRACK_PADDING = 2f;
|
||||
private const float TRACK_LABEL_WIDTH = 140f;
|
||||
private const float EXPAND_BUTTON_SIZE = 14f;
|
||||
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 readonly HashSet<int> _expandedTracks = new HashSet<int>();
|
||||
|
||||
private struct TrackLayout
|
||||
{
|
||||
public float yOffset;
|
||||
public float totalHeight;
|
||||
public int laneCount; // 当前显示的 lane 数(折叠时为 1)
|
||||
public int naturalLaneCount; // 实际需要的 lane 数(用于决定是否显示展开按钮)
|
||||
public int[] clipLanes;
|
||||
}
|
||||
|
||||
private TrackLayout[] _trackLayouts;
|
||||
|
||||
// ─────────────── 颜色 ───────────────
|
||||
|
||||
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 ExpandButtonColor = new Color(0.6f, 0.6f, 0.6f);
|
||||
private static readonly Color LaneSeparatorColor = new Color(0.3f, 0.3f, 0.3f, 0.3f);
|
||||
|
||||
// ─────────────── Label 宽度 ───────────────
|
||||
|
||||
private const float INSPECTOR_LABEL_WIDTH = 155f;
|
||||
|
||||
/// <summary>
|
||||
/// 包装每个 Editor 的绘制,使用 Odin 的 GUIHelper 栈式 Label 宽度,
|
||||
/// 确保 InlineEditor 内部也能生效。
|
||||
/// </summary>
|
||||
protected override void DrawEditor(int index)
|
||||
{
|
||||
GUIHelper.PushLabelWidth(INSPECTOR_LABEL_WIDTH);
|
||||
base.DrawEditor(index);
|
||||
GUIHelper.PopLabelWidth();
|
||||
}
|
||||
|
||||
// ─────────────── 窗口入口 ───────────────
|
||||
|
||||
/// <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.EntityIdToObject(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;
|
||||
_expandedTracks.Clear();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 轨道布局计算
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// 重新计算所有轨道的布局(lane 分配、高度、Y 偏移)。
|
||||
/// </summary>
|
||||
private void ComputeTrackLayouts()
|
||||
{
|
||||
if (targetData?.tracks == null || targetData.tracks.Count == 0)
|
||||
{
|
||||
_trackLayouts = Array.Empty<TrackLayout>();
|
||||
return;
|
||||
}
|
||||
|
||||
int numTracks = targetData.tracks.Count;
|
||||
_trackLayouts = new TrackLayout[numTracks];
|
||||
float currentY = 0;
|
||||
|
||||
for (int i = 0; i < numTracks; i++)
|
||||
{
|
||||
FeedbackTrack track = targetData.tracks[i];
|
||||
bool isExpanded = _expandedTracks.Contains(i);
|
||||
int[] clipLanes = AssignClipLanes(track);
|
||||
|
||||
// 始终计算实际所需 lane 数,用于决定是否显示展开按钮
|
||||
int naturalLaneCount = (clipLanes != null && clipLanes.Length > 0)
|
||||
? clipLanes.Max() + 1
|
||||
: 1;
|
||||
|
||||
// 显示的 lane 数:折叠时固定为 1,展开时用实际值
|
||||
int displayLaneCount = isExpanded ? naturalLaneCount : 1;
|
||||
|
||||
float height = displayLaneCount * LANE_HEIGHT + TRACK_PADDING * 2;
|
||||
|
||||
_trackLayouts[i] = new TrackLayout
|
||||
{
|
||||
yOffset = currentY,
|
||||
totalHeight = height,
|
||||
laneCount = displayLaneCount,
|
||||
naturalLaneCount = naturalLaneCount,
|
||||
clipLanes = clipLanes
|
||||
};
|
||||
|
||||
currentY += height;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为一条轨道中的 Clips 分配 lane,使重叠的 Clip 位于不同 lane。
|
||||
/// 贪心算法:按 startTime 排序,依次放入第一个无冲突的 lane。
|
||||
/// </summary>
|
||||
private int[] AssignClipLanes(FeedbackTrack track)
|
||||
{
|
||||
if (track?.clips == null || track.clips.Count == 0) return Array.Empty<int>();
|
||||
|
||||
int clipCount = track.clips.Count;
|
||||
int[] lanes = new int[clipCount];
|
||||
|
||||
int[] sortedIndices = Enumerable.Range(0, clipCount)
|
||||
.OrderBy(idx => track.clips[idx]?.startTime ?? 0)
|
||||
.ToArray();
|
||||
|
||||
List<float> laneEndTimes = new List<float>();
|
||||
|
||||
foreach (int idx in sortedIndices)
|
||||
{
|
||||
FeedbackClip clip = track.clips[idx];
|
||||
if (clip == null) { lanes[idx] = 0; continue; }
|
||||
|
||||
float clipStart = clip.startTime;
|
||||
int assignedLane = -1;
|
||||
|
||||
for (int lane = 0; lane < laneEndTimes.Count; lane++)
|
||||
{
|
||||
if (clipStart >= laneEndTimes[lane])
|
||||
{
|
||||
assignedLane = lane;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (assignedLane == -1)
|
||||
{
|
||||
assignedLane = laneEndTimes.Count;
|
||||
laneEndTimes.Add(0);
|
||||
}
|
||||
|
||||
lanes[idx] = assignedLane;
|
||||
laneEndTimes[assignedLane] = clip.EndTime;
|
||||
}
|
||||
|
||||
return lanes;
|
||||
}
|
||||
|
||||
private float GetTotalTracksHeight()
|
||||
{
|
||||
if (_trackLayouts == null || _trackLayouts.Length == 0)
|
||||
return LANE_HEIGHT + TRACK_PADDING * 2;
|
||||
|
||||
var last = _trackLayouts[^1];
|
||||
return last.yOffset + last.totalHeight;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 时间轴 GUI 主入口
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
[OnInspectorGUI]
|
||||
[PropertyOrder(-98)]
|
||||
[ShowIf("targetData")]
|
||||
private void DrawTimelineGUI()
|
||||
{
|
||||
if (targetData == null) return;
|
||||
if (autoFitDuration) FitViewDuration();
|
||||
|
||||
ComputeTrackLayouts();
|
||||
|
||||
float tracksHeight = GetTotalTracksHeight();
|
||||
float totalHeight = RULER_HEIGHT + tracksHeight;
|
||||
|
||||
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,
|
||||
tracksHeight
|
||||
);
|
||||
|
||||
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)
|
||||
{
|
||||
GUI.Label(allTracksArea, "No tracks. Add tracks in the inspector below.",
|
||||
EditorStyles.centeredGreyMiniLabel);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int trackIdx = 0; trackIdx < targetData.tracks.Count; trackIdx++)
|
||||
{
|
||||
if (trackIdx >= _trackLayouts.Length) break;
|
||||
|
||||
FeedbackTrack track = targetData.tracks[trackIdx];
|
||||
ref TrackLayout layout = ref _trackLayouts[trackIdx];
|
||||
bool isExpanded = _expandedTracks.Contains(trackIdx);
|
||||
|
||||
float trackY = allTracksArea.y + layout.yOffset;
|
||||
Rect labelRect = new Rect(allTracksArea.x, trackY, TRACK_LABEL_WIDTH, layout.totalHeight);
|
||||
Rect contentRect = new Rect(
|
||||
allTracksArea.x + TRACK_LABEL_WIDTH,
|
||||
trackY,
|
||||
allTracksArea.width - TRACK_LABEL_WIDTH,
|
||||
layout.totalHeight
|
||||
);
|
||||
|
||||
// 轨道背景
|
||||
Color bgColor = (trackIdx % 2 == 0) ? TrackBackgroundColor : TrackAltBackgroundColor;
|
||||
EditorGUI.DrawRect(contentRect, bgColor);
|
||||
|
||||
// 展开时绘制 lane 分隔线
|
||||
if (isExpanded && layout.laneCount > 1)
|
||||
{
|
||||
Handles.color = LaneSeparatorColor;
|
||||
for (int lane = 1; lane < layout.laneCount; lane++)
|
||||
{
|
||||
float lineY = trackY + TRACK_PADDING + lane * LANE_HEIGHT;
|
||||
Handles.DrawLine(
|
||||
new Vector3(contentRect.x, lineY),
|
||||
new Vector3(contentRect.xMax, lineY)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 轨道标签
|
||||
DrawTrackLabel(labelRect, track, trackIdx, isExpanded, layout.laneCount);
|
||||
|
||||
// Clips
|
||||
if (track.clips != null)
|
||||
{
|
||||
for (int clipIdx = 0; clipIdx < track.clips.Count; clipIdx++)
|
||||
{
|
||||
int lane = 0;
|
||||
if (isExpanded && layout.clipLanes != null && clipIdx < layout.clipLanes.Length)
|
||||
lane = layout.clipLanes[clipIdx];
|
||||
|
||||
DrawClip(contentRect, track.clips[clipIdx], trackIdx, clipIdx, duration, lane, isExpanded);
|
||||
}
|
||||
}
|
||||
|
||||
// Mute 叠加层
|
||||
if (track.mute)
|
||||
{
|
||||
EditorGUI.DrawRect(contentRect, MutedOverlayColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绘制轨道标签区域(名称 + mute/solo 指示 + 展开按钮)。
|
||||
/// </summary>
|
||||
private void DrawTrackLabel(Rect labelRect, FeedbackTrack track, int trackIdx, bool isExpanded, int laneCount)
|
||||
{
|
||||
EditorGUI.DrawRect(labelRect, RulerBackgroundColor);
|
||||
|
||||
if (track.solo)
|
||||
{
|
||||
EditorGUI.DrawRect(new Rect(labelRect.x, labelRect.y, 3f, labelRect.height), SoloIndicatorColor);
|
||||
}
|
||||
|
||||
string prefix = track.mute ? "[M] " : track.solo ? "[S] " : "";
|
||||
string displayName = string.IsNullOrEmpty(track.trackName) ? $"Track {trackIdx}" : track.trackName;
|
||||
Color textColor = track.mute ? new Color(0.5f, 0.5f, 0.5f) : new Color(0.85f, 0.85f, 0.85f);
|
||||
|
||||
GUIStyle labelStyle = new GUIStyle(EditorStyles.label)
|
||||
{
|
||||
fontSize = 11,
|
||||
padding = new RectOffset(8, 0, 0, 0),
|
||||
normal = { textColor = textColor }
|
||||
};
|
||||
|
||||
float firstLaneH = LANE_HEIGHT + TRACK_PADDING * 2;
|
||||
Rect nameLabelRect = new Rect(labelRect.x, labelRect.y, labelRect.width - EXPAND_BUTTON_SIZE - 6f, firstLaneH);
|
||||
EditorGUI.LabelField(nameLabelRect, $"{prefix}{displayName}", labelStyle);
|
||||
|
||||
// 使用 naturalLaneCount 判断是否需要显示展开按钮
|
||||
ref TrackLayout layout = ref _trackLayouts[trackIdx];
|
||||
if (layout.naturalLaneCount > 1 || isExpanded)
|
||||
{
|
||||
DrawExpandToggle(labelRect, trackIdx, isExpanded, layout.naturalLaneCount);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绘制展开/折叠三角按钮。
|
||||
/// </summary>
|
||||
private void DrawExpandToggle(Rect labelRect, int trackIdx, bool isExpanded, int laneCount)
|
||||
{
|
||||
float firstLaneH = LANE_HEIGHT + TRACK_PADDING * 2;
|
||||
float btnX = labelRect.xMax - EXPAND_BUTTON_SIZE - 4f;
|
||||
float btnY = labelRect.y + (firstLaneH - EXPAND_BUTTON_SIZE) * 0.5f;
|
||||
Rect btnRect = new Rect(btnX, btnY, EXPAND_BUTTON_SIZE, EXPAND_BUTTON_SIZE);
|
||||
|
||||
EditorGUI.DrawRect(btnRect, new Color(0.25f, 0.25f, 0.25f, 0.8f));
|
||||
|
||||
Vector3 center = btnRect.center;
|
||||
float halfSize = EXPAND_BUTTON_SIZE * 0.25f;
|
||||
Handles.color = isExpanded ? SoloIndicatorColor : ExpandButtonColor;
|
||||
|
||||
if (isExpanded)
|
||||
{
|
||||
Handles.DrawAAConvexPolygon(
|
||||
new Vector3(center.x - halfSize, center.y - halfSize * 0.5f, 0),
|
||||
new Vector3(center.x + halfSize, center.y - halfSize * 0.5f, 0),
|
||||
new Vector3(center.x, center.y + halfSize * 0.5f, 0)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
Handles.DrawAAConvexPolygon(
|
||||
new Vector3(center.x - halfSize * 0.5f, center.y - halfSize, 0),
|
||||
new Vector3(center.x + halfSize * 0.5f, center.y, 0),
|
||||
new Vector3(center.x - halfSize * 0.5f, center.y + halfSize, 0)
|
||||
);
|
||||
}
|
||||
|
||||
// lane 数量角标
|
||||
GUIStyle badgeStyle = new GUIStyle(EditorStyles.miniLabel)
|
||||
{
|
||||
fontSize = 8,
|
||||
normal = { textColor = new Color(0.7f, 0.7f, 0.7f) },
|
||||
alignment = TextAnchor.MiddleCenter
|
||||
};
|
||||
GUI.Label(new Rect(btnRect.xMax - 2f, btnRect.y - 2f, 12f, 10f), laneCount.ToString(), badgeStyle);
|
||||
|
||||
// 点击
|
||||
if (Event.current.type == EventType.MouseDown && Event.current.button == 0
|
||||
&& btnRect.Contains(Event.current.mousePosition))
|
||||
{
|
||||
if (isExpanded)
|
||||
_expandedTracks.Remove(trackIdx);
|
||||
else
|
||||
_expandedTracks.Add(trackIdx);
|
||||
|
||||
_isDirty = true;
|
||||
Event.current.Use();
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────── Clip 绘制 ───────────────
|
||||
|
||||
private void DrawClip(Rect contentRect, FeedbackClip clip, int trackIdx, int clipIdx, float duration, int lane, bool isExpanded)
|
||||
{
|
||||
if (clip == null || duration <= 0) return;
|
||||
|
||||
float startNorm = clip.startTime / duration;
|
||||
float endNorm = clip.EndTime / duration;
|
||||
|
||||
float startX = contentRect.x + startNorm * contentRect.width;
|
||||
float endX = contentRect.x + endNorm * contentRect.width;
|
||||
float clipWidth = Mathf.Max(endX - startX, 2f);
|
||||
|
||||
float clipY, clipH;
|
||||
if (isExpanded)
|
||||
{
|
||||
clipY = contentRect.y + TRACK_PADDING + lane * LANE_HEIGHT + 2f;
|
||||
clipH = LANE_HEIGHT - 4f;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipY = contentRect.y + TRACK_PADDING + 2f;
|
||||
clipH = LANE_HEIGHT - 4f;
|
||||
}
|
||||
|
||||
Rect clipRect = new Rect(startX, clipY, clipWidth, clipH);
|
||||
|
||||
// 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 contentRect = GetTrackContentRect(allTracksArea, hit.trackIndex);
|
||||
float mouseTime = PixelToTime(mousePos.x, contentRect, 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 contentRect = GetTrackContentRect(allTracksArea, _dragTrackIndex);
|
||||
float mouseTime = PixelToTime(mousePos.x, contentRect, 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 || _trackLayouts == null) return result;
|
||||
|
||||
// 通过 Y 坐标确定轨道
|
||||
int trackIndex = -1;
|
||||
for (int i = 0; i < _trackLayouts.Length; i++)
|
||||
{
|
||||
float trackY = allTracksArea.y + _trackLayouts[i].yOffset;
|
||||
float trackBottom = trackY + _trackLayouts[i].totalHeight;
|
||||
if (mousePos.y >= trackY && mousePos.y < trackBottom)
|
||||
{
|
||||
trackIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (trackIndex < 0 || trackIndex >= targetData.tracks.Count) return result;
|
||||
|
||||
Rect contentRect = GetTrackContentRect(allTracksArea, trackIndex);
|
||||
if (!contentRect.Contains(mousePos)) return result;
|
||||
|
||||
var track = targetData.tracks[trackIndex];
|
||||
if (track.clips == null) return result;
|
||||
|
||||
bool isExpanded = _expandedTracks.Contains(trackIndex);
|
||||
ref TrackLayout layout = ref _trackLayouts[trackIndex];
|
||||
|
||||
for (int i = track.clips.Count - 1; i >= 0; i--)
|
||||
{
|
||||
FeedbackClip clip = track.clips[i];
|
||||
if (clip == null) continue;
|
||||
|
||||
float startX = contentRect.x + (clip.startTime / duration) * contentRect.width;
|
||||
float endX = contentRect.x + (clip.EndTime / duration) * contentRect.width;
|
||||
|
||||
if (mousePos.x < startX || mousePos.x > endX) continue;
|
||||
|
||||
// 展开模式下检查 Y 方向 lane
|
||||
if (isExpanded && layout.clipLanes != null && i < layout.clipLanes.Length)
|
||||
{
|
||||
int lane = layout.clipLanes[i];
|
||||
float laneY = contentRect.y + TRACK_PADDING + lane * LANE_HEIGHT;
|
||||
if (mousePos.y < laneY || mousePos.y > laneY + LANE_HEIGHT) continue;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (_trackLayouts == null || trackIndex >= _trackLayouts.Length)
|
||||
{
|
||||
return new Rect(
|
||||
allTracksArea.x + TRACK_LABEL_WIDTH,
|
||||
allTracksArea.y,
|
||||
allTracksArea.width - TRACK_LABEL_WIDTH,
|
||||
LANE_HEIGHT + TRACK_PADDING * 2
|
||||
);
|
||||
}
|
||||
|
||||
ref TrackLayout layout = ref _trackLayouts[trackIndex];
|
||||
return new Rect(
|
||||
allTracksArea.x + TRACK_LABEL_WIDTH,
|
||||
allTracksArea.y + layout.yOffset,
|
||||
allTracksArea.width - TRACK_LABEL_WIDTH,
|
||||
layout.totalHeight
|
||||
);
|
||||
}
|
||||
|
||||
/// <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
|
||||
@@ -0,0 +1,67 @@
|
||||
#if UNITY_EDITOR
|
||||
using Sirenix.OdinInspector.Editor;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSUtilities.Feedback.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 为标记了 [ShakeCurvePreset] 的 AnimationCurve 字段绘制预设按钮组。
|
||||
/// 按钮紧凑排列在曲线编辑器下方。
|
||||
/// </summary>
|
||||
public sealed class ShakeCurvePresetDrawer : OdinAttributeDrawer<ShakeCurvePresetAttribute, AnimationCurve>
|
||||
{
|
||||
private const float BUTTON_HEIGHT = 18f;
|
||||
private const float BUTTON_SPACING = 2f;
|
||||
|
||||
protected override void DrawPropertyLayout(GUIContent label)
|
||||
{
|
||||
// 先绘制默认的曲线字段
|
||||
CallNextDrawer(label);
|
||||
|
||||
// 绘制预设按钮行
|
||||
DrawPresetButtons();
|
||||
}
|
||||
|
||||
private void DrawPresetButtons()
|
||||
{
|
||||
var presets = ShakeCurvePresets.All;
|
||||
if (presets == null || presets.Length == 0) return;
|
||||
|
||||
Rect totalRect = EditorGUILayout.GetControlRect(false, BUTTON_HEIGHT);
|
||||
|
||||
// 计算每个按钮的宽度
|
||||
float totalSpacing = BUTTON_SPACING * (presets.Length - 1);
|
||||
float buttonWidth = (totalRect.width - totalSpacing) / presets.Length;
|
||||
|
||||
GUIStyle buttonStyle = new GUIStyle(EditorStyles.miniButton)
|
||||
{
|
||||
fontSize = 8,
|
||||
padding = new RectOffset(1, 1, 1, 1),
|
||||
fixedHeight = BUTTON_HEIGHT
|
||||
};
|
||||
|
||||
for (int i = 0; i < presets.Length; i++)
|
||||
{
|
||||
float x = totalRect.x + i * (buttonWidth + BUTTON_SPACING);
|
||||
Rect buttonRect = new Rect(x, totalRect.y, buttonWidth, BUTTON_HEIGHT);
|
||||
|
||||
if (GUI.Button(buttonRect, presets[i].name, buttonStyle))
|
||||
{
|
||||
// 记录 Undo 以便撤销
|
||||
if (Property.Tree.UnitySerializedObject != null)
|
||||
{
|
||||
Undo.RecordObject(
|
||||
Property.Tree.UnitySerializedObject.targetObject,
|
||||
$"Apply Shake Curve Preset: {presets[i].name}"
|
||||
);
|
||||
}
|
||||
|
||||
ValueEntry.SmartValue = presets[i].factory();
|
||||
ValueEntry.ApplyChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 84e1f89b58e2496418ff004a677f321b
|
||||
@@ -126,7 +126,7 @@ namespace SLSUtilities.Feedback
|
||||
/// <summary>
|
||||
/// 每帧由外部驱动调用(FeedbackManager 或 Subcontroller)。
|
||||
/// </summary>
|
||||
public void Tick(float unscaledDeltaTime)
|
||||
public void Tick(float deltaTime)
|
||||
{
|
||||
if (_state != FeedbackPlayerState.Playing) return;
|
||||
if (_data == null || _data.tracks == null) return;
|
||||
@@ -155,14 +155,17 @@ namespace SLSUtilities.Feedback
|
||||
FeedbackClip clip = track.clips[clipIdx];
|
||||
if (clip?.action == null) continue;
|
||||
|
||||
float clipDeltaTime = ComputeClipDeltaTime(clip, unscaledDeltaTime);
|
||||
ProcessClip(trackIdx, clipIdx, clip, clipDeltaTime);
|
||||
float clipTimeScale = ComputeClipTimeScale(clip);
|
||||
float clipDeltaTime = deltaTime * clipTimeScale;
|
||||
ProcessClip(trackIdx, clipIdx, clip, clipDeltaTime, clipTimeScale);
|
||||
}
|
||||
}
|
||||
|
||||
_currentTime += unscaledDeltaTime;
|
||||
_currentTime += deltaTime;
|
||||
|
||||
if (_currentTime >= totalDuration)
|
||||
// 仅当时间线游标超过总时长 且 所有 Clip 已结束时才完成。
|
||||
// 这避免了因时间缩放导致 Clip 尚在播放就被提前完成的问题。
|
||||
if (_currentTime >= totalDuration && AllClipsFinished())
|
||||
{
|
||||
_state = FeedbackPlayerState.Idle;
|
||||
_isCompleted = true;
|
||||
@@ -214,25 +217,24 @@ namespace SLSUtilities.Feedback
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据 Clip 的时间设置计算实际 deltaTime。
|
||||
/// 根据 Clip 的时间设置计算综合时间缩放系数。
|
||||
/// </summary>
|
||||
private float ComputeClipDeltaTime(FeedbackClip clip, float unscaledDeltaTime)
|
||||
private float ComputeClipTimeScale(FeedbackClip clip)
|
||||
{
|
||||
if (_timeProvider == null) return unscaledDeltaTime;
|
||||
if (_timeProvider == null) return 1f;
|
||||
if (clip?.action == null || clip.action.IgnoreTimeScale) return 1f;
|
||||
|
||||
FeedbackTimeSettings settings = clip.overrideTimeSettings
|
||||
? clip.timeSettings
|
||||
: _data.defaultTimeSettings;
|
||||
FeedbackTimeSettings settings = clip.overrideTimeSettings ? clip.timeSettings : _data.defaultTimeSettings;
|
||||
|
||||
if (settings == null || !settings.useTimeScale) return unscaledDeltaTime;
|
||||
if (settings == null || settings.timeScaleType == FeedbackTimeSettings.TimeScaleType.Unscaled) return 1f;
|
||||
|
||||
return _timeProvider.GetDeltaTime(settings);
|
||||
return _timeProvider.GetTimeScale(settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理单个 Clip 的生命周期状态转换和回调调用。
|
||||
/// </summary>
|
||||
private void ProcessClip(int trackIdx, int clipIdx, FeedbackClip clip, float deltaTime)
|
||||
private void ProcessClip(int trackIdx, int clipIdx, FeedbackClip clip, float deltaTime, float timeScale)
|
||||
{
|
||||
ref ClipState clipState = ref _clipStates[trackIdx, clipIdx];
|
||||
ref float elapsed = ref _clipElapsedTimes[trackIdx, clipIdx];
|
||||
@@ -247,30 +249,40 @@ namespace SLSUtilities.Feedback
|
||||
clipState = ClipState.Active;
|
||||
elapsed = _currentTime - clip.startTime;
|
||||
|
||||
FeedbackContext ctx = CreateContext(deltaTime, elapsed, safeDuration);
|
||||
FeedbackContext ctx = CreateContext(deltaTime, elapsed, safeDuration, timeScale, clip.timeSettings);
|
||||
clip.action.OnStart(ctx);
|
||||
|
||||
float normalizedTime = Mathf.Clamp01(elapsed / safeDuration);
|
||||
clip.action.OnUpdate(CreateContext(deltaTime, elapsed, safeDuration), normalizedTime);
|
||||
clip.action.OnUpdate(CreateContext(deltaTime, elapsed, safeDuration, timeScale, clip.timeSettings), normalizedTime);
|
||||
}
|
||||
break;
|
||||
|
||||
case ClipState.Active:
|
||||
elapsed += deltaTime;
|
||||
|
||||
// 如果启用动态时间缩放,每帧重新获取当前的时间缩放
|
||||
FeedbackTimeSettings settings = clip.overrideTimeSettings ? clip.timeSettings : _data.defaultTimeSettings;
|
||||
float currentTimeScale = timeScale;
|
||||
if (settings.applyDynamicTimeScale)
|
||||
{
|
||||
currentTimeScale = ComputeClipTimeScale(clip);
|
||||
}
|
||||
|
||||
// 使用调整后的deltaTime进行累加
|
||||
float adjustedDeltaTime = deltaTime * currentTimeScale;
|
||||
elapsed += adjustedDeltaTime;
|
||||
|
||||
if (elapsed >= safeDuration)
|
||||
{
|
||||
elapsed = safeDuration;
|
||||
clipState = ClipState.Finished;
|
||||
|
||||
FeedbackContext ctx = CreateContext(deltaTime, elapsed, safeDuration);
|
||||
FeedbackContext ctx = CreateContext(deltaTime, elapsed, safeDuration, currentTimeScale, clip.timeSettings);
|
||||
clip.action.OnUpdate(ctx, 1f);
|
||||
clip.action.OnEnd(ctx);
|
||||
}
|
||||
else
|
||||
{
|
||||
float normalizedTime = Mathf.Clamp01(elapsed / safeDuration);
|
||||
FeedbackContext ctx = CreateContext(deltaTime, elapsed, safeDuration);
|
||||
FeedbackContext ctx = CreateContext(deltaTime, elapsed, safeDuration, currentTimeScale, clip.timeSettings);
|
||||
clip.action.OnUpdate(ctx, normalizedTime);
|
||||
}
|
||||
break;
|
||||
@@ -302,7 +314,7 @@ namespace SLSUtilities.Feedback
|
||||
|
||||
float elapsed = _clipElapsedTimes[trackIdx, clipIdx];
|
||||
float safeDuration = Mathf.Max(clip.duration, MIN_DURATION);
|
||||
FeedbackContext ctx = CreateContext(0f, elapsed, safeDuration);
|
||||
FeedbackContext ctx = CreateContext(0f, elapsed, safeDuration, 1f, clip.timeSettings);
|
||||
clip.action.OnInterrupt(ctx);
|
||||
}
|
||||
|
||||
@@ -311,10 +323,37 @@ namespace SLSUtilities.Feedback
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查所有应播放的 Clip 是否都已完成。
|
||||
/// 用于时间缩放场景下,避免 Clip 尚在播放就提前完成整个 Feedback。
|
||||
/// </summary>
|
||||
private bool AllClipsFinished()
|
||||
{
|
||||
if (_clipStates == null) return true;
|
||||
|
||||
List<FeedbackTrack> tracks = _data.tracks;
|
||||
|
||||
for (int trackIdx = 0; trackIdx < tracks.Count; trackIdx++)
|
||||
{
|
||||
FeedbackTrack track = tracks[trackIdx];
|
||||
if (!ShouldPlayTrack(track)) continue;
|
||||
if (track.clips == null) continue;
|
||||
|
||||
for (int clipIdx = 0; clipIdx < track.clips.Count; clipIdx++)
|
||||
{
|
||||
if (_clipStates[trackIdx, clipIdx] == ClipState.Active)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 FeedbackContext 实例。
|
||||
/// </summary>
|
||||
private FeedbackContext CreateContext(float deltaTime, float elapsedTime, float duration)
|
||||
private FeedbackContext CreateContext(float deltaTime, float elapsedTime,
|
||||
float duration, float timeScale, FeedbackTimeSettings timeSettings)
|
||||
{
|
||||
return new FeedbackContext
|
||||
{
|
||||
@@ -322,7 +361,9 @@ namespace SLSUtilities.Feedback
|
||||
owner = _ownerTransform,
|
||||
deltaTime = deltaTime,
|
||||
elapsedTime = elapsedTime,
|
||||
duration = duration
|
||||
duration = duration,
|
||||
timeScale = timeScale,
|
||||
timeSettings = timeSettings
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,7 +385,7 @@ namespace SLSUtilities.FunctionalAnimation
|
||||
[HideReferenceObjectPicker]
|
||||
[LabelText("@NameForInspector")]
|
||||
public FuncAnimPayloadBase payload;
|
||||
public string NameForInspector => payload != null ? payload.NameForInspector : "NULL";
|
||||
public string NameForInspector => payload != null ? payload.NameForInspector + (payload.mute ? " (Muted)" : "") : "NULL";
|
||||
|
||||
public FuncAnimEvent(float triggerTime, FuncAnimPayloadBase payload, bool isEnd)
|
||||
{
|
||||
|
||||
@@ -29,6 +29,9 @@ namespace SLSUtilities.FunctionalAnimation
|
||||
[PropertyOrder(-1)] // 把它显示在最上面
|
||||
[HideInInspector]
|
||||
public string eventName;
|
||||
|
||||
[Tooltip("是否静默执行该事件(不触发任何反馈)")]
|
||||
public bool mute = false;
|
||||
|
||||
[NonSerialized]
|
||||
public FuncAnimData parentData;
|
||||
|
||||
@@ -17,26 +17,65 @@ namespace SLSUtilities.General
|
||||
return new Vector3(vector.x, 0, vector.z);
|
||||
}
|
||||
}
|
||||
|
||||
public class LerpFloat
|
||||
|
||||
public abstract class LerpValue<T>
|
||||
{
|
||||
public T currentValue;
|
||||
public T targetValue;
|
||||
public bool IsPausing { get; set; }
|
||||
|
||||
public bool advancedSettings = false;
|
||||
|
||||
public abstract void Update(float deltaTime);
|
||||
|
||||
public abstract void Update(float customSpeed, float deltaTime);
|
||||
}
|
||||
|
||||
public class LerpFloat : LerpValue<float>
|
||||
{
|
||||
public float currentValue;
|
||||
public float targetValue;
|
||||
public float lerpSpeed;
|
||||
public float increaseSpeed;
|
||||
public float decreaseSpeed;
|
||||
|
||||
public LerpFloat(float initialValue, float lerpSpeed)
|
||||
{
|
||||
this.currentValue = initialValue;
|
||||
this.targetValue = initialValue;
|
||||
this.advancedSettings = false;
|
||||
this.lerpSpeed = lerpSpeed;
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
|
||||
public LerpFloat(float initialValue, float increaseSpeed, float decreaseSpeed)
|
||||
{
|
||||
currentValue = Mathf.Lerp(currentValue, targetValue, lerpSpeed * deltaTime);
|
||||
this.currentValue = initialValue;
|
||||
this.targetValue = initialValue;
|
||||
this.advancedSettings = true;
|
||||
this.increaseSpeed = increaseSpeed;
|
||||
this.decreaseSpeed = decreaseSpeed;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (IsPausing) return;
|
||||
|
||||
if (advancedSettings)
|
||||
{
|
||||
if(targetValue > currentValue)
|
||||
{
|
||||
currentValue = Mathf.Lerp(currentValue, targetValue, increaseSpeed * deltaTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
currentValue = Mathf.Lerp(currentValue, targetValue, decreaseSpeed * deltaTime);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
currentValue = Mathf.Lerp(currentValue, targetValue, lerpSpeed * deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(float customSpeed, float deltaTime)
|
||||
public override void Update(float customSpeed, float deltaTime)
|
||||
{
|
||||
currentValue = Mathf.Lerp(currentValue, targetValue, customSpeed * deltaTime);
|
||||
}
|
||||
|
||||
@@ -81,6 +81,12 @@ namespace SLSUtilities.General
|
||||
if (_lookup.Count == 0 && _entries.Count > 0) Rebuild();
|
||||
return _lookup.TryGetValue(key, out value);
|
||||
}
|
||||
|
||||
public Dictionary<TKey, TValue> ToDictionary()
|
||||
{
|
||||
if (_lookup.Count == 0 && _entries.Count > 0) Rebuild();
|
||||
return new Dictionary<TKey, TValue>(_lookup);
|
||||
}
|
||||
|
||||
// ================= 核心:排序与重建 =================
|
||||
|
||||
|
||||
Reference in New Issue
Block a user