Files
ft/Client/Assets/Editor/BossTimelineBinderEditor.cs
2026-06-29 21:18:33 +08:00

519 lines
16 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.
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);
}
}