using System.Collections.Generic; using UnityEditor; using UnityEngine; using BaseGames.Animation; namespace BaseGames.Editor { /// /// AnimationEventConfigSO 自定义 Inspector(架构 §AnimationModule)。 /// 功能: /// - 以时间线色块可视化事件分布 /// - 自动检测 Clip 长度漂移(超过 5 帧则显示警告) /// - 验证归一化时间范围 [0, 1] /// - 一键对事件按归一化时间排序 /// [CustomEditor(typeof(AnimationEventConfigSO))] public class EventConfigEditor : UnityEditor.Editor { // ── 事件类型 → 色块颜色映射 ──────────────────────────────────────── private static readonly Dictionary _colorMap = new() { { AnimationEventType.EnableHitBox, new Color(0.9f, 0.2f, 0.2f, 0.8f) }, // 红 { AnimationEventType.DisableHitBox, new Color(0.9f, 0.2f, 0.2f, 0.8f) }, { AnimationEventType.AttackImpact, new Color(0.9f, 0.2f, 0.2f, 0.8f) }, { AnimationEventType.EnableIFrame, new Color(0.2f, 0.8f, 0.2f, 0.8f) }, // 绿 { AnimationEventType.DisableIFrame, new Color(0.2f, 0.8f, 0.2f, 0.8f) }, { AnimationEventType.Footstep, new Color(0.2f, 0.4f, 0.9f, 0.8f) }, // 蓝 { AnimationEventType.PlaySFX, new Color(0.2f, 0.4f, 0.9f, 0.8f) }, { AnimationEventType.LandImpact, new Color(0.2f, 0.4f, 0.9f, 0.8f) }, { AnimationEventType.JumpLaunch, new Color(0.2f, 0.4f, 0.9f, 0.8f) }, { AnimationEventType.EnableParryWindow, new Color(0.9f, 0.8f, 0.1f, 0.8f) }, // 黄 { AnimationEventType.DisableParryWindow,new Color(0.9f, 0.8f, 0.1f, 0.8f) }, { AnimationEventType.TriggerFeedback, new Color(0.6f, 0.2f, 0.9f, 0.8f) }, // 紫 { AnimationEventType.CancelWindowOpen, new Color(0.9f, 0.5f, 0.1f, 0.8f) }, // 橙 { AnimationEventType.CancelWindowClose, new Color(0.9f, 0.5f, 0.1f, 0.8f) }, { AnimationEventType.SpawnProjectile, new Color(0.9f, 0.9f, 0.9f, 0.8f) }, // 白 { AnimationEventType.RoarStart, new Color(0.9f, 0.9f, 0.9f, 0.8f) }, { AnimationEventType.RoarEnd, new Color(0.9f, 0.9f, 0.9f, 0.8f) }, { AnimationEventType.PhaseTwoStart, new Color(0.9f, 0.9f, 0.9f, 0.8f) }, }; private const float TimelineHeight = 24f; private const float MarkerWidth = 3f; private const float DriftThresholdFrames = 5f; public override void OnInspectorGUI() { var config = (AnimationEventConfigSO)target; serializedObject.Update(); // ── 时间线预览 ─────────────────────────────────────────────── EditorGUILayout.LabelField("事件时间线预览", EditorStyles.boldLabel); DrawTimeline(config); EditorGUILayout.Space(4f); // ── 标准字段 ───────────────────────────────────────────────── DrawDefaultInspector(); EditorGUILayout.Space(4f); // ── 验证警告 ───────────────────────────────────────────────── ValidateEntries(config); // ── Clip 长度漂移检测 ───────────────────────────────────────── if (config.targetClip != null && config.ExpectedClipLength > 0f) { float fps = config.targetClip.frameRate; float actualLen = config.targetClip.length; float drift = Mathf.Abs(actualLen - config.ExpectedClipLength) * fps; if (drift > DriftThresholdFrames) { EditorGUILayout.HelpBox( $"⚠ Clip 长度已变化 {drift:F1} 帧(期望 {config.ExpectedClipLength:F3}s," + $"实际 {actualLen:F3}s)。\n请检查事件时机是否需要更新。", MessageType.Warning); } } EditorGUILayout.Space(4f); // ── 操作按钮 ───────────────────────────────────────────────── EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("按时间排序")) { SortEvents(config); } if (config.targetClip != null && GUILayout.Button("记录当前 Clip 长度")) { Undo.RecordObject(config, "记录 Clip 长度"); config.ExpectedClipLength = config.targetClip.length; EditorUtility.SetDirty(config); } EditorGUILayout.EndHorizontal(); serializedObject.ApplyModifiedProperties(); } // ── 时间线绘制 ──────────────────────────────────────────────────── private static void DrawTimeline(AnimationEventConfigSO config) { Rect rect = GUILayoutUtility.GetRect(GUIContent.none, GUIStyle.none, GUILayout.Height(TimelineHeight), GUILayout.ExpandWidth(true)); // 背景轨道 EditorGUI.DrawRect(rect, new Color(0.15f, 0.15f, 0.15f, 1f)); // 标尺刻度(每 10% 一条) for (int i = 0; i <= 10; i++) { float x = rect.x + rect.width * i / 10f; float h = (i % 5 == 0) ? rect.height * 0.6f : rect.height * 0.3f; var tick = new Rect(x, rect.y + rect.height - h, 1f, h); EditorGUI.DrawRect(tick, new Color(0.5f, 0.5f, 0.5f, 0.8f)); } if (config.events == null) return; foreach (var entry in config.events) { float nx = Mathf.Clamp01(entry.normalizedTime); float xPos = rect.x + rect.width * nx - MarkerWidth * 0.5f; var markerRect = new Rect(xPos, rect.y + 2f, MarkerWidth, rect.height - 4f); Color color = _colorMap.TryGetValue(entry.eventType, out var c) ? c : new Color(0.8f, 0.8f, 0.8f, 0.8f); EditorGUI.DrawRect(markerRect, color); } } // ── 验证 ────────────────────────────────────────────────────────── private static void ValidateEntries(AnimationEventConfigSO config) { if (config.events == null) return; for (int i = 0; i < config.events.Length; i++) { float t = config.events[i].normalizedTime; if (t < 0f || t > 1f) { EditorGUILayout.HelpBox( $"事件 [{i}] {config.events[i].eventType}:" + $"normalizedTime = {t:F3} 超出 [0, 1] 范围。", MessageType.Error); } } } // ── 排序 ────────────────────────────────────────────────────────── private static void SortEvents(AnimationEventConfigSO config) { if (config.events == null || config.events.Length < 2) return; Undo.RecordObject(config, "排序动画事件"); System.Array.Sort(config.events, (a, b) => a.normalizedTime.CompareTo(b.normalizedTime)); EditorUtility.SetDirty(config); } } }