在上一节中,我们构建了一个基础事件系统:
-
所有 UI 控件都继承自
EventUIBase; -
控件会自动向上查找父级
PanelBase; -
页面统一通过
ProcessEvent<T>()处理所有控件事件。
这为构建通用的 UI 行为打下了良好基础。
但现在你会遇到一个极其现实的开发难题:
❓ 为什么每次都要手动挂 ButtonCustom、ToggleCustom?
——不仅繁琐,还容易挂错、漏挂,影响运行。
本节我们要解决的就是这个问题:实现一键挂载所有 UI 控件对应的 Custom 脚本,彻底解放双手。
🧩 目标:根据控件自动挂载对应脚本
比如:
-
有 Button,就自动挂
ButtonCustom -
有 Toggle,就自动挂
ToggleCustom -
有 TMP_InputField,就自动挂
TMP_InputFieldCustom -
……以此类推
我们通过 编辑器扩展 + 反射机制 来实现这一目标。
🛠️ 第一步:编写挂载工具入口
编辑器扩展必须写在 Editor 文件夹下,且类必须是 static。以下是最简入口:
using UnityEditor;
using UnityEngine;
public static class FrameWorkTools
{
[MenuItem("FrameWorkTools!/一键挂载基础脚本")]
public static void MountBaseUIComponent()
{
Debug.Log("MountBaseUIComponent");
}
}
编译后,Unity 编辑器顶部菜单栏就会出现:
FrameWorkTools! → 一键挂载基础脚本
点击它,即可运行我们的批处理逻辑。
🧠 第二步:反射匹配组件 → 挂载 Custom 脚本
下面是核心逻辑,分步解读:
[MenuItem("FrameWorkTools!/一键挂载基础脚本")]
public static void MountBaseUIComponent()
{
GameObject[] allObjects = GetAllGameObjectsInScene();
foreach (var obj in allObjects)
{
ProcessUIComponent<Button>(obj);
ProcessUIComponent<Toggle>(obj);
ProcessUIComponent<Dropdown>(obj);
ProcessUIComponent<TextMeshProUGUI>(obj);
}
Debug.Log("✅ 所有 UI 组件处理完毕,基础脚本已自动挂载!");
}
🔍 GameObject 扫描与递归收集
private static GameObject[] GetAllGameObjectsInScene()
{
var allObjects = new HashSet<GameObject>();
foreach (var root in SceneManager.GetActiveScene().GetRootGameObjects())
CollectAllGameObjects(root, allObjects);
return allObjects.ToArray();
}
private static void CollectAllGameObjects(GameObject obj, HashSet<GameObject> collected)
{
if (!collected.Add(obj)) return;
foreach (Transform child in obj.transform)
CollectAllGameObjects(child.gameObject, collected);
}
⚙️ 自动挂载逻辑(基于组件类型推导 Custom 脚本)
private static void ProcessUIComponent<T>(GameObject obj) where T : Component
{
T uiComponent = obj.GetComponent<T>();
if (uiComponent == null) return;
string customName = typeof(T).Name + "Custom";
Type customType = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypes())
.FirstOrDefault(t => t.Name == customName && typeof(MonoBehaviour).IsAssignableFrom(t));
if (customType == null)
{
Debug.LogWarning($"⚠️ 未找到脚本:{customName}");
return;
}
var existing = obj.GetComponents(customType);
foreach (var e in existing)
Undo.DestroyObjectImmediate(e);
Undo.AddComponent(obj, customType);
EditorUtility.SetDirty(obj);
}
🔰 FAQ:我 UI 少,能不用这个工具吗?
当然可以!
如果你只是做一个简单 demo,或者只有少量控件,也可以手动挂脚本,无伤大雅。
但一旦 UI 控件超过 5 个页面,自动化就是节省脑细胞的关键,尤其在多人项目中,统一标准更是不可或缺。
🔮 小预告:页面是怎么区分的?
你可能已经注意到了,虽然我们统一了 UI 控件的事件处理方式,但我们目前只有一个 PanelBase 类。
那多个页面怎么办?难道所有页面事件都写在一个 PanelBase 里?
当然不是。
这个类叫 PanelBase,本身就是为派生多个面板逻辑类准备的。
下一节,我们就来创建属于每个页面自己的 PanelLogic 类,正式进入多页面逻辑拆分阶段。
(但要注意:我们不会在下一节立刻解决“脚本与控件的自动绑定”问题,那是后面的章节的工作)