UI框架从0到1第三节:【一键挂载】用编辑器扩展解放双手,告别手动拖脚本

56 阅读3分钟

在上一节中,我们构建了一个基础事件系统:

  • 所有 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 类,正式进入多页面逻辑拆分阶段。

(但要注意:我们不会在下一节立刻解决“脚本与控件的自动绑定”问题,那是后面的章节的工作)