using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEngine; using UnityEditor; using Sirenix.OdinInspector; using Sirenix.OdinInspector.Editor; using UnityEditor.Callbacks; namespace SLSUtilities.FunctionalAnimation { public class FuncAnimEditorWindow : OdinEditorWindow { // 定义一个菜单项来打开这个窗口 [MenuItem("Tools/SLS Utilities/Functional Animation Editor")] private static void OpenWindow() { GetWindow().Show(); } [Title("技能数据编辑器")] [ShowInInspector, AssetsOnly, PropertyOrder(-100)] [LabelText("当前编辑的技能")] [OnValueChanged("OnDataChanged")] // 当我们选择新的Data时,触发 public FuncAnimData targetData; // ----------------- 内联数据编辑器 ----------------- [ShowInInspector, PropertyOrder(1)] [ShowIf("targetData")] // 只有在选择了 targetData 时才显示 [InlineEditor(Expanded = true, ObjectFieldMode = InlineEditorObjectFieldModes.Hidden)] public FuncAnimData dataEditor; private enum DragMode { None, Event, IntervalLeft, IntervalRight, IntervalMove } private DragMode currentDragMode = DragMode.None; private int draggingEventIndex = -1; private int draggingIntervalIndex = -1; // 现在代表 interval 列表的索引 private bool isDirty = false; // (修改) 新的布局常量 private const float RULER_HEIGHT = 20f; // 标尺的高度 private const float TRACK_HEIGHT = 22f; // 每个区间轨道的高度 (含2px间距) private const float TRACK_LABEL_WIDTH = 120f; // 轨道标签的宽度 private const float DRAG_HANDLE_WIDTH = 5f; // 颜色缓存 private static Dictionary eventColorCache = new Dictionary(); // ----------------- 自定义时间轴 GUI ----------------- // 双击资产时打开自定义编辑器窗口 [OnOpenAsset(1)] public static bool OnOpenAsset(int instanceID, int line) { FuncAnimData funcAnimData = EditorUtility.InstanceIDToObject(instanceID) as FuncAnimData; if (funcAnimData != null) { OpenWindow(); var window = GetWindow(); window.targetData = funcAnimData; window.OnDataChanged(); return true; } return false; } [OnInspectorGUI] [PropertyOrder(-99)] [ShowIf("targetData")] private void DrawTimelineGUI() { if (targetData.animationClip == null) { EditorGUILayout.HelpBox("请为 FuncAnimData 指定一个 AnimationClip。", MessageType.Warning); return; } AutoSetupPayloadNames(this.targetData); float duration = targetData.animationClip.length; if (duration <= 0) duration = 1f; // 避免除零 float frameRate = targetData.animationClip.frameRate; int numIntervalTracks = (targetData.intervals != null) ? targetData.intervals.Count : 0; float totalHeight = RULER_HEIGHT + (numIntervalTracks * TRACK_HEIGHT); Rect totalArea = GUILayoutUtility.GetRect(GUIContent.none, GUIStyle.none, GUILayout.Height(totalHeight)); // --- (核心布局修改) --- Rect trackLabelHeaderArea = 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 ); // (修改) 轨道总区域也需要偏移,但它由 DrawIntervalTracks 内部处理 Rect allTracksArea = new Rect( totalArea.x, totalArea.y + RULER_HEIGHT, totalArea.width, totalHeight - RULER_HEIGHT ); // --- (修改结束) --- // 绘制背景 (给左上角标尺头也画上背景) DrawTimelineBackground(totalArea); EditorGUI.DrawRect(trackLabelHeaderArea, new Color(0.18f, 0.18f, 0.18f)); DrawTicks(rulerArea, duration, frameRate); // 刻度画在 *新* 的 rulerArea DrawIntervalTracks(allTracksArea, duration); // 区间 (内部已处理偏移) DrawEvents(totalArea, rulerArea, duration); // 事件画在 *新* 的 rulerArea HandleMouseInput(totalArea, rulerArea, allTracksArea, duration); if (isDirty) { Repaint(); isDirty = false; } if (FuncAnimData.EditorWantsRepaint) { FuncAnimData.EditorWantsRepaint = false; Repaint(); } GUILayout.Space(10); } private void DrawTimelineBackground(Rect area) { EditorGUI.DrawRect(area, new Color(0.18f, 0.18f, 0.18f)); } private void DrawIntervalTracks(Rect allTracksArea, float duration) { if (targetData.intervals == null) return; float currentY = allTracksArea.y; for (int i = 0; i < targetData.intervals.Count; i++) { var interval = targetData.intervals[i]; Rect rowRect = new Rect(allTracksArea.x, currentY, allTracksArea.width, TRACK_HEIGHT); Rect labelRect = new Rect(rowRect.x, rowRect.y, TRACK_LABEL_WIDTH, rowRect.height - 2); Rect trackRect = new Rect(rowRect.x + TRACK_LABEL_WIDTH, rowRect.y, rowRect.width - TRACK_LABEL_WIDTH, rowRect.height - 2); EditorGUI.DrawRect(trackRect, new Color(0.25f, 0.25f, 0.25f)); string label = interval.GetIntervalLabel(); GUIStyle labelStyle = new GUIStyle(EditorStyles.label); if (i == draggingIntervalIndex) { labelStyle.fontStyle = FontStyle.Bold; labelStyle.normal.textColor = Color.yellow; } EditorGUI.LabelField(labelRect, label, labelStyle); // --- (核心数据修改) --- // (修改) 直接使用秒 float startTime = interval.timeRange.x; float endTime = interval.timeRange.y; // --- (修改结束) --- // (不变) 这里的归一化是 *为了绘制*,所以是正确的 float startX = trackRect.x + (startTime / duration) * trackRect.width; float endX = trackRect.x + (endTime / duration) * trackRect.width; float width = Math.Max(0, endX - startX); Rect intervalBarRect = new Rect(startX, trackRect.y, width, trackRect.height); Color color = GetColorForIntervalType(interval.intervalType); if (i == draggingIntervalIndex) color *= 1.5f; EditorGUI.DrawRect(intervalBarRect, color); currentY += TRACK_HEIGHT; } } // (修改) private void DrawEvents(Rect totalArea, Rect rulerArea, float duration) { if (targetData.eventCollection.animEvents == null) return; GUIStyle labelStyle = new GUIStyle(EditorStyles.miniLabel); for (int i = 0; i < targetData.eventCollection.animEvents.Count; i++) { var evt = targetData.eventCollection.animEvents[i]; if (evt.payload == null) continue; float triggerTime = evt.triggerTime; float normalizedTime = Mathf.Clamp01(triggerTime / duration); // --- (核心布局修改) --- // (修改) X 坐标现在基于 rulerArea float xPos = rulerArea.x + normalizedTime * rulerArea.width; // --- (修改结束) --- Type payloadType = evt.payload.GetType(); Color baseColor = GetColorForEventType(payloadType); if (evt.isEnd) baseColor = new Color(1f, 0.3f, 1f); Color finalColor = (i == draggingEventIndex) ? Color.yellow : baseColor; // (修改) 标记线从标尺顶部开始 Rect eventMarkerRect = new Rect(xPos - 1, rulerArea.y, 2, totalArea.height - rulerArea.y); EditorGUI.DrawRect(eventMarkerRect, finalColor); string label = string.IsNullOrEmpty(evt.payload.eventName) ? payloadType.Name : evt.payload.eventName; if (evt.isEnd) label = "[END] " + label; Rect labelRect = new Rect(xPos + 3, rulerArea.y, 100, rulerArea.height); labelStyle.normal.textColor = finalColor; GUI.Label(labelRect, label, labelStyle); GUI.Label(eventMarkerRect, new GUIContent("", label)); } } private void DrawTicks(Rect rulerArea, float duration, float frameRate) { // (不变) 此方法已基于 rulerArea,所以无需修改 if (duration <= 0) return; Handles.color = Color.gray; int numTicks = Mathf.FloorToInt(duration / 0.5f); for (int i = 0; i <= numTicks; i++) { float time = i * 0.5f; float xPos = rulerArea.x + (time / duration) * rulerArea.width; Handles.DrawLine(new Vector3(xPos, rulerArea.y), new Vector3(xPos, rulerArea.yMax)); GUI.Label(new Rect(xPos + 3, rulerArea.y, 50, 20), $"{time:F1}s"); } string totalTimeLabel = $"{duration:F2}s ({duration * frameRate:F0}f)"; GUI.Label(new Rect(rulerArea.xMax - 80, rulerArea.y, 80, 20), totalTimeLabel); } private Color GetColorForIntervalType(IntervalType type) { // (此方法与上一阶段完全相同,为节省空间已折叠) switch (type) { case IntervalType.Cancellable: return new Color(1.0f, 1.0f, 0.5f, 0.6f); case IntervalType.Startup: return new Color(1.0f, 0.5f, 0.5f, 0.6f); case IntervalType.ExternalDisruption: return new Color(0.5f, 0.0f, 0.5f, 0.6f); case IntervalType.Active: return new Color(0.5f, 1.0f, 0.5f, 0.6f); case IntervalType.ActionDisruption: return new Color(0.5f, 0.5f, 1.0f, 0.6f); case IntervalType.MovementDisruption: return new Color(1.0f, 0.5f, 1.0f, 0.6f); case IntervalType.Invincible: return new Color(0.5f, 1.0f, 1.0f, 0.6f); case IntervalType.RootMotion: return new Color(1.0f, 0.5f, 0.0f, 0.6f); case IntervalType.Preinput: return new Color(0.0f, 0.5f, 1.0f, 0.6f); case IntervalType.Custom: return new Color(0.7f, 0.7f, 0.7f, 0.6f); default: return new Color(0.7f, 0.7f, 0.7f, 0.6f); } } // 当 targetData 改变时,更新 dataEditor 的引用 private void OnDataChanged() { dataEditor = targetData; } 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) { /* ... 重置拖拽 ... */ } EditorGUIUtility.AddCursorRect(totalArea, MouseCursor.Arrow); return; } // --- 1. (修改) 更新光标 --- UpdateMouseCursor(totalArea, rulerArea, allTracksArea, mousePos, duration, e.control); // --- 2. (修改) 鼠标按下 --- if (e.type == EventType.MouseDown && e.button == 0) { Undo.RecordObject(targetData, "Modify FuncAnim Timeline"); // (修改) 检查事件 (使用 rulerArea) int eventIndex = GetEventAt(rulerArea, mousePos, duration); if (eventIndex != -1) { currentDragMode = DragMode.Event; draggingEventIndex = eventIndex; isDirty = true; e.Use(); return; } // (不变) 检查区间 (int intervalIndex, DragMode dragMode, Rect trackRect) = GetIntervalAt(allTracksArea, mousePos, duration, e.control); if (intervalIndex != -1) { currentDragMode = dragMode; draggingIntervalIndex = intervalIndex; isDirty = true; e.Use(); return; } // ... (重置拖拽) ... } // --- 3. (修改) 鼠标拖拽 --- if (e.type == EventType.MouseDrag && currentDragMode != DragMode.None) { float mouseTime = 0; if (currentDragMode == DragMode.Event) { // (修改) 基于 rulerArea 计算时间 if (rulerArea.width <= 0) return; mouseTime = Mathf.Clamp((mousePos.x - rulerArea.x) / rulerArea.width * duration, 0, duration); var evt = targetData.eventCollection.animEvents[draggingEventIndex]; evt.triggerTime = SnapToFrame(mouseTime, duration); } else // 处理区间 { Rect trackRect = GetTrackRect(allTracksArea, draggingIntervalIndex); if (trackRect.width <= 0) return; mouseTime = Mathf.Clamp((mousePos.x - trackRect.x) / trackRect.width * duration, 0, duration); // --- (核心数据修改) --- // (修改) 计算绝对时间增量 float mouseDeltaTime = (e.delta.x / trackRect.width) * duration; // --- (修改结束) --- var interval = targetData.intervals[draggingIntervalIndex]; switch (currentDragMode) { case DragMode.IntervalMove: // (修改) 使用绝对时间 float newTimeX = interval.timeRange.x + mouseDeltaTime; float newTimeY = interval.timeRange.y + mouseDeltaTime; if (newTimeX < 0) { float offset = -newTimeX; newTimeX += offset; newTimeY += offset; } if (newTimeY > duration) { float offset = newTimeY - duration; newTimeX -= offset; newTimeY -= offset; } interval.timeRange.x = SnapToFrame(newTimeX, duration); interval.timeRange.y = SnapToFrame(newTimeY, duration); break; case DragMode.IntervalLeft: // (修改) 使用绝对时间 float newLeftTime = SnapToFrame(mouseTime, duration); interval.timeRange.x = Mathf.Min(newLeftTime, interval.timeRange.y); break; case DragMode.IntervalRight: // (修改) 使用绝对时间 float newRightTime = SnapToFrame(mouseTime, duration); interval.timeRange.y = Mathf.Max(newRightTime, interval.timeRange.x); break; } targetData.intervals[draggingIntervalIndex] = interval; } EditorUtility.SetDirty(targetData); isDirty = true; e.Use(); } // --- 4. (不变) 鼠标松开 --- if (e.type == EventType.MouseUp && e.button == 0) { currentDragMode = DragMode.None; draggingEventIndex = -1; draggingIntervalIndex = -1; isDirty = true; e.Use(); } } // --- (重构) 辅助方法 --- // (修改) private int GetEventAt(Rect rulerArea, Vector2 mousePos, float duration) { if (targetData.eventCollection.animEvents == null) return -1; for (int i = targetData.eventCollection.animEvents.Count - 1; i >= 0; i--) { float triggerTime = targetData.eventCollection.animEvents[i].triggerTime; float normalizedTime = Mathf.Clamp01(triggerTime / duration); // (修改) 基于 rulerArea float xPos = rulerArea.x + normalizedTime * rulerArea.width; if (Mathf.Abs(mousePos.x - xPos) <= DRAG_HANDLE_WIDTH) { return i; } } return -1; } private Rect GetTrackRect(Rect allTracksArea, int trackIndex) { return new Rect( allTracksArea.x + TRACK_LABEL_WIDTH, allTracksArea.y + (trackIndex * TRACK_HEIGHT), allTracksArea.width - TRACK_LABEL_WIDTH, TRACK_HEIGHT - 2 ); } // (修改) private (int, DragMode, Rect) GetIntervalAt(Rect allTracksArea, Vector2 mousePos, float duration, bool isCtrlPressed) { if (targetData.intervals == null || duration <= 0) return (-1, DragMode.None, Rect.zero); if (!allTracksArea.Contains(mousePos)) return (-1, DragMode.None, Rect.zero); float yInTracks = mousePos.y - allTracksArea.y; int trackIndex = Mathf.FloorToInt(yInTracks / TRACK_HEIGHT); if (trackIndex < 0 || trackIndex >= targetData.intervals.Count) return (-1, DragMode.None, Rect.zero); var interval = targetData.intervals[trackIndex]; Rect trackRect = GetTrackRect(allTracksArea, trackIndex); // --- (核心数据修改) --- // (修改) 使用 timeRange (秒) 并归一化 *用于点击检测* float startX = trackRect.x + (interval.timeRange.x / duration) * trackRect.width; float endX = trackRect.x + (interval.timeRange.y / duration) * trackRect.width; // --- (修改结束) --- if (mousePos.x >= trackRect.x && mousePos.x <= trackRect.xMax) { if (mousePos.x >= startX && mousePos.x <= endX) { if (isCtrlPressed) return (trackIndex, DragMode.IntervalMove, trackRect); if (mousePos.x - startX <= DRAG_HANDLE_WIDTH) return (trackIndex, DragMode.IntervalLeft, trackRect); if (endX - mousePos.x <= DRAG_HANDLE_WIDTH) return (trackIndex, DragMode.IntervalRight, trackRect); return (trackIndex, DragMode.IntervalMove, trackRect); } } return (-1, DragMode.None, Rect.zero); } // (修改) private void UpdateMouseCursor(Rect totalArea, Rect rulerArea, Rect allTracksArea, Vector2 mousePos, float duration, bool isCtrlPressed) { // (修改) 使用 rulerArea if (GetEventAt(rulerArea, mousePos, duration) != -1) { EditorGUIUtility.AddCursorRect(totalArea, MouseCursor.MoveArrow); return; } (int intervalIndex, DragMode dragMode, Rect trackRect) = GetIntervalAt(allTracksArea, mousePos, duration, isCtrlPressed); if (intervalIndex != -1) { if (dragMode == DragMode.IntervalLeft || dragMode == DragMode.IntervalRight) { EditorGUIUtility.AddCursorRect(totalArea, MouseCursor.ResizeHorizontal); } else { EditorGUIUtility.AddCursorRect(totalArea, MouseCursor.MoveArrow); } return; } EditorGUIUtility.AddCursorRect(totalArea, MouseCursor.Arrow); } // 将时间吸附到最近的帧上 private float SnapToFrame(float time, float duration) { if (!targetData.animationClip) return time; float frameRate = targetData.animationClip.frameRate; if (frameRate <= 0) return time; float frameDuration = 1.0f / frameRate; int frame = Mathf.RoundToInt(time / frameDuration); return Mathf.Clamp(frame * frameDuration, 0, duration); } // (全新) 辅助方法:获取事件类型对应的颜色 private Color GetColorForEventType(Type type) { if (eventColorCache.TryGetValue(type, out Color color)) { return color; } // 没找到,通过反射查找 Attribute var attr = type.GetCustomAttribute(); color = attr?.Color ?? Color.red; // 默认颜色 eventColorCache[type] = color; return color; } private void AutoSetupPayloadNames(FuncAnimData data) // <-- (修改) 接收 data 参数 { if (data == null) return; // <-- (新增) 安全检查 // 1. 处理简单的 Payload 列表 AutoNamePayloadList(data.eventCollection.startEvents, "Start"); AutoNamePayloadList(data.eventCollection.disruptionEvents, "Disruption"); AutoNamePayloadList(data.eventCollection.updateEvents, "Update"); AutoNamePayloadList(data.eventCollection.updateUntilEvents, "UpdateUntil"); // 2. 处理嵌套的列表 (timelineEvents) if (data.eventCollection.animEvents != null) // (修改) 使用 data. { AutoNamePayloadList(data.eventCollection.animEvents.Select(e => e.payload).ToList(), "Anim"); } } /// /// 遍历一个Payload列表,为所有eventName为空的条目命名 /// (这个方法无需修改) /// private void AutoNamePayloadList(List list, string prefix) where T : FuncAnimPayloadBase { if (list == null || list.Count == 0) return; var typeCounts = new Dictionary(); foreach (var payload in list) { if (payload == null) continue; Type payloadType = payload.GetType(); typeCounts.TryGetValue(payloadType, out int currentCount); if (string.IsNullOrEmpty(payload.eventName)) { payload.eventName = $"{prefix}{payloadType.Name}{currentCount}"; } typeCounts[payloadType] = currentCount + 1; } } } }