using System; using System.Collections.Generic; using System.Linq; using Sirenix.OdinInspector; using SLSUtilities.General; using UnityEngine; namespace SLSUtilities.FunctionalAnimation { public static class FuncAnimInteractionKeys { public const string SpecifiedActionDisruption = "SpecifiedActionDisruption"; public const string ExemptedPayloadAnimPairOnSelfOverrideDisruption = "ExemptedPayloadAnimPairOnSelfOverrideDisruption"; } [Serializable] public class FuncAnimInteractionDictionary : SerializedDictionary, FuncAnimInteractionPair> { public override void Add(string key, List value) { if (ContainsKey(key)) { HandleDuplicate(key, value); return; } _entries.Add(new FuncAnimInteractionPair { interactionKey = key, values = value }); _lookup[key] = value; _orderedKeys.Add(key); } } [Serializable] public struct FuncAnimInteractionPair : ISerializedPair> { [ValueDropdown("KeyDropdown")] [HorizontalGroup("H")] [HideLabel] public string interactionKey; [HorizontalGroup("H")] [LabelText("Interaction Values")] public List values; public string Key => interactionKey; public List Value => values; public static List KeyDropdown() { return new List { FuncAnimInteractionKeys.SpecifiedActionDisruption, FuncAnimInteractionKeys.ExemptedPayloadAnimPairOnSelfOverrideDisruption }; } } 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(), DisruptionType.NormalAction, 1.0f, 0, true); [Title("技能区间")] [ListDrawerSettings(ListElementLabelName = "@this.GetIntervalLabel()", AddCopiesLastElement = true)] public List intervals = new List() { 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("技能交互标签,用于标记技能与其他系统的交互关系,例如可以用来标记哪些技能可以被特定状态打断等")] [DictionaryTitle("Interactions")] [SerializedDictionarySettings("Key", "Values")] [ToolbarButton("OpenInteractionsGuide", SdfIconType.QuestionCircle, "打开 Interactions 使用说明")] public FuncAnimInteractionDictionary interactions = new FuncAnimInteractionDictionary(); private void OpenInteractionsGuide() { #if UNITY_EDITOR Type guideWindowType = Type.GetType("SLSUtilities.FunctionalAnimation.FuncAnimInteractionsGuideWindow"); guideWindowType?.GetMethod("ShowWindow")?.Invoke(null, null); #endif } [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; } } } public List GetSuppressedDisruptionEventNames(FuncAnimData nextData, FuncAnimDisruptionCause cause) { if (cause != FuncAnimDisruptionCause.SelfOverride || nextData == null || interactions == null) { return null; } if (!interactions.TryGetValue(FuncAnimInteractionKeys.ExemptedPayloadAnimPairOnSelfOverrideDisruption, out List entries) || entries == null) { return null; } List suppressedEventNames = new List(); string targetAnimationName = nextData.animInfo.animationName; foreach (string entry in entries) { if (!TryParseSelfOverrideExemption(entry, out string configuredTargetName, out string eventName)) { continue; } if (configuredTargetName != "*" && configuredTargetName != targetAnimationName) { continue; } suppressedEventNames.Add(eventName); } return suppressedEventNames; } private static bool TryParseSelfOverrideExemption(string entry, out string targetAnimationName, out string eventName) { targetAnimationName = null; eventName = null; if (string.IsNullOrWhiteSpace(entry)) { return false; } string[] parts = entry.Split(new[] { ':' }, 2); if (parts.Length != 2) { Debug.LogWarning($"[FuncAnimData.Interactions] 无效的 SelfOverride disruption event 豁免配置:'{entry}'。正确格式为 PayloadEventName:TargetAnimationName。"); return false; } eventName = parts[0].Trim(); targetAnimationName = parts[1].Trim(); if (string.IsNullOrEmpty(targetAnimationName) || string.IsNullOrEmpty(eventName)) { Debug.LogWarning($"[FuncAnimData.Interactions] 无效的 SelfOverride disruption event 豁免配置:'{entry}'。TargetAnimationName 和 PayloadEventName 都不能为空。"); return false; } return true; } private void RequestRepaint() { #if UNITY_EDITOR EditorWantsRepaint = true; #endif } public FuncAnimInterval Interval(IntervalType type, string name = "", int index = 0) { List 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(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 { [TitleGroup("时间轴事件")] [ListDrawerSettings(ShowFoldout = true, DraggableItems = true)] public List animEvents = new List(); [TitleGroup("时间轴事件")] [DisableInEditorMode] [ShowInInspector] [DoNotDrawAsReference] [ListDrawerSettings(ShowFoldout = true, DraggableItems = false, ListElementLabelName = "NameForInspector")] [Tooltip("在动作被打断时触发的事件列表")] public List endEvents => animEvents.FindAll(evt => evt.isEnd); [TitleGroup("非时间轴事件")] [ListDrawerSettings(ShowFoldout = true, DraggableItems = false, ListElementLabelName = "NameForInspector")] [SerializeReference] public List startEvents = new List(); [TitleGroup("非时间轴事件")] [ListDrawerSettings(ShowFoldout = true, DraggableItems = false, ListElementLabelName = "NameForInspector")] [SerializeReference] public List disruptionEvents = new List(); [TitleGroup("非时间轴事件")] [ListDrawerSettings(ShowFoldout = true, DraggableItems = false, ListElementLabelName = "NameForInspector")] [SerializeReference] public List updateEvents = new List(); [TitleGroup("非时间轴事件")] [Tooltip("技能播放期间每帧执行")] [ListDrawerSettings(ShowFoldout = true, DraggableItems = false, ListElementLabelName = "NameForInspector")] [SerializeReference] public List> updateUntilEvents = new List>(); 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)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; } /// /// 创建此事件的独立副本,包括深拷贝 payload。 /// public FuncAnimEvent DeepClone() { return new FuncAnimEvent(triggerTime, payload?.DeepClone(), isEnd); } public void Invoke() { if (payload != null) { if (payload.isOnce && payload.hasExecuted) return; payload.Invoke(); payload.hasExecuted = true; } } } public class VariableCollection { [InfoBox("RootMoveZ 计算方法:Active Interval的结束时Z坐标减去开始时Z坐标。")] public Dictionary variables = new Dictionary(); public T GetVariable(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(this.variables); return clone; } } public static class FuncAnimExtensions { public static void Invoke(this IList events) { if (events == null) return; foreach (FuncAnimEvent evt in events) { evt.Invoke(); } } public static void Invoke(this IList events) { if (events == null) return; foreach (FuncAnimPayloadBase evt in events) { evt.Invoke(); } } } }