Files
MinFt/Client/Assets/Editor/AutoBindTimelineByFieldNameMenu.cs
Liubing\LB 3d8d4d18f3 first commit
先修复一下,错误的场景

删除不必要的钓场资产

修复into场景

update:更新meta文件,修复报错

update:修复资源

修复第一章节建造

修复建造第三章

update:删除多余内容

update:更新README.md

update:还原图标

update:更新README

update:更新配置
2026-04-28 02:17:22 +08:00

340 lines
11 KiB
C#

#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEditor;
#if UNITY_2021_2_OR_NEWER
using PrefabStageUtility = UnityEditor.SceneManagement.PrefabStageUtility;
#else
using PrefabStageUtility = UnityEditor.Experimental.SceneManagement.PrefabStageUtility;
#endif
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
public static class AutoBindTimelineByFieldNameMenu
{
private const string MenuPath = "GameObject/Timeline/按字段名自动挂载 Timeline";
private static readonly string[] PreferredFolderKeywords = { "timeline", "playable" };
[MenuItem(MenuPath, false, 49)]
private static void AutoBindTimelinesByFieldName()
{
if (!TryGetTargetBehaviours(Selection.activeGameObject, out var behaviours, out var error))
{
Debug.LogError(error);
return;
}
if (!TryGetSearchFolders(Selection.activeGameObject, out var searchFolders, out error))
{
Debug.LogError(error, Selection.activeGameObject);
return;
}
var changedCount = 0;
var unchangedCount = 0;
var missingCount = 0;
var duplicateCount = 0;
var details = new StringBuilder();
foreach (var behaviour in behaviours)
{
var serializedObject = new SerializedObject(behaviour);
var componentChanged = false;
Undo.RecordObject(behaviour, "Auto Bind Timelines By Field Name");
foreach (var field in GetPlayableAssetFields(behaviour.GetType()))
{
var property = serializedObject.FindProperty(field.Name);
if (property == null || property.propertyType != SerializedPropertyType.ObjectReference)
{
continue;
}
var searchResult = FindTimeline(field.Name, searchFolders);
if (searchResult.Status == TimelineSearchStatus.Missing)
{
missingCount++;
details.AppendLine($"未找到: {GetComponentLabel(behaviour)}.{field.Name}");
continue;
}
if (searchResult.Status == TimelineSearchStatus.Duplicate)
{
duplicateCount++;
details.AppendLine($"重名冲突: {GetComponentLabel(behaviour)}.{field.Name}");
foreach (var path in searchResult.Paths)
{
details.AppendLine($" - {path}");
}
continue;
}
if (property.objectReferenceValue == searchResult.Asset)
{
unchangedCount++;
continue;
}
property.objectReferenceValue = searchResult.Asset;
changedCount++;
componentChanged = true;
details.AppendLine($"已挂载: {GetComponentLabel(behaviour)}.{field.Name} -> {searchResult.Paths[0]}");
}
serializedObject.ApplyModifiedProperties();
if (componentChanged)
{
EditorUtility.SetDirty(behaviour);
}
}
var summary = new StringBuilder();
summary.AppendLine($"Timeline 挂载完成: 修改 {changedCount},未变更 {unchangedCount},未找到 {missingCount},重名冲突 {duplicateCount}。");
summary.AppendLine($"搜索目录: {string.Join(" | ", searchFolders)}");
if (details.Length > 0)
{
summary.AppendLine(details.ToString().TrimEnd());
}
if (missingCount > 0 || duplicateCount > 0)
{
Debug.LogError(summary.ToString(), Selection.activeGameObject);
return;
}
Debug.Log(summary.ToString(), Selection.activeGameObject);
}
[MenuItem(MenuPath, true)]
private static bool ValidateAutoBindTimelinesByFieldName()
{
return TryGetTargetBehaviours(Selection.activeGameObject, out _, out _);
}
private static bool TryGetTargetBehaviours(GameObject selectedObject, out List<MonoBehaviour> behaviours, out string error)
{
behaviours = new List<MonoBehaviour>();
error = null;
if (selectedObject == null)
{
error = "未选中任何 GameObject。";
return false;
}
behaviours = selectedObject
.GetComponentsInChildren<MonoBehaviour>(true)
.Where(behaviour => behaviour != null && GetPlayableAssetFields(behaviour.GetType()).Any())
.ToList();
if (behaviours.Count > 0)
{
return true;
}
error = $"选中对象 `{selectedObject.name}` 层级下未找到包含序列化 PlayableAsset 字段的组件。";
return false;
}
private static bool TryGetSearchFolders(GameObject selectedObject, out List<string> searchFolders, out string error)
{
searchFolders = new List<string>();
error = null;
var prefabPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(selectedObject);
if (string.IsNullOrEmpty(prefabPath))
{
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
if (prefabStage != null)
{
prefabPath = prefabStage.assetPath;
}
}
if (string.IsNullOrEmpty(prefabPath))
{
error = "当前选择对象不在 Prefab 实例或 Prefab 编辑模式中,无法确定搜索目录。";
return false;
}
var prefabFolder = NormalizePath(Path.GetDirectoryName(prefabPath));
if (string.IsNullOrEmpty(prefabFolder))
{
error = $"Prefab 路径 `{prefabPath}` 无法转换为有效目录。";
return false;
}
var fallbackFolders = new List<string>();
var currentFolder = prefabFolder;
while (!string.IsNullOrEmpty(currentFolder) && currentFolder.StartsWith("Assets", StringComparison.Ordinal))
{
fallbackFolders.Add(currentFolder);
var preferredFolders = AssetDatabase
.GetSubFolders(currentFolder)
.Where(IsPreferredSearchFolder)
.Select(NormalizePath)
.OrderBy(path => path.Count(ch => ch == '/'))
.ThenBy(path => path, StringComparer.Ordinal)
.ToList();
foreach (var folder in preferredFolders)
{
AddUnique(searchFolders, folder);
}
AddUnique(searchFolders, currentFolder);
if (string.Equals(currentFolder, "Assets", StringComparison.Ordinal))
{
break;
}
currentFolder = NormalizePath(Path.GetDirectoryName(currentFolder));
}
if (searchFolders.Count == 0)
{
foreach (var folder in fallbackFolders)
{
AddUnique(searchFolders, folder);
}
}
if (searchFolders.Count == 0)
{
error = $"Prefab 路径 `{prefabPath}` 无法生成有效搜索目录。";
return false;
}
return true;
}
private static IEnumerable<FieldInfo> GetPlayableAssetFields(Type type)
{
return type
.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(field =>
typeof(PlayableAsset).IsAssignableFrom(field.FieldType) &&
IsSerializedField(field));
}
private static bool IsSerializedField(FieldInfo field)
{
if (field.IsDefined(typeof(NonSerializedAttribute), true))
{
return false;
}
return field.IsPublic || field.IsDefined(typeof(SerializeField), true);
}
private static TimelineSearchResult FindTimeline(string fieldName, IReadOnlyList<string> searchFolders)
{
foreach (var folder in searchFolders)
{
var paths = AssetDatabase
.FindAssets($"{fieldName} t:{nameof(TimelineAsset)}", new[] { folder })
.Select(AssetDatabase.GUIDToAssetPath)
.Where(path => string.Equals(Path.GetFileNameWithoutExtension(path), fieldName, StringComparison.Ordinal))
.Distinct()
.ToList();
if (paths.Count == 0)
{
continue;
}
if (paths.Count > 1)
{
return TimelineSearchResult.CreateDuplicate(paths);
}
var asset = AssetDatabase.LoadAssetAtPath<TimelineAsset>(paths[0]);
if (asset == null)
{
return TimelineSearchResult.CreateMissing();
}
return TimelineSearchResult.CreateFound(asset, paths[0]);
}
return TimelineSearchResult.CreateMissing();
}
private static bool IsPreferredSearchFolder(string folderPath)
{
var folderName = Path.GetFileName(folderPath);
if (string.IsNullOrEmpty(folderName))
{
return false;
}
return PreferredFolderKeywords.Any(keyword =>
folderName.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0);
}
private static void AddUnique(List<string> folders, string folder)
{
if (!string.IsNullOrEmpty(folder) && !folders.Contains(folder))
{
folders.Add(folder);
}
}
private static string NormalizePath(string path)
{
return string.IsNullOrEmpty(path) ? string.Empty : path.Replace('\\', '/');
}
private static string GetComponentLabel(MonoBehaviour behaviour)
{
return $"{behaviour.gameObject.name}/{behaviour.GetType().Name}";
}
private enum TimelineSearchStatus
{
Missing,
Found,
Duplicate,
}
private readonly struct TimelineSearchResult
{
public readonly TimelineSearchStatus Status;
public readonly TimelineAsset Asset;
public readonly IReadOnlyList<string> Paths;
private TimelineSearchResult(TimelineSearchStatus status, TimelineAsset asset, IReadOnlyList<string> paths)
{
Status = status;
Asset = asset;
Paths = paths;
}
public static TimelineSearchResult CreateMissing()
{
return new TimelineSearchResult(TimelineSearchStatus.Missing, null, Array.Empty<string>());
}
public static TimelineSearchResult CreateFound(TimelineAsset asset, string path)
{
return new TimelineSearchResult(TimelineSearchStatus.Found, asset, new[] { path });
}
public static TimelineSearchResult CreateDuplicate(IReadOnlyList<string> paths)
{
return new TimelineSearchResult(TimelineSearchStatus.Duplicate, null, paths);
}
}
}
#endif