519 lines
16 KiB
C#
519 lines
16 KiB
C#
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<string> bossNames = new List<string>();
|
||
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<string> actionNames = new List<string>();
|
||
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<PlayableDirector>();
|
||
if (entry.director == null)
|
||
{
|
||
entry.director = child.GetComponentInChildren<PlayableDirector>(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<PlayableAsset> strongMatches = new List<PlayableAsset>();
|
||
List<PlayableAsset> weakMatches = new List<PlayableAsset>();
|
||
|
||
foreach (string guid in guids)
|
||
{
|
||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||
var asset = AssetDatabase.LoadAssetAtPath<PlayableAsset>(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);
|
||
}
|
||
} |