458 lines
17 KiB
C#
458 lines
17 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using Sirenix.OdinInspector;
|
||
using SLSUtilities.General;
|
||
using UnityEngine;
|
||
|
||
#if UNITY_EDITOR
|
||
using UnityEditor;
|
||
using UnityEditor.Callbacks;
|
||
#endif
|
||
|
||
namespace SLSUtilities.FunctionalAnimation
|
||
{
|
||
public enum TimeDisplayMode
|
||
{
|
||
[InspectorName("秒 (s)")] Seconds,
|
||
[InspectorName("帧 (f)")] Frames
|
||
}
|
||
|
||
[CreateAssetMenu(fileName = "NewFuncAnimData", menuName = "Functional Animation/FuncAnimData", order = 1)]
|
||
public class FuncAnimData : SerializedScriptableObject
|
||
{
|
||
public static bool EditorWantsRepaint = false;
|
||
|
||
[ReadOnly, ShowInInspector]
|
||
public FuncAnimDataCollection parentCollection;
|
||
|
||
[Title("编辑器设置")]
|
||
[EnumToggleButtons]
|
||
[OnValueChanged("RequestRepaint")]
|
||
public TimeDisplayMode timeMode;
|
||
|
||
[Title("核心动画")]
|
||
[Required("必须指定一个动画片段")]
|
||
[OnValueChanged("RequestRepaint")]
|
||
public AnimationClip animationClip;
|
||
|
||
[Title("编辑时信息")] public FuncAnimInfo animInfo = new FuncAnimInfo("AnimationName", "StateName",
|
||
true, new List<string>(), DisruptionType.NormalAction, 1.0f, 0, true);
|
||
|
||
[Title("技能区间")]
|
||
[ListDrawerSettings(
|
||
ListElementLabelName = "@this.GetIntervalLabel()",
|
||
AddCopiesLastElement = true)]
|
||
public List<FuncAnimInterval> intervals = new List<FuncAnimInterval>()
|
||
{
|
||
new FuncAnimInterval(IntervalType.ExternalDisruption),
|
||
new FuncAnimInterval(IntervalType.Active),
|
||
new FuncAnimInterval(IntervalType.Preinput),
|
||
new FuncAnimInterval(IntervalType.ActionDisruption),
|
||
new FuncAnimInterval(IntervalType.MovementDisruption),
|
||
new FuncAnimInterval(IntervalType.RootMotion)
|
||
};
|
||
|
||
[Title("交互处理")]
|
||
[Tooltip("技能交互标签,用于标记技能与其他系统的交互关系,例如可以用来标记哪些技能可以被特定状态打断等")]
|
||
public Dictionary<string, List<string>> interactions = new Dictionary<string, List<string>>();
|
||
|
||
[Title("动画事件")]
|
||
public EventCollection eventCollection = new EventCollection();
|
||
|
||
[Title("变量存储")]
|
||
public VariableCollection variableCollection = new VariableCollection();
|
||
|
||
// (新增) 核心方法:将父级引用传递给子级
|
||
// 这让子对象 (Event/Interval) 能够访问 animationClip 来计算帧
|
||
[OnInspectorInit("UpdateChildReferences")]
|
||
[OnInspectorGUI("UpdateChildReferences")] // 在绘制其他所有内容之前运行
|
||
private void UpdateChildReferences()
|
||
{
|
||
if (intervals != null)
|
||
{
|
||
foreach (var interval in intervals)
|
||
{
|
||
if (interval != null) interval.parentData = this;
|
||
}
|
||
}
|
||
|
||
if (eventCollection.animEvents != null)
|
||
{
|
||
foreach (var animEvent in eventCollection.animEvents)
|
||
{
|
||
if (animEvent is { payload: not null }) animEvent.payload.parentData = this;
|
||
}
|
||
|
||
foreach (var payload in eventCollection.startEvents)
|
||
{
|
||
if (payload != null) payload.parentData = this;
|
||
}
|
||
|
||
foreach (var payload in eventCollection.disruptionEvents)
|
||
{
|
||
if (payload != null) payload.parentData = this;
|
||
}
|
||
|
||
foreach (var payload in eventCollection.updateEvents)
|
||
{
|
||
if (payload != null) payload.parentData = this;
|
||
}
|
||
|
||
foreach (var payload in eventCollection.updateUntilEvents)
|
||
{
|
||
if (payload != null) payload.parentData = this;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void RequestRepaint()
|
||
{
|
||
#if UNITY_EDITOR
|
||
EditorWantsRepaint = true;
|
||
#endif
|
||
}
|
||
|
||
public FuncAnimInterval Interval(IntervalType type, string name = "", int index = 0)
|
||
{
|
||
List<FuncAnimInterval> find = intervals.FindAll(interval => interval.intervalType == type);
|
||
|
||
if (find.Count == 1)
|
||
{
|
||
if (type != IntervalType.Custom)
|
||
{
|
||
return find[0];
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(name) && find[0].intervalName == name)
|
||
{
|
||
return find[0];
|
||
}
|
||
|
||
Debug.LogWarning($"[FuncAnimData.Interval] 找不到名为 '{name}' 的自定义区间。");
|
||
return null;
|
||
}
|
||
|
||
if (find.Count == 0)
|
||
{
|
||
Debug.LogWarning($"[FuncAnimData.Interval] 找不到类型为 '{type}' 的区间。");
|
||
return null;
|
||
}
|
||
|
||
if (index < 0 || index >= find.Count)
|
||
{
|
||
Debug.LogWarning($"[FuncAnimData.Interval] 类型为 '{type}' 的区间数量不足,无法获取索引 {index}。");
|
||
return null;
|
||
}
|
||
|
||
return find[index];
|
||
}
|
||
|
||
public T Variable<T>(string key, T defaultValue = default)
|
||
{
|
||
if (variableCollection!= null &&
|
||
variableCollection.variables!= null &&
|
||
variableCollection.variables.TryGetValue(key, out string valueStr))
|
||
{
|
||
try
|
||
{
|
||
return (T)Convert.ChangeType(valueStr, typeof(T));
|
||
}
|
||
catch
|
||
{
|
||
Debug.LogWarning($"[FuncAnimData.VariableCollection] 无法将变量 '{key}' 的值转换为类型 {typeof(T)},返回默认值。");
|
||
return defaultValue;
|
||
}
|
||
}
|
||
return defaultValue;
|
||
}
|
||
}
|
||
}
|
||
|
||
namespace SLSUtilities.FunctionalAnimation
|
||
{
|
||
public enum IntervalType
|
||
{
|
||
[InspectorName("前摇 (Startup)")]
|
||
Startup = 10,
|
||
[InspectorName("外部打断窗口 (External Disruption)")]
|
||
ExternalDisruption = 11,
|
||
[InspectorName("强制外部打断窗口 (Forced External Disruption),如果没有此区间,默认整个动作都可被强制打断")]
|
||
ForcedExternalDisruption = 12,
|
||
[InspectorName("攻击判定 (Active)")]
|
||
Active = 20,
|
||
[InspectorName("预输入窗口 (Pre-Input)")]
|
||
Preinput = 22,
|
||
[InspectorName("新动作打断窗口 (Action Disruption)")]
|
||
ActionDisruption = 30,
|
||
[InspectorName("移动打断窗口 (Movement Disruption)")]
|
||
MovementDisruption = 31,
|
||
[InspectorName("强制动作打断窗口 (Forced Action Disruption) 如果没有此区间,默认整个动作都可被强制打断")]
|
||
ForcedActionDisruption = 32,
|
||
[InspectorName("Root Motion 作用区间,角色会使用动画中的位移")]
|
||
RootMotion = 40,
|
||
[InspectorName("自定义 (Custom)")]
|
||
Custom = 100
|
||
}
|
||
|
||
[Serializable]
|
||
public class FuncAnimInterval // <-- 关键改动: struct 变为 class
|
||
{
|
||
[NonSerialized, HideInInspector]
|
||
public FuncAnimData parentData;
|
||
|
||
// (新增) 辅助函数
|
||
private float GetClipDuration() => (parentData == null || parentData.animationClip == null)
|
||
? 10.0f : parentData.animationClip.length; // 默认10s
|
||
private bool ShowSeconds() => parentData == null || parentData.timeMode == TimeDisplayMode.Seconds || parentData.animationClip == null;
|
||
private bool ShowFrames() => parentData != null && parentData.timeMode == TimeDisplayMode.Frames && parentData.animationClip != null;
|
||
private int GetMaxFrames() => (parentData == null || parentData.animationClip == null || parentData.animationClip.frameRate == 0)
|
||
? 100 : Mathf.RoundToInt(parentData.animationClip.length * parentData.animationClip.frameRate);
|
||
|
||
public string GetIntervalLabel()
|
||
{
|
||
return intervalType == IntervalType.Custom ? (string.IsNullOrEmpty(intervalName) ? "Custom" : intervalName) : intervalType.ToString();
|
||
}
|
||
|
||
public IntervalType intervalType;
|
||
|
||
[ShowIf("intervalType", IntervalType.Custom)]
|
||
public string intervalName;
|
||
|
||
// (修改) 仅在 "Seconds" 模式下显示
|
||
[ShowIf("ShowSeconds")]
|
||
[MinMaxSlider(0f, "@GetClipDuration()", ShowFields = true)]
|
||
[LabelText("Time Range (s)")]
|
||
[SuffixLabel("秒")]
|
||
public Vector2 timeRange;
|
||
|
||
// (新增) 在 "Frames" 模式下显示的 "帧" 属性
|
||
[ShowInInspector, ShowIf("ShowFrames"), MinMaxSlider(0, "@GetMaxFrames()", ShowFields = true)]
|
||
[LabelText("Frame Range")]
|
||
public Vector2Int FrameRange
|
||
{
|
||
get
|
||
{
|
||
if (parentData == null || parentData.animationClip == null) return Vector2Int.zero;
|
||
float frameRate = parentData.animationClip.frameRate;
|
||
if (frameRate == 0) return Vector2Int.zero;
|
||
|
||
// (修改) 从 timeRange (秒) 计算
|
||
return new Vector2Int(
|
||
Mathf.RoundToInt(timeRange.x * frameRate),
|
||
Mathf.RoundToInt(timeRange.y * frameRate)
|
||
);
|
||
}
|
||
set
|
||
{
|
||
if (parentData == null || parentData.animationClip == null) return;
|
||
float frameRate = parentData.animationClip.frameRate;
|
||
if (frameRate == 0) return;
|
||
|
||
// (修改) 转换回 timeRange (秒)
|
||
timeRange.x = (float)value.x / frameRate;
|
||
timeRange.y = (float)value.y / frameRate;
|
||
}
|
||
}
|
||
|
||
// (修改) 属性现在直接返回 timeRange
|
||
public float StartTime => timeRange.x;
|
||
public float EndTime => timeRange.y;
|
||
public float Duration => Mathf.Max(0, timeRange.y - timeRange.x);
|
||
|
||
public FuncAnimInterval(IntervalType intervalType)
|
||
{
|
||
this.parentData = null;
|
||
this.intervalType = intervalType;
|
||
this.intervalName = "";
|
||
this.timeRange = Vector2.zero;
|
||
}
|
||
}
|
||
}
|
||
|
||
namespace SLSUtilities.FunctionalAnimation
|
||
{
|
||
[Serializable]
|
||
public class EventCollection : ICloneable<EventCollection>
|
||
{
|
||
[TitleGroup("时间轴事件")]
|
||
[ListDrawerSettings(ShowFoldout = true, DraggableItems = true)]
|
||
public List<FuncAnimEvent> animEvents = new List<FuncAnimEvent>();
|
||
|
||
[TitleGroup("时间轴事件")]
|
||
[DisableInEditorMode]
|
||
[ShowInInspector]
|
||
[Tooltip("在动作被打断时触发的事件列表")]
|
||
public List<FuncAnimEvent> endEvents => animEvents.FindAll(evt => evt.isEnd);
|
||
|
||
[TitleGroup("非时间轴事件")]
|
||
[Tooltip("在技能开始播放动画 *之前* 立即执行")]
|
||
[ListDrawerSettings(ShowFoldout = true, DraggableItems = false)]
|
||
[SerializeReference]
|
||
public List<FuncAnimPayloadBase> startEvents = new List<FuncAnimPayloadBase>();
|
||
|
||
[TitleGroup("非时间轴事件")]
|
||
[Tooltip("在技能被(StopSkill) *打断* 时执行")]
|
||
[ListDrawerSettings(ShowFoldout = true, DraggableItems = false)]
|
||
[SerializeReference]
|
||
public List<FuncAnimPayloadBase> disruptionEvents = new List<FuncAnimPayloadBase>();
|
||
|
||
[TitleGroup("非时间轴事件")]
|
||
[Tooltip("技能播放期间每帧执行")]
|
||
[ListDrawerSettings(ShowFoldout = true, DraggableItems = false)]
|
||
[SerializeReference]
|
||
public List<FuncAnimPayloadBase> updateEvents = new List<FuncAnimPayloadBase>();
|
||
|
||
[TitleGroup("非时间轴事件")]
|
||
[Tooltip("技能播放期间每帧执行")]
|
||
[ListDrawerSettings(ShowFoldout = true, DraggableItems = false)]
|
||
[SerializeReference]
|
||
public List<FuncAnimPayloadBase<bool>> updateUntilEvents = new List<FuncAnimPayloadBase<bool>>();
|
||
|
||
public EventCollection Clone()
|
||
{
|
||
EventCollection clone = new EventCollection();
|
||
clone.animEvents = this.animEvents.Select(evt => evt.DeepClone()).ToList();
|
||
clone.startEvents = this.startEvents.Select(p => p?.DeepClone()).ToList();
|
||
clone.disruptionEvents = this.disruptionEvents.Select(p => p?.DeepClone()).ToList();
|
||
clone.updateEvents = this.updateEvents.Select(p => p?.DeepClone()).ToList();
|
||
clone.updateUntilEvents = this.updateUntilEvents
|
||
.Select(p => (FuncAnimPayloadBase<bool>)p?.DeepClone()).ToList();
|
||
return clone;
|
||
}
|
||
}
|
||
|
||
|
||
[Serializable]
|
||
public class FuncAnimEvent
|
||
{
|
||
// (新增) 非序列化的父级引用
|
||
private FuncAnimData parentData => payload?.parentData;
|
||
|
||
// (新增) 辅助方法
|
||
private bool ShowSeconds() => parentData == null || parentData.timeMode == TimeDisplayMode.Seconds || parentData.animationClip == null;
|
||
private bool ShowFrames() => parentData != null && parentData.timeMode == TimeDisplayMode.Frames && parentData.animationClip != null;
|
||
private int GetMaxFrames() => (parentData == null || parentData.animationClip == null || parentData.animationClip.frameRate == 0)
|
||
? 100 : Mathf.RoundToInt(parentData.animationClip.length * parentData.animationClip.frameRate);
|
||
|
||
|
||
// (修改) 仅在 "Seconds" 模式下显示
|
||
[HorizontalGroup("EventLine", Width = 0.15f)]
|
||
[Tooltip("事件触发时间 (秒)")] [SuffixLabel("秒", Overlay = true)]
|
||
[LabelText("时间")] [LabelWidth(40)]
|
||
[ShowIf("ShowSeconds")]
|
||
public float triggerTime;
|
||
|
||
// (新增字段)
|
||
[HorizontalGroup("EventLine", Width = 0.15f)]
|
||
[ShowInInspector, ShowIf("ShowFrames"), SuffixLabel("f", Overlay = true)]
|
||
[LabelText("帧")] [LabelWidth(40)]
|
||
[MinValue(0)] [MaxValue("@GetMaxFrames()")]
|
||
public int TriggerFrame
|
||
{
|
||
get
|
||
{
|
||
if (parentData == null || parentData.animationClip == null) return 0;
|
||
float frameRate = parentData.animationClip.frameRate;
|
||
return Mathf.RoundToInt(triggerTime * frameRate);
|
||
}
|
||
set
|
||
{
|
||
if (parentData == null || parentData.animationClip == null) return;
|
||
float frameRate = parentData.animationClip.frameRate;
|
||
if (frameRate > 0)
|
||
{
|
||
triggerTime = (float)value / frameRate;
|
||
}
|
||
}
|
||
}
|
||
|
||
// (修改) "Bool" 是第二列
|
||
[HorizontalGroup("EventLine", Width = 0.15f)]
|
||
[Tooltip("如果技能被打断,是否也触发此事件?(即'End'事件)")]
|
||
[LabelWidth(40)]
|
||
[PropertyOrder(1)]
|
||
public bool isEnd;
|
||
|
||
// (修改) "Payload" 是第三列
|
||
[HorizontalGroup("EventLine")]
|
||
[HideLabel]
|
||
[SerializeReference]
|
||
[PropertyOrder(2)]
|
||
[HideReferenceObjectPicker]
|
||
[LabelText("@NameForInspector")]
|
||
public FuncAnimPayloadBase payload;
|
||
public string NameForInspector => payload != null ? payload.NameForInspector + (payload.mute ? " (Muted)" : "") : "NULL";
|
||
|
||
public FuncAnimEvent(float triggerTime, FuncAnimPayloadBase payload, bool isEnd)
|
||
{
|
||
this.triggerTime = triggerTime;
|
||
this.isEnd = isEnd;
|
||
this.payload = payload;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建此事件的独立副本,包括深拷贝 payload。
|
||
/// </summary>
|
||
public FuncAnimEvent DeepClone()
|
||
{
|
||
return new FuncAnimEvent(triggerTime, payload?.DeepClone(), isEnd);
|
||
}
|
||
|
||
public void Invoke()
|
||
{
|
||
payload?.Invoke();
|
||
}
|
||
}
|
||
|
||
public class VariableCollection
|
||
{
|
||
[InfoBox("RootMoveZ 计算方法:Active Interval的结束时Z坐标减去开始时Z坐标。")]
|
||
public Dictionary<string, string> variables = new Dictionary<string, string>();
|
||
|
||
public T GetVariable<T>(string key, T defaultValue = default)
|
||
{
|
||
if (variables.TryGetValue(key, out string valueStr))
|
||
{
|
||
try
|
||
{
|
||
return (T)Convert.ChangeType(valueStr, typeof(T));
|
||
}
|
||
catch
|
||
{
|
||
Debug.LogWarning($"[FuncAnimData.VariableCollection] 无法将变量 '{key}' 的值转换为类型 {typeof(T)},返回默认值。");
|
||
return defaultValue;
|
||
}
|
||
}
|
||
return defaultValue;
|
||
}
|
||
|
||
public VariableCollection Clone()
|
||
{
|
||
VariableCollection clone = new VariableCollection();
|
||
clone.variables = new Dictionary<string, string>(this.variables);
|
||
return clone;
|
||
}
|
||
}
|
||
|
||
public static class FuncAnimExtensions
|
||
{
|
||
public static void Invoke(this IList<FuncAnimEvent> events)
|
||
{
|
||
if (events == null) return;
|
||
foreach (FuncAnimEvent evt in events)
|
||
{
|
||
evt.Invoke();
|
||
}
|
||
}
|
||
|
||
public static void Invoke(this IList<FuncAnimPayloadBase> events)
|
||
{
|
||
if (events == null) return;
|
||
foreach (FuncAnimPayloadBase evt in events)
|
||
{
|
||
evt.Invoke();
|
||
}
|
||
}
|
||
}
|
||
} |