Files
2026-05-10 11:47:55 -04:00

574 lines
23 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 UnityEditor.Callbacks;
namespace SLSUtilities.FunctionalAnimation
{
public class FuncAnimEditorWindow : OdinEditorWindow
{
// 定义一个菜单项来打开这个窗口
[MenuItem("Tools/SLS Utilities/Functional Animation Editor")]
private static void OpenWindow()
{
GetWindow<FuncAnimEditorWindow>().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<Type, Color> eventColorCache = new Dictionary<Type, Color>();
// ----------------- 自定义时间轴 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<FuncAnimEditorWindow>();
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.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.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<EventColorAttribute>();
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");
}
}
/// <summary>
/// 遍历一个Payload列表为所有eventName为空的条目命名
/// (这个方法无需修改)
/// </summary>
private void AutoNamePayloadList<T>(List<T> list, string prefix) where T : FuncAnimPayloadBase
{
if (list == null || list.Count == 0) return;
var typeCounts = new Dictionary<Type, int>();
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;
}
}
}
}
#endif