先修复一下,错误的场景 删除不必要的钓场资产 修复into场景 update:更新meta文件,修复报错 update:修复资源 修复第一章节建造 修复建造第三章 update:删除多余内容 update:更新README.md update:还原图标 update:更新README update:更新配置
340 lines
11 KiB
C#
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
|