UI相关优化补充

This commit is contained in:
2026-05-25 13:21:41 +08:00
parent 3c812cfb41
commit a1f9122153
54 changed files with 2008 additions and 112 deletions

View File

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

View File

@@ -0,0 +1,58 @@
using UnityEditor;
using UnityEngine;
using BaseGames.Core;
namespace BaseGames.Editor.Inspector
{
/// <summary>
/// [RequiredField] 的 Inspector 绘制:未赋值时显示红色 HelpBox。
/// </summary>
[CustomPropertyDrawer(typeof(RequiredFieldAttribute))]
public class RequiredFieldDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
bool missing = IsMissing(property);
if (missing)
{
var msgHeight = EditorGUIUtility.singleLineHeight * 1.6f;
var msgRect = new Rect(position.x, position.y, position.width, msgHeight);
var attr = (RequiredFieldAttribute)attribute;
var hint = string.IsNullOrEmpty(attr.Hint) ? "" : $" ({attr.Hint})";
EditorGUI.HelpBox(msgRect, $"必填字段未赋值{hint}", MessageType.Error);
var fieldRect = new Rect(position.x, position.y + msgHeight + 2, position.width,
EditorGUIUtility.singleLineHeight);
var prev = GUI.color; GUI.color = new Color(1f, 0.6f, 0.6f);
EditorGUI.PropertyField(fieldRect, property, label);
GUI.color = prev;
}
else
{
EditorGUI.PropertyField(position, property, label, true);
}
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
bool missing = IsMissing(property);
float baseH = EditorGUI.GetPropertyHeight(property, label, true);
return missing ? baseH + EditorGUIUtility.singleLineHeight * 1.6f + 4f : baseH;
}
private static bool IsMissing(SerializedProperty p)
{
switch (p.propertyType)
{
case SerializedPropertyType.ObjectReference:
return p.objectReferenceValue == null;
case SerializedPropertyType.String:
return string.IsNullOrEmpty(p.stringValue);
case SerializedPropertyType.ExposedReference:
return p.exposedReferenceValue == null;
default:
return false;
}
}
}
}

View File

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

View File

@@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace BaseGames.Editor.Modules
{
/// <summary>
/// DataHub 事件频道反查模块 —— 列出项目中所有 EventChannelSO 子类资产;
/// 选中后可扫描全项目中引用该频道的 Prefab / ScriptableObject
/// 解决"谁发布/谁订阅了某条事件"在大型项目中难以追踪的问题。
///
/// 反查实现:遍历所有 Prefab 与 SO 资产,用 AssetDatabase.GetDependencies
/// 检查依赖链是否包含目标频道资产路径。首次扫描会耗时较长(取决于资产规模),
/// 之后按选中频道增量计算。
/// </summary>
public class EventChannelModule : IDataModule, IDataModuleOrdered
{
public string ModuleId => "eventchannel";
public string DisplayName => "事件频道";
public string IconName => "d_AudioMixerController Icon";
public int DisplayOrder => 200;
private ListView _channelList;
private readonly List<EventChannelEntry> _allChannels = new();
private ScriptableObject _selected;
private VisualElement _refsContainer;
private struct EventChannelEntry
{
public string TypeName;
public string AssetPath;
public ScriptableObject Asset;
}
public void Initialize() => Refresh();
public void OnActivated() => Refresh();
private void Refresh()
{
_allChannels.Clear();
// 扫描所有 ScriptableObject 子类,筛选名称以 "EventChannelSO" 结尾的(项目约定)。
var types = TypeCache.GetTypesDerivedFrom<ScriptableObject>();
foreach (var t in types)
{
if (t.IsAbstract) continue;
if (!t.Name.EndsWith("EventChannelSO", StringComparison.Ordinal)) continue;
var guids = AssetDatabase.FindAssets($"t:{t.Name}");
foreach (var g in guids)
{
var path = AssetDatabase.GUIDToAssetPath(g);
var asset = AssetDatabase.LoadAssetAtPath<ScriptableObject>(path);
if (asset == null) continue;
_allChannels.Add(new EventChannelEntry
{
TypeName = t.Name,
AssetPath = path,
Asset = asset
});
}
}
_allChannels.Sort((a, b) =>
{
int c = string.Compare(a.TypeName, b.TypeName, StringComparison.Ordinal);
return c != 0 ? c : string.Compare(a.AssetPath, b.AssetPath, StringComparison.Ordinal);
});
_channelList?.Rebuild();
}
public void BuildListPane(VisualElement container, Action<UnityEngine.Object> onSelected)
{
var header = new Label($"频道总数:{_allChannels.Count}");
header.style.unityFontStyleAndWeight = FontStyle.Bold;
header.style.paddingLeft = 6;
header.style.paddingTop = 4;
container.Add(header);
_channelList = new ListView(_allChannels, 20,
() => new Label { style = { paddingLeft = 6 } },
(ve, i) =>
{
var entry = _allChannels[i];
var label = (Label)ve;
label.text = $"[{entry.TypeName}] {System.IO.Path.GetFileNameWithoutExtension(entry.AssetPath)}";
label.tooltip = entry.AssetPath;
});
_channelList.style.flexGrow = 1;
_channelList.selectionChanged += sel =>
{
var first = sel.FirstOrDefault();
if (first is EventChannelEntry e)
{
_selected = e.Asset;
onSelected?.Invoke(e.Asset);
}
};
container.Add(_channelList);
}
public void BuildDetailPane(VisualElement container, UnityEngine.Object selected)
{
_selected = selected as ScriptableObject;
container.Clear();
if (_selected == null)
{
var hint = new Label("请在左侧选择一条频道资产。");
hint.style.paddingLeft = 8; hint.style.paddingTop = 8;
container.Add(hint);
return;
}
// Header
var title = new Label($"{_selected.GetType().Name} · {_selected.name}");
title.style.unityFontStyleAndWeight = FontStyle.Bold;
title.style.fontSize = 14;
title.style.paddingLeft = 8; title.style.paddingTop = 6;
container.Add(title);
var pathLabel = new Label(AssetDatabase.GetAssetPath(_selected))
{
style = { paddingLeft = 8, paddingBottom = 6, color = new StyleColor(new Color(0.7f, 0.7f, 0.7f)) }
};
container.Add(pathLabel);
var scanBtn = new Button(() => ScanReferences(_selected))
{ text = "扫描全项目引用Prefab / ScriptableObject" };
scanBtn.style.marginLeft = 8; scanBtn.style.marginRight = 8;
container.Add(scanBtn);
_refsContainer = new VisualElement();
_refsContainer.style.paddingLeft = 8; _refsContainer.style.paddingTop = 6;
container.Add(_refsContainer);
}
private void ScanReferences(ScriptableObject channel)
{
if (channel == null || _refsContainer == null) return;
_refsContainer.Clear();
var targetPath = AssetDatabase.GetAssetPath(channel);
if (string.IsNullOrEmpty(targetPath)) return;
var found = new List<string>();
try
{
EditorUtility.DisplayProgressBar("扫描引用", "正在扫描 Prefab 与 SO …", 0f);
var prefabGuids = AssetDatabase.FindAssets("t:Prefab");
var soGuids = AssetDatabase.FindAssets("t:ScriptableObject");
var all = prefabGuids.Concat(soGuids).ToArray();
for (int i = 0; i < all.Length; i++)
{
var p = AssetDatabase.GUIDToAssetPath(all[i]);
if (p == targetPath) continue;
if (i % 64 == 0)
EditorUtility.DisplayProgressBar("扫描引用", p, (float)i / all.Length);
var deps = AssetDatabase.GetDependencies(p, recursive: false);
if (System.Array.IndexOf(deps, targetPath) >= 0)
found.Add(p);
}
}
finally { EditorUtility.ClearProgressBar(); }
var summary = new Label($"找到 {found.Count} 个直接引用:");
summary.style.unityFontStyleAndWeight = FontStyle.Bold;
_refsContainer.Add(summary);
foreach (var path in found)
{
var btn = new Button(() =>
{
var obj = AssetDatabase.LoadMainAssetAtPath(path);
EditorGUIUtility.PingObject(obj);
Selection.activeObject = obj;
})
{ text = path };
btn.style.unityTextAlign = TextAnchor.MiddleLeft;
btn.style.marginTop = 2;
_refsContainer.Add(btn);
}
}
}
}

View File

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