概述

August 14, 2025 · View on GitHub

README in English.

概述

此工具专为对于有以下特点的GameObject(或prefab)而设计:

  • 节点结构各不相同;
  • 对其中的大量节点及其组件有较多操作。

典型使用场景:游戏UI界面。

此工具旨在让开发者在操作上述GameObject时,能够以最少代码量且最少性能开销的方式,快速访问到GameObject中需要的关键节点的GameObject对象及其组件。

核心功能

  1. 为GameObject或prefab生成定制的用于绑定变量与目标关键节点及其组件的代码。
  2. 自动向容器节点上挂载步骤1中生成的组件, 无需任何手动拖拽以进行关联操作。
  3. 自动关联上述组件中的序列化属性与其对应的关键节点及其组件
  4. 模板节点(子容器)的实例管理。

名词解释

容器

容器是所有关键节点的集合,逻辑代码中可以通过容器快速获取到其中的关键节点及其组件。

容器组件是挂在容器节点上的一个组件(Component),其代码由此工具生成。该组件用于存储对于该容器中所有关键节点及其组件的引用。

容器内包含的关键节点均为容器所在节点的子节点。一个关键节点只会属于一个容器,对应其父节点中离它最近的容器节点。

根容器

其容器本身不再属于其他容器,其组件类型名称与容器本身节点的名称相同。

子容器

名称以"i_"开头的关键节点,它将包含容器的所有特性。

子容器组件的类型名默认为其所在父容器的类型名称与节点名称去掉"i_"的部分的组合,并以下划线分隔,详见子容器节点组件名称的规则

子容器通常用于定义展示内容中重复元素的模板,所以本工具中也称子容器为模板。

节点

即容器节点及其下所有的GameObject对象,所有节点可组成树状的层级结构。

关键节点

容器的子节点中,名称以"m_""i_""o_""a_"前缀开头的节点为关键节点。

  • "m_""o_"开头的节点是普通关键节点,关键节点及其组件将被该节点所在的容器引用,二者差异为外部访问代码中相应序列化属性的权限差异;
  • "i_"开头的节点为子容器节点,除包含所有普通关键的特性外,此工具还将在此节点上为其挂载容器组件,用于存储对该容器节点下所有关键节点及其节点组件的引用。
  • "a_"开头的关键节点,仅用于标记该节点需要在逻辑启动时激活。

子容器的关键节点在树形结构中为叶子节点,其不会再有子节点。

非关键节点在本工具插件中将被完全忽略。

关键节点名称除前缀以外的部分将被用作字段名称或属性名称,所以节点名称除前缀以外的部分应满足变量的命名规则。

容器组件中,每个关键节点对应一个成员变量,这个变量是其GameObjectTransform/RectTransform与节点上其他所有支持的组件的集合

示例:

节点组件

逻辑功能开发时挂载于关键节点上的功能组件,如UnityEngine.UI.ImageUnityEngine.UI.Text等,将与节点的GameObjectTransform/RectTransform对象一起被其所在的容器引用。

组件类型需要被注册后组件才会被引用,此工具内已经实现了部分组件的注册。您可根据需求,通过编写少量代码将所需组件的类型进行注册,以使该类型的组件会被容器组件引用。

组件集合

为方便同时操作某一节点上gameObjecttransform以及该节点上一个或多个组件,引入组件集合的概念,关键节点上的上述对象都是此集合中的元素。

组件集合在生成的代码中,表现为内部类。

其类名规则:{组件1名称}_{组件2名称}_..._{组件N名称}_Set

其成员包括:

  • gameObjectGameObject类型,对应该节点gameObject对象。
  • transform/rectTransformTransform/RectTransform类型,对应该节点的transform对象。二者只会存在一个,取决于该transform是否为RectTransform
  • 节点上所有受支持的组件:成员名称为[注册组件支持]时为该组件确定好的变量名称,成员类型即为组件的类型。

一个典型的UGUI按钮节点(同时挂有ButtonImage两个组件)对应的集合类定义如下:

// 此代码段源自生成的容器代码,并作为其中的内部类。
[System.Serializable]
public class RectTransform_Button_Image_Set {

    [SerializeField]
    private GameObject m_GameObject;
    public GameObject gameObject { get { return m_GameObject; } }

    [SerializeField]
    private RectTransform m_rectTransform;
    public RectTransform rectTransform { get { return m_rectTransform; } }

    [SerializeField]
    private Button m_button;
    public Button button { get { return m_button; } }

    [SerializeField]
    private Image m_image;
    public Image image { get { return m_image; } }

}

每个关键节点都会使用与之匹配的集合,这个集合只包含该节点上挂有的受支持的组件。

推荐尽量不要在逻辑代码中声明该组件集合类型的变量。

绑定

容器C#组件中的序列化属性与对应的关键节点节点组件进行关联,即在编辑器中为容器组件中的序列化属性相应的赋值。

节点结构与逻辑结构

节点结构即为Prefab或GameObject对象及其所有子节点在Hierarchy窗口中展示的结构。

逻辑结构为根容器、普通关键节点子容器之间的树形结构。

节点结构是逻辑结构的基础,也是划分逻辑结构的依据。在此方案中,在修改节点结构时,如果没有涉及逻辑结构的改变,则重新生成代码并进行重新绑定的操作不会产生任何变更。

特性

获取到根容器后即可访问到全部关键节点及其组件

  • 对一个有众多子节点且结构复杂的GameObject对象进行操作的过程中,理论上仅需要调用一次GetComponent即可。
  • 逻辑中所需要用到的节点,都对应根容器子容器组件中的同名成员属性。

生成的根容器代码节选:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

public class ui_demo : MonoBehaviour {

    [SerializeField]
    private RectTransform_Canvas_Set m_Self;
    public RectTransform_Canvas_Set Self { get { return m_Self; } }

    [SerializeField]
    private RectTransform_Text_Set m_title;
    public RectTransform_Text_Set title { get { return m_title; } }

    [SerializeField]
    private RectTransform_Text_Set m_content;
    public RectTransform_Text_Set content { get { return m_content; } }

    [SerializeField]
    private RectTransform_ui_demo_item_Set m_item;
    public RectTransform_ui_demo_item_Set item { get { return m_item; } }

    [SerializeField]
    private RectTransform_Button_Image_Set m_btn_ok;
    public RectTransform_Button_Image_Set btn_ok { get { return m_btn_ok; } }

    [SerializeField]
    private RectTransform_Button_Image_Set m_btn_cancel;
    public RectTransform_Button_Image_Set btn_cancel { get { return m_btn_cancel; } }

    public void Open() {
        m_item.item?.Open();
    }

    private UnityEvent mOnClear;
    public UnityEvent onClear {
        get {
            if (mOnClear == null) { mOnClear = new UnityEvent(); }
            return mOnClear;
        }
    }

    public void Clear() {
        m_item.CacheAll();
        m_btn_ok.button?.onClick.RemoveAllListeners();
        m_btn_cancel.button?.onClick.RemoveAllListeners();
        if (mOnClear != null) { mOnClear.Invoke(); mOnClear.RemoveAllListeners(); }
    }

    [System.Serializable]
    public class RectTransform_Button_Image_Set { ... }

    [System.Serializable]
    public class RectTransform_Canvas_Set { ... }

    [System.Serializable]
    public class RectTransform_Text_Set { ... }

    [System.Serializable]
    public class RectTransform_ui_demo_item_Set { ... }

}

可快速获取一个节点上的多个组件

  • 业务逻辑中可能会用到同一个节点上的gameObject以及一个多个Component,例如RectTransformButtonImage
  • 在生成的代码中,每一个节点都对应着一个组件集合,这个集合中包含了GameObjectTransform/RectTransform以及其他Component对象。
  • 集合中的GameObjectTransform/RectTransform以及其他Component对象都是这个集合类型中的成员变量,具体组件对应的变量名称根据规则预先设定。

一个典型UGUI按钮节点上的组件示例:

其集合类定义见组件集合中的示例代码。

子容器节点的实例管理

  • 子容器节点可以作为模板节点使用,以按需创建多个实例。
  • 生成的代码中提供了创建和回收及清理模板节点实例的方法,这些方法属于其所在容器子容器节点。

使用示例可参考文档中关于模板实例

方便回收重用

  • 所有生成的容器组件都包含Open()Clear()方法。其中Open()方法用于调起容器中所有关键节点上带有Open()方法的组件,Clear()方法用于清理所有关键节点组件的UnityEvent事件、回收所有子容器实例。
  • 当资源实例需要被回收时,调用根容器Clear()方法即可。
  • 所有容器组件都定义有onClear事件,当容器被回收(调用容器的Clear()方法)时,会委派此事件,帮助对容器实例关联的逻辑进行及时清理。

支持节点对象数组

  • 多个关键节点可以被关联到容器中同一个数组类型的成员变量中。

  • 同个数组中的关键节点支持默认规则排序和自定义排序。

  • 所有类型的关键节点均支持节点对象数组。

规则详述

关键节点命名规则

  • 在使用此工具界面时指定的根节点为根容器节点,它的名称应满足类型命名规则。
  • 对于普通关键节点,节点名称中除前缀以外的部分应满足变量的命名规则。
  • 对于子容器节点,由于子容器节点是其所在容器节点的一个节点,所以其节点名称中除前缀以外的部分应满足变量的命名规则;同时由于子容器必须对应一个生成的子容器组件,所以其名称也会被用于指定该组件的名称。子容器组件名称的详细规则与特殊规则见下文。

根容器组件类型名称的规则

  • 根容器组件的类型名称与根节点名称相同。

子容器组件类型名称的规则

默认规则

默认生成的子容器组件名称为:{所在容器的组件名称}_{子容器节点名称除i_以外的部分}

自定义后缀

使用此规则时,子容器节点的名称规则为:i_{变量名称}|{组件类型名后缀}。此节点名称中以单竖线'|'将“变量名称”与“组件类型名后缀”分隔。

此时,生成的子容器组件名称为:{所在容器的组件名称}_{组件类型名后缀}

完全自定义

使用此规则时,子容器节点的名称规则为:i_{变量名称}||{组件类型全名}。此节点名称中以双竖线"||"将“变量名称”与“组件类型全名”分隔。

此时,生成的子容器组件名称为:{组件类型全名}

数组元素节点及命名规则

  • 关键节点的名称以[][$index](其中index为自然数)后缀结尾的,认为该节点位于数组中。
  • 容器中数组变量的名称不包含[]部分。
  • 数组中关键节点的顺序,默认按节点结构从上到下排序;若某个节点名称后的中括号中指定了索引,则该节点必占据指定索引的位置,其余未指定索引的节点在数组中的其余位置按默认规则排序。

使用partial类和public属性

可以为对每个根容器子容器组件类进行如下定制:

  • 是否将容器组件类为partial类,此设定用于对容器组件类进行自定义扩展。
  • 是否使用"public property"的方式定义节点属性,此设定用于保护序列化属性不被修改。

以上定制均可以设置全局默认值。

若使用"partial类"方式将逻辑写在容器组件的同个类中,建议用private属性;若仅使其发挥容器(序列化和绑定)的作用,建议使用非partial类与public属性。

关于生成代码中容器类型的Self属性

在以下情况下,可能会用到Self属性:

  • 需要使用UGUI界面根节点的Canvas组件。
  • 获取i_ListItem列表元素实例节点上的Button组件。

忽略子节点规则

在某些特殊情况下,即使某些节点的名称满足关键节点的命名规则,但你可能不希望这些节点出现在容器之中,例如:

  • 其他代码的特殊规则要求节点的命名恰好满足关键节点的命名规则,但不希望该节点成为此工具中的关键节点
  • 通过prefab B通过prefab嵌套的方式将prefab A作为其中的子节点,并以上述的命名方式将其为关键节点,但prefab A中的核心组件已经是通过此工具生成过代码的根容器了,此时不希望prefab A中的关键节点出现在prefab B的容器中。

通过节点名称前缀忽略子节点

节点名称以~开头时,该节点的所有子节点不会属于任何容器

但以~开头的节点,如果名称的其余部分仍满足关键节点的命名规则,由它会被当作正常的关键节点进行处理。

忽略自定义组件节点下的子节点

需要在使用为此工具增加其他类型组件的支持规则进行注册自定义类型的代码中,在函数返回结果之前,调用其SetAbortChild(true)方法。

[SupportedComponentType]
static SupportedTypeData DefineTypeMyComponent() {
    return new SupportedTypeData(typeof(MyComponent), 1000)
        // 调用此方法,用于忽略该组件所在节点的所有子节点。
        // 此方法返回this,所以代码可简写为链式形式。
        .SetAbortChild(true);
}

生成代码的存储路径

对于每一个GameObject对象或prefab资源都可以独立指定存放其生成代码的文件夹,但此工具中并没有存储此二者之间的对应关系。

在此工具的操作打开一个GameObject对象或prefab时,此工具会自动在工程中查找与之匹配代码,并将此路径作为下一次生成代码时的目标文件夹。同时,将此对象中所有容器类型的基类、partial类和public属性相关的配置一并从代码中读取。

关于模板实例

  • 每个子容器节点(模板)在其容器中对应的属性对象,都包含GetInstance()CacheInstance()CacheAll()三个方法,分别用于获取单个实例、回收单个实例和回收所有未被回收的实例。
  • 每次通过GetInstance()方法获取到的模板实例,Transform的属性均与模板节点的Transform属性相同,且新的实例节点在Hierarchy窗口中将位于模板节点的下方且紧挨模板节点,即其siblingIndex刚好比模板节点的siblingIndex大1。
  • 推荐使用Layout组件,并在代码中逆序遍历数据,配合GetInstance()方法创建列表。
private YourUI mUI;
void ListDemo() {
    // 取消激活模板节点。
    mUI.listItem.gameObject.SetActive(false);
    List<SomeData> datas = GetSomeDatas();
    // listItem为YourUI中的模板节点(子容器节点)。
    // 创建节点实例前先将列表清空,将之前使用的所有节点回收。
    mUI.listItem.CacheAll();
    // 逆序遍历数据。
    // 第N+1次创建的节点,将位于第N次创建的节点之前。
    for (int i = datas.Count - 1; i >= 0; i--) {
        SomeData data = datas[i];
        var item = mUI.listItem.GetInstance();
        // 需要手动激活节点。
        item.Self.gameObject.SetActive(true);
        // 获取someTextNode节点中的Text组件。
        Text textComp = item.someTextNode.text;
        textComp.text = data.someText;
        // 需要在此模板实例实例(item)被回收时,附带回收(注销)的对象,可这样操作。
        IDisposable disposable = DoSomeBind(item.someNode.someComponent, data.someProperty);
        // item的onClear事件,将在调用mUI.Clear()、mUI.listItem.CacheAll()等回收方法时触发,用于释放(注销)与此模板实例的绑定关系。
        item.onClear.AddListener(() => {
            disposable.Dispose();
        });
    }
}
// 界面关闭回调入口
void OnClose() {
    // 调用此方法后,mUI容器中所有的关键节点及其组件都将调用其Clear方法。
    // 此后,mUI所在的gameObject将处于等待回收重用的状态。
    mUI.Clear();
}

操作步骤

为GameObject新增或更新容器组件

  1. 根据需求创建或编辑资源,合理规划其关键节点子容器,并为其取合适的名称。

  2. 在Project容器中选择prefab右键点击"GreatClock > Open Binding Tool"打开操作界面,或通过菜单栏"GreatClock > Open Binding Tool"打开操作界面;并将要操作的GameObject或prefab对象拖拽至界面中"Game Object"处。

    打开工具界面

    工具界面

  3. 检查并修改用于存放生成代码的文件夹及代码使用的命名空间。

  4. 在操作界面中检查关键节点节点组件

  5. 检查容器组件的基类以及所有容器组件类的partial类和public属性的设置。

  6. 点击"生成并挂载"按钮,工具将自动生成或更新容器组件代码,并将容器与其关键节点组件建立引用。此步骤完成后,选择此prefab后,Inspector界面将如下图所示:

    预览

  7. 使用partial类或者获取容器对象的方式来实现对界面中关键节点及其节点组件的操作。以下代码为控制ui_demo.prefab实例的一个示例:

private ui_demo mUI;
void OnLoaded(GameObject go) {
    ui_demo mUI = go.GetComponent<ui_demo>();
    // 为两按钮注册点击事件的回调函数。
    mUI.btn_ok.button.AddListener(OnClickOK);
    mUI.btn_cancel.button.AddListener(OnClickCancel);
    // item节点为模板,此模板不作为展示之用。
    mUI.item.gameObject.SetActive(false);
    // 调用容器的Open方法,以启动所有关键节点上所有组件的Open方法(如果有)。
    mUI.Open();
    mUI.title.text.text = "Title";
    mUI.content.text.text = "Content content ...";
    for (int i = 0; i < 3; i++) {
        var item = mUI.item.GetInstance();
        item.Self.gameObject.SetActive(true);
        // 为item.icon以及item.count设置内容
        item.icon.image.sprite = GetSprite();
        item.count.text.text = GetCountText();
    }
}
void OnClose() {
    if (mUI != null) {
        // 界面关闭,此gameObject在回收前,调用Clear方法以回收所有模板实例,
        // 并调用所有关键节点上所有组件的Clear方法(如果有),以保证对象可以重用。
        mUI.Clear();
        mUI = null;
    }
}
private void OnClickOK() { }
private void OnClickCancel() { }

为此工具增加其他类型组件的支持

在编辑器代码中增加如下代码段,即可向此工具增加对自定义或其他组件的支持。

using GreatClock.Common.SerializeTools;

// 类名并不重要,但应运行在Unity Editor环境中
public class SerializeToolExtend {
    
    // 此Attribute为必需,方法应为static并返回包含定制化信息的SupportedTypeData实例
    // 方法名称并不重要,方便代码阅读及管理即可,但必须是无参的方法。
    [SupportedComponentType]
    static SupportedTypeData DefineTypeMyComponent() {
        // 指定要支持组件的类型,排序优先级。
        return new SupportedTypeData(typeof(MyComponent), 1000);
    }
    
    [SupportedComponentType]
    static SupportedTypeData DefineTypeTween() {
        // 增加此工具对MyTweenComponent组件的支持,
        // 并为此组件指定生成代码中的变量名为tween。
        return new SupportedTypeData(typeof(MyTweenComponent), 1000)
            .SetVariableName("tween");
    }
    
}

覆盖内置支持的组件

在此工具内部SerializedComponentWindow.cs文件中,已经通过上述方法注册并支持了TransformRectTransform以及CanvasImage等UGUI的常用组件。这些组件无需增加支持即可直接使用。

如果实际项目中,如果需要对这些已经支持了的组件的注册规则进行调整,也可以按上述规则,在项目中进行二次定义。对于二次定义的组件,其所有注册的规则将以工具外的定义为准。

SupportedTypeData类的成员属性

属性名称类型设置方法默认值用途描述
typeType构造函数-需要支持的组件类型,应为Component的子类,不可为空。
priorityint构造函数-用于指定该组件在组件列表中的出现顺序,数值越小越靠前。
showNamestringSetShowNamenull在预览窗口中,该组件的展示名称。默认值与类型名称相同。
nameSpacestringSetNameSpacenullcodeTypeName配合使用,用于手动指定生成代码中,该组件的类型及使用的命名空间。
codeTypeNamestringSetCodeTypeNamenullnameSpace配合使用,用于手动指定生成代码中,该组件的类型及使用的命名空间。
variableNamestringSetVariableNamenull生成的代码中,该组件对应的变量名。默认值为类型名称的小写开头的驼峰规则。
clearEventsOnClearboolSetClearEventsOnCleartrue是否需要在Clear方法中清空组件中已注册的UnityEvent事件(如按钮点击事件)。
abortChildboolSetAbortChildfalse是否需要忽略该组件所在节点的所有子节点。

注意事项

  • 自动生成的代码,无需阅读,更不要进行手动编辑。
  • 除数组元素外,同一容器中的关键节点不可重名。
  • 关键节点名称应避开C#语言关键字、MonoBehaviour中已有的属性名称。
  • 此工具仅会对当前规则下同名的容器子容器脚本进行创建或更新,不会对原有失效容器进行清理。如果将子容器节点进行移动、重命名、改为非容器节点等导致容器组件名称有修改的操作,需要手动将原容器组件及相应代码进行清理。
  • 在逻辑代码中,关键节点及其组件是以容器的成员变量方式被访问,这样更方便逻辑代码对关键节点和组件的调用,但也更容易产生编译问题。此编译问题也能第一时间反应出资源的修改对代码逻辑造成的影响。

常见问题与技巧

生成代码后的编译错误

可能会在生成代码后产生编译错误的情况以及解决方法:

需要注意,只有在生成代码后没有编译错误时才可正确进行绑定操作。因此在解决完编译错误后,需要重新让此工具执行绑定操作。

多个结构相同的子容器使用同一类型组件

用到的规则:子容器组件类型名称的规则中的自定义后缀完全自定义

注意事项:

例如有如下一个领奖台的需求:

  • 台上冠亚季军的高度和样式并不相同,并prefab中定义如何展示;
  • 冠亚季军的台子上都需要展示:获奖者姓名、代表国家、获奖成绩,同时都需一个节点用于放置获奖者的模型;
  • 上述都需要展示的内容中,样式也会各不相同,并在prefab中进行定制;
  • 冠亚季军领奖台,对应三个节点,其节点顺序为:亚军、冠军、季军。
  • 三个节点使用同一个的领奖台组件类型,并列在一个长度为3的数组中,数组顺序为冠军、亚军、季军。

针对上述需求,领奖台prefab的节点结构定义如下:

为podium.prefab生成的容器代码节选:

// podium.cs
public class podium : MonoBehaviour {
    ...
    [SerializeField]
    private RectTransform_podium_stage_Set[] m_stages;
    public RectTransform_podium_stage_Set[] stages { get { return m_stages; } }
    ...
}

// podium_stage.cs
public class podium_stage : MonoBehaviour {

    [SerializeField]
    private RectTransform_Set m_Self;
    public RectTransform_Set Self { get { return m_Self; } }

    [SerializeField]
    private RectTransform_Text_Set m_player_name;
    public RectTransform_Text_Set player_name { get { return m_player_name; } }

    [SerializeField]
    private RectTransform_Text_Set m_player_nationality;
    public RectTransform_Text_Set player_nationality { get { return m_player_nationality; } }

    [SerializeField]
    private RectTransform_Text_Set m_player_score;
    public RectTransform_Text_Set player_score { get { return m_player_score; } }

    [SerializeField]
    private RectTransform_Set m_player_model_root;
    public RectTransform_Set player_model_root { get { return m_player_model_root; } }

    ...
}