Unity实战:基于ScriptableObject注册表的UI架构——Manager与Page的ID联动机制详解

前言:摆脱"硬拖拽"的噩梦

在Unity项目开发中,UI管理一直是让人头疼的问题。很多初学者习惯在脚本里直接public GameObject homePage,然后在Inspector里手动拖拽赋值。

这种做法的致命缺陷在于:

  1. 跨场景引用丢失:一旦预制体被重新导入或移动,引用直接断开。

  2. 耦合度过高:HomePage脚本里如果引用了SettingPanel,修改起来牵一发而动全身。

  3. 无法统一管理:每个界面都需要单独写创建和销毁逻辑。

本文将介绍一种工业级 的UI架构思路:利用ScriptableObject作为数据注册表(Registry),配合UIManager的ID寻址机制,实现UI界面的自动匹配与生命周期管理。


一、架构核心三要素

这套架构本质上是一个**"数据驱动"**的UI调度系统,由以下三部分组成:

  1. UIRegistry(注册表资产):它是配置中心,存储着"ID"与"预制体"的映射关系。

  2. UIManager(调度中心):它是唯一的管理入口,负责创建、缓存、打开和关闭界面。

  3. UIWindow / UIPage(视图层):挂在Canvas上的具体界面,带有唯一的身份标识(ID)。

核心联动逻辑 :业务层只传string ID → Manager拿着ID查缓存 → 缓存没有则去Registry拿预制体 → 实例化并返回。


二、注册表(UIRegistry):数据与代码的解耦神器

我们不再将预制体直接挂在MonoBehaviour脚本上,而是创建了一个ScriptableObject

csharp

复制代码
// 核心数据结构:一个条目包含一个ID和一个预制体引用
[Serializable]
public class UIRegistryEntry
{
    public string id;
    public UIWindow prefab;//有UIwindow类或继承的预制体
}

// 注册表资产
public class UIRegistry : ScriptableObject
{
    private List<UIRegistryEntry> pages;
    private List<UIRegistryEntry> popups;

    // 核心方法:通过ID获取Page预制体
    public UIPage GetPagePrefab(string id) { ... }

    // 核心方法:通过ID获取Popup预制体
    public UIPopup GetPopupPrefab(string id) { ... }
}

为什么要用ScriptableObject?

因为它是数据资产 。我们可以直接在Project窗口中右键创建UIRegistry资产,策划和美术不用碰代码,就能在Inspector面板里配置每个ID对应的预制体。这彻底解决了硬编码引用丢失的问题。

纯数据层解耦最好的工具SO(ScriptableObject)小白教程-CSDN博客SO怎么用详情可以看我的前传


三、调度中心(UIManager):ID驱动的发动机

UIManager是单例,它的内部维护着两个**字典(Dictionary)**作为对象池,存储已经实例化好的界面。

csharp

复制代码
private Dictionary<string, UIPage> pageInstances = new ...();
private Dictionary<string, UIPopup> popupInstances = new ...();

关键的联动方法:GetOrCreate

这是Manager和Registry联动的核心"握手"逻辑。公开的OpenPageOpenPopup最终都会调用这里的私有方法:

csharp

复制代码
private UIPage GetOrCreatePage(string pageId)
{
    // 1. 先查字典(对象池),有则直接返回,避免重复实例化
    if (pageInstances.TryGetValue(pageId, out UIPage instance))
        return instance;

    // 2. 字典没有,则触发联动:去注册表(Registry)里根据ID拿预制体
    UIPage prefab = registry.GetPagePrefab(pageId);
    
    // 3. 拿到预制体后,实例化并放入池子
    instance = Instantiate(prefab, pageRoot);
    pageInstances[pageId] = instance;
    return instance;
}

联动精髓 :UIManager 不关心 预制体叫什么名字,也不关心它放在哪个文件夹。它只信任Registry告诉它的结果。只要ID对得上,Registry就能把正确的预制体交给Manager。


四、视图层(UIPage/UIWindow):挂载在Canvas上的身份证

为了能让Manager找到我们,每一个UI预制体上的脚本必须继承自UIWindow,并拥有一个WindowId

csharp

复制代码
public abstract class UIWindow : MonoBehaviour
{
    [SerializeField] private string windowId; // 这就是挂载在预制体上的ID

    public string WindowId => string.IsNullOrWhiteSpace(windowId) ? GetType().Name : windowId;
    
    public void Open(object args = null) { ... }
    public void Close() { ... }
}

匹配规则

当你把HomePageCanvas脚本挂载到预制体上时,需要在Inspector中填写的windowId(或者默认使用类名),必须UIRegistry资产里配置的ID完全一致

这才是联动的最后一步------Registry里存的ID是"钥匙",预制体上挂载的ID是"锁芯",只有两者匹配,Manager才能成功地通过ID驱动界面显示。


五、完整的调用链路(联动流程)

我们以"点击首页按钮,打开ID为'100'的设置弹窗"为例,走一遍完整的联动流程:

  1. 业务层调用 (HomePageCanvas脚本中):

    UIManager.Instance.OpenPopup("100", null);

  2. Manager接收指令:拿到ID "100"。

  3. 第一步联动(查缓存)

    Manager去popupInstances字典里找有没有Key为"100"的实例。如果有,直接拿出来用(复用);如果没有,进入下一步。

  4. 第二步联动(查注册表)

    Manager调用registry.GetPopupPrefab("100")。Registry在自己的数据列表中寻找ID为"100"的条目,找到对应的预制体并返回给Manager。

  5. 第三步联动(实例化与匹配)

    Manager拿到预制体后,执行Instantiate,将其生成在popupRoot下,并存入字典。随后调用该实例的Open()方法。

  6. 视图显示:弹窗出现。


六、这种"大厂标准"写法带来了什么?

维度 传统做法 本架构(联动模式)
创建方式 new GameObject 或 拖拽引用 Manager.OpenPage("ID")
预制体查找 手动拖拽 (易丢失) 通过Registry的SO数据自动映射
缓存管理 无,每次都Instantiate 自带Dictionary对象池,性能更优
模块解耦 A界面直接引用B界面 A界面只传字符串,彻底解耦
可配置性 依赖代码修改 策划可在SO资产中调整预制体映射

这套逻辑之所以被大量商业项目采用,是因为它将**"资源的配置"** (Registry)、"资源的创建与管理" (Manager)和**"资源的视图表现"**(UIWindow)彻底分离。


七、总结

UIManager与UIRegistry的联动,本质上是一场基于ID的"按名寻址"

  • Registry 提供了"ID → Prefab"的数据字典。

  • Manager 提供了"缓存池"和"创建工厂"。

  • Page 提供了"身份证明"。

当业务层发出Open指令时,Manager通过ID驱动Registry寻找资源,再通过ID匹配实例进行显示。这一套组合拳下来,你的UI框架将具备极强的健壮性和可扩展性。

这趟代码的流程就是外部canvas调用UIManager.Instance.OpenPopup("100", null);方法单利给出我要打开ID为100的这个popup,UImanager立刻调用方法UIPopup popup = GetOrCreatePopup(popupId);去create,

去注册表查看这个方法主动调用

UIPopup prefab = registry != null ? registry.GetPopupPrefab(popupId) : null;

注册表被调用查找egistry.GetPopupPrefab(popupId)这个ID最后列表得到值与列表匹配

UIwindow是让mono匹配统一可以open的操作继承作用