using System.Collections.Generic; using System.Text.RegularExpressions; using UnityEditor; using UnityEngine; using UnityEngine.Playables; [CustomEditor(typeof(BossTimelineBinder))] public class BossTimelineBinderEditor : UnityEditor.Editor { private BossTimelineBinder binder; private void OnEnable() { binder = (BossTimelineBinder)target; } public override void OnInspectorGUI() { serializedObject.Update(); DrawDefaultInspector(); EditorGUILayout.Space(12); EditorGUILayout.LabelField("Boss Timeline Helper", EditorStyles.boldLabel); DrawRootFolderHint(); EditorGUILayout.Space(8); DrawAutoButtons(); EditorGUILayout.Space(12); DrawSelectionUI(); EditorGUILayout.Space(8); DrawActionButtons(); serializedObject.ApplyModifiedProperties(); } private void DrawRootFolderHint() { if (string.IsNullOrEmpty(binder.timelineRootFolder)) { EditorGUILayout.HelpBox("先填写 timelineRootFolder。", MessageType.Info); } } private void DrawAutoButtons() { if (GUILayout.Button("Auto Collect Bosses", GUILayout.Height(28))) { AutoCollectBosses(); } if (GUILayout.Button("Collect Actions For Current Boss", GUILayout.Height(24))) { CollectActionsForCurrentBoss(); } if (GUILayout.Button("Collect Actions For All Bosses", GUILayout.Height(24))) { CollectActionsForAllBosses(); } if (GUILayout.Button("Auto Fill Timeline Keywords For All Bosses", GUILayout.Height(24))) { AutoFillTimelineKeywordsForAllBosses(); } } private void DrawSelectionUI() { if (binder.bosses == null || binder.bosses.Count == 0) { EditorGUILayout.HelpBox("还没有 Boss 数据。先点 Auto Collect Bosses。", MessageType.Warning); return; } List bossNames = new List(); for (int i = 0; i < binder.bosses.Count; i++) { string name = string.IsNullOrEmpty(binder.bosses[i].bossName) ? $"Boss {i}" : binder.bosses[i].bossName; bossNames.Add(name); } binder.selectedBossIndex = Mathf.Clamp(binder.selectedBossIndex, 0, binder.bosses.Count - 1); binder.selectedBossIndex = EditorGUILayout.Popup("Current Boss", binder.selectedBossIndex, bossNames.ToArray()); var currentBoss = binder.bosses[binder.selectedBossIndex]; if (currentBoss.actionNodes == null || currentBoss.actionNodes.Count == 0) { EditorGUILayout.HelpBox("当前 Boss 没有动作数据。先点 Collect Actions For Current Boss。", MessageType.Warning); return; } List actionNames = new List(); for (int i = 0; i < currentBoss.actionNodes.Count; i++) { string name = currentBoss.actionNodes[i].actionName; string keyword = currentBoss.actionNodes[i].timelineKeyword; if (!string.IsNullOrEmpty(keyword)) name += $" ({keyword})"; actionNames.Add(name); } binder.selectedActionIndex = Mathf.Clamp(binder.selectedActionIndex, 0, currentBoss.actionNodes.Count - 1); binder.selectedActionIndex = EditorGUILayout.Popup("Current Action", binder.selectedActionIndex, actionNames.ToArray()); } private void DrawActionButtons() { using (new EditorGUI.DisabledScope(binder.bosses == null || binder.bosses.Count == 0)) { if (GUILayout.Button("Apply Selected Boss + Action", GUILayout.Height(32))) { ApplySelectedBossAndAction(); } if (GUILayout.Button("Select Matched Timeline Asset In Project", GUILayout.Height(24))) { SelectMatchedTimelineAsset(); } if (GUILayout.Button("Ping Current Boss Root", GUILayout.Height(24))) { PingCurrentBossRoot(); } if (GUILayout.Button("Ping Current Action Node", GUILayout.Height(24))) { PingCurrentActionNode(); } } } private void AutoCollectBosses() { Undo.RecordObject(binder, "Auto Collect Bosses"); binder.bosses.Clear(); Transform root = binder.transform; for (int i = 0; i < root.childCount; i++) { Transform child = root.GetChild(i); if (child == null) continue; var entry = new BossTimelineBinder.BossEntry(); entry.bossName = child.name; entry.bossRoot = child.gameObject; entry.director = child.GetComponent(); if (entry.director == null) { entry.director = child.GetComponentInChildren(true); } entry.actionContainer = FindActionContainer(child); binder.bosses.Add(entry); } binder.selectedBossIndex = 0; binder.selectedActionIndex = 0; EditorUtility.SetDirty(binder); Debug.Log($"已自动收集 {binder.bosses.Count} 个 Boss。"); } private Transform FindActionContainer(Transform bossRoot) { if (bossRoot == null) return null; Transform direct = bossRoot.Find("boss"); if (direct != null) return direct; for (int i = 0; i < bossRoot.childCount; i++) { Transform child = bossRoot.GetChild(i); if (Normalize(child.name) == "boss") return child; } return null; } private void CollectActionsForCurrentBoss() { if (binder.bosses == null || binder.bosses.Count == 0) { Debug.LogWarning("还没有 Boss 数据。"); return; } int bossIndex = Mathf.Clamp(binder.selectedBossIndex, 0, binder.bosses.Count - 1); var boss = binder.bosses[bossIndex]; CollectActionsForBoss(boss); binder.selectedActionIndex = 0; EditorUtility.SetDirty(binder); } private void CollectActionsForAllBosses() { if (binder.bosses == null || binder.bosses.Count == 0) { Debug.LogWarning("还没有 Boss 数据。"); return; } Undo.RecordObject(binder, "Collect Actions For All Bosses"); for (int i = 0; i < binder.bosses.Count; i++) { CollectActionsForBoss(binder.bosses[i]); } binder.selectedActionIndex = 0; EditorUtility.SetDirty(binder); Debug.Log("已为所有 Boss 收集动作节点。"); } private void CollectActionsForBoss(BossTimelineBinder.BossEntry boss) { if (boss == null) return; if (boss.actionContainer == null) { Debug.LogWarning($"Boss [{boss.bossName}] 没找到 actionContainer。"); return; } boss.actionNodes.Clear(); for (int i = 0; i < boss.actionContainer.childCount; i++) { Transform child = boss.actionContainer.GetChild(i); if (child == null) continue; var node = new BossTimelineBinder.BossActionNode(); node.actionName = child.name; node.actionNode = child.gameObject; node.timelineKeyword = GetDefaultTimelineKeyword(child.name); boss.actionNodes.Add(node); } Debug.Log($"Boss [{boss.bossName}] 已收集 {boss.actionNodes.Count} 个动作节点。"); } private void AutoFillTimelineKeywordsForAllBosses() { if (binder.bosses == null || binder.bosses.Count == 0) { Debug.LogWarning("还没有 Boss 数据。"); return; } Undo.RecordObject(binder, "Auto Fill Timeline Keywords"); for (int i = 0; i < binder.bosses.Count; i++) { var boss = binder.bosses[i]; if (boss.actionNodes == null) continue; for (int j = 0; j < boss.actionNodes.Count; j++) { var action = boss.actionNodes[j]; action.timelineKeyword = GetDefaultTimelineKeyword(action.actionName); } } EditorUtility.SetDirty(binder); Debug.Log("已为所有 Boss 自动填写 timelineKeyword。"); } private string GetDefaultTimelineKeyword(string actionName) { string normalized = Normalize(actionName); if (normalized == "idle") return "idle"; if (normalized == "attack") return "attack"; if (normalized == "show") return "show"; if (normalized == "die") return "death"; if (normalized == "hit") return "attacked01"; if (normalized == "hit02" || normalized == "hit2") return "attacked02"; return normalized; } private void ApplySelectedBossAndAction() { if (binder.bosses == null || binder.bosses.Count == 0) { Debug.LogWarning("没有 Boss 数据。"); return; } int bossIndex = Mathf.Clamp(binder.selectedBossIndex, 0, binder.bosses.Count - 1); var boss = binder.bosses[bossIndex]; if (boss.bossRoot == null) { Debug.LogWarning("当前 Boss 的 bossRoot 为空。"); return; } if (boss.director == null) { Debug.LogWarning($"Boss [{boss.bossName}] 的 PlayableDirector 为空。"); return; } if (boss.actionNodes == null || boss.actionNodes.Count == 0) { Debug.LogWarning($"Boss [{boss.bossName}] 没有动作节点。"); return; } int actionIndex = Mathf.Clamp(binder.selectedActionIndex, 0, boss.actionNodes.Count - 1); var action = boss.actionNodes[actionIndex]; if (action.actionNode == null) { Debug.LogWarning("当前动作节点为空。"); return; } // 1. 切换 Boss 显示 for (int i = 0; i < binder.bosses.Count; i++) { var otherBoss = binder.bosses[i]; if (otherBoss.bossRoot == null) continue; Undo.RecordObject(otherBoss.bossRoot, "Toggle Boss Root"); otherBoss.bossRoot.SetActive(i == bossIndex); EditorUtility.SetDirty(otherBoss.bossRoot); } // 2. 切换动作节点显示 for (int i = 0; i < boss.actionNodes.Count; i++) { var node = boss.actionNodes[i].actionNode; if (node == null) continue; Undo.RecordObject(node, "Toggle Action Node"); node.SetActive(i == actionIndex); EditorUtility.SetDirty(node); } // 3. 查找并挂 Timeline PlayableAsset timelineAsset = FindBestTimelineAssetForBossAction(boss, action); if (timelineAsset == null) { Debug.LogWarning($"未找到匹配的 Timeline。Boss=[{boss.bossName}] Action=[{action.actionName}] Keyword=[{action.timelineKeyword}]"); return; } Undo.RecordObject(boss.director, "Assign Timeline Asset"); boss.director.playableAsset = timelineAsset; EditorUtility.SetDirty(boss.director); boss.director.RebuildGraph(); boss.director.time = 0; boss.director.Evaluate(); Selection.activeObject = boss.director.gameObject; Debug.Log($"已应用 Boss=[{boss.bossName}] Action=[{action.actionName}] Timeline=[{timelineAsset.name}]"); } private PlayableAsset FindBestTimelineAssetForBossAction(BossTimelineBinder.BossEntry boss, BossTimelineBinder.BossActionNode action) { if (string.IsNullOrEmpty(binder.timelineRootFolder)) { Debug.LogWarning("timelineRootFolder 为空。"); return null; } string[] guids = AssetDatabase.FindAssets("t:TimelineAsset", new[] { binder.timelineRootFolder }); if (guids == null || guids.Length == 0) { Debug.LogWarning($"在目录中没有找到 TimelineAsset:{binder.timelineRootFolder}"); return null; } int bossNumber = ExtractTrailingNumber(boss.bossName); string keyword = Normalize(action.timelineKeyword); string bossNumberPlain = bossNumber > 0 ? bossNumber.ToString() : ""; string bossNumberPad2 = bossNumber > 0 ? bossNumber.ToString("00") : ""; List strongMatches = new List(); List weakMatches = new List(); foreach (string guid in guids) { string path = AssetDatabase.GUIDToAssetPath(guid); var asset = AssetDatabase.LoadAssetAtPath(path); if (asset == null) continue; string assetName = Normalize(asset.name); // 排除 player if (assetName.Contains("player")) continue; // 必须包含动作关键字 if (!string.IsNullOrEmpty(keyword) && !assetName.Contains(keyword)) continue; bool bossMatch = false; if (!string.IsNullOrEmpty(bossNumberPad2) && assetName.Contains("boss" + bossNumberPad2)) bossMatch = true; else if (!string.IsNullOrEmpty(bossNumberPlain) && assetName.Contains("boss" + bossNumberPlain)) bossMatch = true; if (bossMatch) strongMatches.Add(asset); else weakMatches.Add(asset); } if (strongMatches.Count > 0) return strongMatches[0]; if (weakMatches.Count > 0) return weakMatches[0]; return null; } private int ExtractTrailingNumber(string name) { if (string.IsNullOrEmpty(name)) return -1; Match match = Regex.Match(name, @"(\d+)$"); if (!match.Success) return -1; int value; if (int.TryParse(match.Value, out value)) return value; return -1; } private string Normalize(string value) { if (string.IsNullOrEmpty(value)) return string.Empty; return value.Replace("_", "").Replace(" ", "").ToLowerInvariant(); } private void SelectMatchedTimelineAsset() { if (binder.bosses == null || binder.bosses.Count == 0) return; int bossIndex = Mathf.Clamp(binder.selectedBossIndex, 0, binder.bosses.Count - 1); var boss = binder.bosses[bossIndex]; if (boss.actionNodes == null || boss.actionNodes.Count == 0) return; int actionIndex = Mathf.Clamp(binder.selectedActionIndex, 0, boss.actionNodes.Count - 1); var action = boss.actionNodes[actionIndex]; PlayableAsset asset = FindBestTimelineAssetForBossAction(boss, action); if (asset == null) { Debug.LogWarning("没有找到匹配的 TimelineAsset。"); return; } Selection.activeObject = asset; EditorGUIUtility.PingObject(asset); } private void PingCurrentBossRoot() { if (binder.bosses == null || binder.bosses.Count == 0) return; int bossIndex = Mathf.Clamp(binder.selectedBossIndex, 0, binder.bosses.Count - 1); var boss = binder.bosses[bossIndex]; if (boss.bossRoot == null) return; Selection.activeObject = boss.bossRoot; EditorGUIUtility.PingObject(boss.bossRoot); } private void PingCurrentActionNode() { if (binder.bosses == null || binder.bosses.Count == 0) return; int bossIndex = Mathf.Clamp(binder.selectedBossIndex, 0, binder.bosses.Count - 1); var boss = binder.bosses[bossIndex]; if (boss.actionNodes == null || boss.actionNodes.Count == 0) return; int actionIndex = Mathf.Clamp(binder.selectedActionIndex, 0, boss.actionNodes.Count - 1); var action = boss.actionNodes[actionIndex]; if (action.actionNode == null) return; Selection.activeObject = action.actionNode; EditorGUIUtility.PingObject(action.actionNode); } }