feat: add collect asset search to Bundle Collector

This commit is contained in:
何冠峰
2026-05-25 16:38:58 +08:00
parent 92e34fb8b5
commit b31bb1bcfb
10 changed files with 454 additions and 13 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 07c436f0dfa2bcf43b12628aed2aa91c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,52 @@
namespace YooAsset.Editor
{
/// <summary>
/// 收集资源搜索结果
/// </summary>
public class CollectAssetSearchResult
{
/// <summary>
/// 命中的收集器分组
/// </summary>
public BundleCollectorGroup Group { get; }
/// <summary>
/// 命中的收集器分组索引
/// </summary>
public int GroupIndex { get; }
/// <summary>
/// 命中的收集器
/// </summary>
public BundleCollector Collector { get; }
/// <summary>
/// 命中的收集器索引
/// </summary>
public int CollectorIndex { get; }
/// <summary>
/// 命中的资源路径
/// </summary>
public string AssetPath { get; }
/// <summary>
/// 构建收集资源搜索结果
/// </summary>
/// <param name="group">命中的收集器分组</param>
/// <param name="groupIndex">命中的收集器分组索引</param>
/// <param name="collector">命中的收集器</param>
/// <param name="collectorIndex">命中的收集器索引</param>
/// <param name="assetPath">命中的资源路径</param>
public CollectAssetSearchResult(BundleCollectorGroup group, int groupIndex,
BundleCollector collector, int collectorIndex, string assetPath)
{
Group = group;
GroupIndex = groupIndex;
Collector = collector;
CollectorIndex = collectorIndex;
AssetPath = assetPath;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ff1a09e6aca8b104c9b256885b06130f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,148 @@
using System;
using UnityEngine;
using UnityEditor;
namespace YooAsset.Editor
{
/// <summary>
/// 收集资源搜索工具类
/// </summary>
public static class CollectAssetSearchUtility
{
/// <summary>
/// 验证搜索路径
/// </summary>
/// <param name="input">搜索路径</param>
/// <returns>搜索路径错误类型</returns>
public static ECollectAssetSearchError ValidateSearchPath(string input)
{
if (string.IsNullOrEmpty(input))
return ECollectAssetSearchError.InputPathEmpty;
if (input.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase) == false)
return ECollectAssetSearchError.InputPathMissingAssetsPrefix;
if (input.EndsWith("/"))
return ECollectAssetSearchError.InputPathEndsWithSlash;
string fileName = System.IO.Path.GetFileName(input);
if (fileName.Contains(".") == false)
return ECollectAssetSearchError.InputPathMissingExtension;
if (AssetDatabase.IsValidFolder(input))
return ECollectAssetSearchError.InputPathIsFolder;
string guid = AssetDatabase.AssetPathToGUID(input);
if (string.IsNullOrEmpty(guid))
return ECollectAssetSearchError.AssetPathNotExists;
return ECollectAssetSearchError.None;
}
/// <summary>
/// 获取搜索路径错误提示信息
/// </summary>
/// <param name="error">搜索路径错误类型</param>
/// <param name="input">搜索路径</param>
/// <returns>错误提示信息</returns>
public static string GetSearchPathErrorMessage(ECollectAssetSearchError error, string input)
{
switch (error)
{
case ECollectAssetSearchError.InputPathEmpty:
return "Please enter an asset path.";
case ECollectAssetSearchError.InputPathMissingAssetsPrefix:
return "Path must start with Assets/.";
case ECollectAssetSearchError.InputPathEndsWithSlash:
return "Please enter a file path. Do not end with /.";
case ECollectAssetSearchError.InputPathMissingExtension:
return "Path is missing a file extension (e.g. .prefab, .png, .mat).";
case ECollectAssetSearchError.InputPathIsFolder:
return "Please enter an asset file path, not a folder path.";
case ECollectAssetSearchError.AssetPathNotExists:
return $"Asset not found: {input}";
default:
return "Invalid input format.";
}
}
/// <summary>
/// 在指定 Package 中搜索资源路径,找到第一个命中结果即返回
/// </summary>
/// <param name="package">搜索的资源包裹</param>
/// <param name="assetPath">资源路径</param>
/// <returns>搜索结果,如果未找到返回 null</returns>
public static CollectAssetSearchResult SearchAssetPath(BundleCollectorPackage package, string assetPath)
{
if (ValidateSearchPath(assetPath) != ECollectAssetSearchError.None)
return null;
IAssetIgnoreRule ignoreRule = BundleCollectorSettingData.GetAssetIgnoreRuleInstance(package.IgnoreRuleName);
var command = new CollectCommand(package.PackageName, ignoreRule);
command.SetFlag(ECollectFlags.IgnoreGetDependencies, true);
command.UniqueBundleName = BundleCollectorSettingData.Setting.UniqueBundleName;
command.EnableAddressable = package.EnableAddressable;
command.SupportExtensionless = package.SupportExtensionless;
command.LocationToLower = package.LocationToLower;
command.IncludeAssetGUID = package.IncludeAssetGUID;
command.AutoCollectShaders = package.AutoCollectShaders;
for (int groupIndex = 0; groupIndex < package.Groups.Count; groupIndex++)
{
var group = package.Groups[groupIndex];
for (int collectIndex = 0; collectIndex < group.Collectors.Count; collectIndex++)
{
var collector = group.Collectors[collectIndex];
// 判断收集器是否可能收集指定资源
if (IsCandidateCollector(collector, assetPath) == false)
continue;
try
{
// 检测配置是否有效
collector.CheckConfigError();
// 收集有效资源信息
var collectAssets = collector.GetAllCollectAssets(command, group);
foreach (var collectAsset in collectAssets)
{
if (string.Equals(collectAsset.AssetInfo.AssetPath, assetPath, StringComparison.OrdinalIgnoreCase))
{
return new CollectAssetSearchResult(group, groupIndex, collector, collectIndex, assetPath);
}
}
}
catch (Exception e)
{
Debug.LogError($"Invalid collector : {collector.CollectPath}, error: {e.Message}");
}
}
}
// 未找到匹配资源
return null;
}
/// <summary>
/// 判断收集器是否可能收集指定资源
/// </summary>
/// <param name="collector">收集器</param>
/// <param name="assetPath">资源路径</param>
/// <returns>如果收集器可能收集该资源返回 true</returns>
private static bool IsCandidateCollector(BundleCollector collector, string assetPath)
{
if (string.IsNullOrEmpty(collector.CollectPath))
return false;
if (AssetDatabase.IsValidFolder(collector.CollectPath))
{
string folderPath = collector.CollectPath.TrimEnd('/') + "/";
return assetPath.StartsWith(folderPath, StringComparison.OrdinalIgnoreCase);
}
// 注意:资源收集器也可能直接配置的单个资源路径
return string.Equals(assetPath, collector.CollectPath, StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6c8bc2931bda7884198893f1d2e8f364
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,44 @@
namespace YooAsset.Editor
{
/// <summary>
/// 搜索错误类型
/// </summary>
public enum ECollectAssetSearchError
{
/// <summary>
/// 无错误
/// </summary>
None,
/// <summary>
/// 输入路径为空
/// </summary>
InputPathEmpty,
/// <summary>
/// 输入路径缺少 Assets/ 路径前缀
/// </summary>
InputPathMissingAssetsPrefix,
/// <summary>
/// 输入路径以斜杠结尾
/// </summary>
InputPathEndsWithSlash,
/// <summary>
/// 输入路径缺少文件扩展名
/// </summary>
InputPathMissingExtension,
/// <summary>
/// 输入路径的是文件夹路径
/// </summary>
InputPathIsFolder,
/// <summary>
/// 资源文件不存在
/// </summary>
AssetPathNotExists,
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9a2c8a878d6041b4d8bad2a2d2f1896d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -60,21 +60,22 @@ namespace YooAsset.Editor
/// <returns>如果收集器配置有效返回 true</returns>
public bool IsValid()
{
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(CollectPath) == null)
string assetGUID = AssetDatabase.AssetPathToGUID(CollectPath);
if (string.IsNullOrEmpty(assetGUID))
return false;
if (CollectorType == ECollectorType.None)
return false;
if (BundleCollectorSettingData.HasAddressRuleName(AddressRuleName) == false)
return false;
if (BundleCollectorSettingData.HasBundlePackRuleName(PackRuleName) == false)
return false;
if (BundleCollectorSettingData.HasAssetFilterRuleName(FilterRuleName) == false)
return false;
if (BundleCollectorSettingData.HasAddressRuleName(AddressRuleName) == false)
return false;
return true;
}

View File

@@ -25,6 +25,8 @@ namespace YooAsset.Editor
window.minSize = new Vector2(800, 600);
}
private const string PlaceholderClass = "search-placeholder";
private Button _saveButton;
private List<string> _collectorTypeList;
private List<RuleDisplayName> _groupActiveRuleList;
@@ -35,6 +37,11 @@ namespace YooAsset.Editor
private VisualElement _helpBoxContainer;
private ToolbarSearchField _searchField;
private TextField _searchTextField;
private Button _searchButton;
private Label _searchResultLabel;
private Button _globalSettingsButton;
private Button _packageSettingsButton;
@@ -66,6 +73,8 @@ namespace YooAsset.Editor
private ScrollView _collectorScrollView;
private PopupField<RuleDisplayName> _activeRulePopupField;
private string _highlightAssetPath;
private int _highlightCollectorIndex = -1;
private int _lastModifyPackageIndex = 0;
private int _lastModifyGroupIndex = 0;
private bool _showGlobalSettings = false;
@@ -219,6 +228,44 @@ namespace YooAsset.Editor
_saveButton = root.Q<Button>("SaveButton");
_saveButton.clicked += OnSaveButtonClicked;
// 搜索相关
_searchField = root.Q<ToolbarSearchField>("SearchField");
_searchTextField = _searchField.Q<TextField>();
_searchTextField.RegisterCallback<FocusInEvent>(evt =>
{
if (_searchTextField.ClassListContains(PlaceholderClass))
{
_searchField.value = string.Empty;
ClearSearchPlaceholder();
}
});
_searchTextField.RegisterCallback<FocusOutEvent>(evt =>
{
if (string.IsNullOrEmpty(_searchField.value))
ApplySearchPlaceholder();
});
_searchField.RegisterCallback<DragUpdatedEvent>(evt =>
{
if (DragAndDrop.objectReferences.Length > 0)
DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
});
_searchField.RegisterCallback<DragPerformEvent>(evt =>
{
if (DragAndDrop.objectReferences.Length > 0)
{
string assetPath = AssetDatabase.GetAssetPath(DragAndDrop.objectReferences[0]);
if (string.IsNullOrEmpty(assetPath) == false)
{
_searchField.value = assetPath;
ClearSearchPlaceholder();
}
}
});
ApplySearchPlaceholder();
_searchButton = root.Q<Button>("SearchButton");
_searchButton.clicked += OnSearchButtonClicked;
_searchResultLabel = root.Q<Label>("SearchResultLabel");
// 包裹容器
_packageContainer = root.Q("PackageContainer");
@@ -423,6 +470,8 @@ namespace YooAsset.Editor
private void RefreshWindow()
{
_highlightAssetPath = null;
_highlightCollectorIndex = -1;
_groupContainer.visible = false;
_collectorContainer.visible = false;
@@ -455,6 +504,54 @@ namespace YooAsset.Editor
{
BundleCollectorSettingData.SaveFile();
}
private void OnSearchButtonClicked()
{
_highlightAssetPath = null;
_highlightCollectorIndex = -1;
FillCollectorViewData();
string searchInput = GetSearchInput();
var pathError = CollectAssetSearchUtility.ValidateSearchPath(searchInput);
if (pathError != ECollectAssetSearchError.None)
{
string message = CollectAssetSearchUtility.GetSearchPathErrorMessage(pathError, searchInput);
ShowSearchResult(message, new Color(1f, 0.4f, 0.4f));
return;
}
var selectPackage = _packageListView.selectedItem as BundleCollectorPackage;
if (selectPackage == null)
{
string message = "No package selected. Please select a package first.";
ShowSearchResult(message, new Color(1f, 0.8f, 0.3f));
return;
}
var searchResult = CollectAssetSearchUtility.SearchAssetPath(selectPackage, searchInput);
if (searchResult == null)
{
string message = $"No results found in package '{selectPackage.PackageName}'.";
ShowSearchResult(message, new Color(1f, 0.8f, 0.3f));
return;
}
string resultMessage = $"Found in group '{searchResult.Group.GroupName}', collector '{searchResult.Collector.CollectPath}'";
ShowSearchResult(resultMessage, Color.white);
_searchResultLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
_searchResultLabel.style.unityTextAlign = TextAnchor.MiddleLeft;
_highlightAssetPath = searchResult.AssetPath;
_highlightCollectorIndex = searchResult.CollectorIndex;
_groupContainer.visible = true;
_lastModifyGroupIndex = searchResult.GroupIndex;
if (_groupListView.selectedIndex == searchResult.GroupIndex)
FillCollectorViewData();
else
_groupListView.selectedIndex = searchResult.GroupIndex;
}
private void OnGlobalSettingsButtonClicked()
{
_showGlobalSettings = !_showGlobalSettings;
@@ -480,6 +577,45 @@ namespace YooAsset.Editor
return ruleDisplayName.ClassName;
}
// 搜索栏相关
private void ShowSearchResult(string message, Color color)
{
_searchResultLabel.text = message;
_searchResultLabel.style.color = color;
_searchResultLabel.style.unityFontStyleAndWeight = FontStyle.Normal;
_searchResultLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
_searchResultLabel.style.display = DisplayStyle.Flex;
}
private void ClearSearchResult()
{
_searchResultLabel.text = string.Empty;
_searchResultLabel.style.display = DisplayStyle.None;
}
private void ApplySearchPlaceholder()
{
_searchField.value = "Drag or enter asset path here (e.g. Assets/Res/icon.png)";
if (_searchTextField.ClassListContains(PlaceholderClass) == false)
_searchTextField.AddToClassList(PlaceholderClass);
var inputElement = _searchTextField.Q("unity-text-input");
inputElement.style.color = new Color(0.7f, 0.7f, 0.7f, 0.6f);
}
private void ClearSearchPlaceholder()
{
if (_searchTextField.ClassListContains(PlaceholderClass))
{
_searchTextField.RemoveFromClassList(PlaceholderClass);
var inputElement = _searchTextField.Q("unity-text-input");
inputElement.style.color = StyleKeyword.Null;
}
}
private string GetSearchInput()
{
if (_searchTextField.ClassListContains(PlaceholderClass))
return string.Empty;
return _searchField.value;
}
// 设置栏相关
private void RefreshSettings()
{
@@ -607,6 +743,8 @@ namespace YooAsset.Editor
}
private void OnPackageListViewSelectionChange(IEnumerable<object> objs)
{
ClearSearchResult();
var selectPackage = _packageListView.selectedItem as BundleCollectorPackage;
if (selectPackage == null)
{
@@ -752,6 +890,15 @@ namespace YooAsset.Editor
BindCollectorListViewItem(element, i);
_collectorScrollView.Add(element);
}
if (_highlightCollectorIndex >= 0 && _highlightCollectorIndex < selectGroup.Collectors.Count)
{
var targetElement = _collectorScrollView[_highlightCollectorIndex];
var foldout = targetElement.Q<Foldout>("Foldout1");
if (foldout != null)
foldout.value = true;
_highlightCollectorIndex = -1;
}
}
private VisualElement MakeCollectorListViewItem()
{
@@ -1033,13 +1180,6 @@ namespace YooAsset.Editor
// 清空旧元素
foldout.Clear();
// 检测配置是否有效
if (collector.IsValid() == false)
{
collector.CheckConfigError();
return;
}
List<CollectAssetInfo> collectAssetInfos = null;
try
@@ -1055,12 +1195,15 @@ namespace YooAsset.Editor
command.IncludeAssetGUID = _includeAssetGUIDToggle.value;
command.AutoCollectShaders = _autoCollectShadersToggle.value;
// 检测配置是否有效
collector.CheckConfigError();
// 收集有效资源信息
collectAssetInfos = collector.GetAllCollectAssets(command, group);
}
catch (System.Exception e)
catch (Exception e)
{
Debug.LogError(e.ToString());
Debug.LogError($"Invalid collector : {collector.CollectPath}, error: {e.Message}");
}
if (collectAssetInfos != null)
@@ -1085,6 +1228,13 @@ namespace YooAsset.Editor
label.style.width = 300;
label.style.marginLeft = 0;
label.style.flexGrow = 1;
if (string.IsNullOrEmpty(_highlightAssetPath) == false &&
string.Equals(collectAsset.AssetInfo.AssetPath, _highlightAssetPath, StringComparison.OrdinalIgnoreCase))
{
label.style.color = new Color(1f, 0.2f, 0.2f);
}
elementRow.Add(label);
}
}

View File

@@ -5,6 +5,11 @@
<ui:Button text="Import" display-tooltip-when-elided="true" name="ImportButton" style="width: 50px; background-color: rgb(56, 147, 58);" />
<ui:Button text="Fix" display-tooltip-when-elided="true" name="FixButton" style="width: 50px; background-color: rgb(56, 147, 58);" />
</uie:Toolbar>
<uie:Toolbar name="SearchToolbar" style="display: flex; flex-direction: row;">
<uie:ToolbarSearchField focusable="true" name="SearchField" style="flex-grow: 1;" />
<ui:Button text="Search" display-tooltip-when-elided="true" name="SearchButton" style="width: 60px; background-color: rgb(56, 147, 58);" />
</uie:Toolbar>
<ui:Label name="SearchResultLabel" style="display: none; height: 24px; -unity-text-align: middle-center; padding-left: 5px; padding-right: 5px;" />
<ui:VisualElement name="PublicContainer" style="background-color: rgb(79, 79, 79); border-left-width: 5px; border-right-width: 5px; border-top-width: 5px; border-bottom-width: 5px;">
<ui:VisualElement name="HelpBoxContainer" style="flex-grow: 1;" />
<ui:VisualElement name="GlobalSettingsContainer">