三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Unity UI系统深度解析:从UGUI到UI Toolkit的性能优化与实战指南

Unity UI系统深度解析:从UGUI到UI Toolkit的性能优化与实战指南

1. 项目概述:为什么UI系统是Unity开发者的核心技能?

如果你在Unity社区里待过一阵子,或者看过那些招聘要求,会发现“熟练掌握UGUI/UI Toolkit”几乎是每个Unity岗位的标配。这绝不是巧合。无论是制作一款休闲手游,还是一个复杂的企业级数据可视化应用,用户界面都是产品与玩家、用户直接对话的窗口。一个流畅、直观、美观的UI,能极大提升用户体验和留存率;而一个卡顿、混乱、难以操作的UI,则足以毁掉一个内核再优秀的游戏或应用。因此,深入理解Unity的UI系统,绝不仅仅是学会怎么拖拽几个按钮和图片,而是掌握一套从底层渲染、事件交互到性能优化、跨平台适配的完整知识体系。

在Unity的发展历程中,UI系统经历了多次迭代。早期的开发者可能还记得用代码硬写OnGUI的IMGUI时代,或者依赖强大第三方插件NGUI的日子。如今,UGUI(Unity UI)作为官方主力,凭借其基于GameObject的直观工作流和强大的可视化编辑器,已成为绝大多数项目的首选。而新生的UI Toolkit,则以其声明式、样式驱动的设计,瞄准了运行时复杂UI和编辑器扩展两大场景。理解这三者(IMGUI, UGUI, UI Toolkit)的定位与差异,是构建高效UI的第一步。本篇文章,我将以一个从业十余年的视角,为你彻底拆解Unity UI系统的核心,不止于API调用,更深入到设计理念、性能瓶颈和实战避坑指南,让你从“会用”进阶到“精通”。

2. Unity三大UI系统深度对比与选型指南

2.1 IMGUI:编辑器之魂,运行时之殇

IMGUI(Immediate Mode GUI)是Unity最古老的UI系统。它的工作模式非常独特:你需要在OnGUI这类每帧调用的方法中,通过编写代码来“立即”声明和绘制UI元素。比如,一个按钮的创建就是一句GUI.Button(new Rect(10, 10, 50, 20), “Click”);

它的核心优势在于极致的灵活性与可控性,特别适合Unity编辑器自身的扩展开发。因为编辑器的工具窗口生命周期复杂,且需要高度定制化的控件,IMGUI这种无状态、按帧绘制的模式非常契合。你在Asset Store下载的很多插件,其自定义Inspector或EditorWindow几乎都是基于IMGUI构建的。

注意:虽然IMGUI看似简单,但将其用于运行时游戏UI是绝对不推荐的。因为它的“立即模式”意味着每帧都需要重新构造整个UI的绘制指令,无法进行有效的合批与裁剪,在UI元素稍多时就会造成严重的性能开销。我曾见过一个项目在早期用IMGUI做了个简单的调试面板,当面板上有几十个可交互控件时,帧率直接掉了大半。所以,请牢记:IMGUI仅用于编辑器扩展,运行时UI请转向UGUI或UI Toolkit。

2.2 UGUI:游戏UI的绝对主力与实战首选

UGUI(Unity UI)是当前Unity游戏开发中无可争议的UI标准解决方案。它采用基于GameObject的“保留模式”,你在场景中创建的每个ImageText (Legacy)TextMeshPro组件,都是一个实体对象。这种模式与Unity的ECS(面向数据的技术栈)概念相悖,但却无比符合美术和策划人员的工作习惯——所见即所得。

UGUI的核心架构围绕Canvas(画布)、RectTransform和EventSystem展开。

  1. Canvas:所有UGUI元素的根容器,负责UI的渲染。它有一个关键属性“Render Mode”,分为Screen Space(屏幕空间)、World Space(世界空间)和Camera Space(相机空间)。绝大多数UI使用Screen Space - Overlay,它直接渲染在屏幕最上层,不受场景相机影响。
  2. RectTransform:这是UGUI的基石,继承自Transform,但增加了锚点(Anchors)、轴心点(Pivot)和对齐(Alignment)等专为2D布局设计的属性。理解锚点是如何决定UI元素相对于父级或屏幕的定位和拉伸行为,是掌握UGUI布局的关键。
  3. EventSystem:处理所有输入事件(点击、拖拽、选中等)的中枢。它通过Graphic Raycaster组件(通常挂在Canvas上)来检测鼠标或触摸点落在了哪个Graphic(如Image,Text)上。

UGUI的选型理由非常充分:

  • 生态成熟:拥有海量的教程、插件(如DOTween的UI扩展、Fungus叙事工具)和社区支持。
  • 美术友好:可视化编辑器强大,动画系统(Animator)能无缝结合,方便制作入场、退场等动态效果。
  • 性能可优化:虽然默认设置下可能存在性能陷阱,但通过合理的设置(如Canvas合批、图集打包、动静分离),完全可以满足中重度手游的性能要求。

2.3 UI Toolkit:面向未来的声明式UI系统

UI Toolkit是Unity正在大力投入的新一代UI系统。它的设计灵感来源于Web技术(HTML/CSS),采用声明式的UXML文件描述结构,USS文件定义样式,C#脚本处理逻辑。这种分离关注点的设计,对于构建大型、复杂的运行时UI(如MMO游戏的背包、技能树)或高度定制化的编辑器工具,有着天然的优势。

与UGUI的核心差异对比:

特性维度UGUIUI Toolkit
设计哲学面向对象,基于GameObject声明式,基于样式和模板
工作流主要在Scene视图和Inspector中拖拽配置编写UXML/USS,或在UI Builder中可视化设计
性能特点依赖Canvas合批,元素多时Draw Call可能较高自身渲染效率高,特别擅长处理大量重复元素(如长列表)
主要场景游戏运行时UI(HUD、弹窗、主菜单)1. 编辑器扩展 2. 运行时复杂UI(背包、商店、设置)
学习曲线对Unity开发者更直观,易于上手需要理解类似Web的前端概念,初期有一定门槛

当前阶段的选择建议:

  • 对于大多数游戏项目,尤其是中小型团队和快速原型开发,UGUI依然是首选。它的成熟度、社区资源和与Unity其他系统(如动画、物理)的集成度暂时无法被替代。
  • 如果你在开发编辑器工具,或者你的游戏有极其复杂的、数据驱动的UI界面(例如包含数百个可滚动物品的仓库),强烈建议开始学习和评估UI Toolkit。Unity官方已明确表示,UI Toolkit是编辑器UI的未来,并且其运行时功能正在快速完善。

3. UGUI核心组件与布局系统实战精讲

3.1 Canvas渲染机制与性能命门

Canvas是UGUI的渲染管理器,它的设置直接影响UI的渲染效率和效果。创建一个UI元素时,Unity会自动生成一个Canvas。但多个Canvas的存在会破坏合批。

Canvas的渲染流程可以简单理解为:当一个Canvas下的UI元素需要被渲染时,Unity会检查它们的材质和纹理。如果连续的几个元素使用相同的材质和纹理(通常来自同一张图集),并且层级顺序(由Hierarchy中的顺序和组件上的Depth影响)允许,它们就可以被“合批”到一个Draw Call中。Draw Call是CPU向GPU提交绘制指令的次数,是衡量渲染性能的关键指标之一,次数越少越好。

这里有一个至关重要的实战经验:Canvas有一个Additional Shader Channels属性。默认情况下,它可能只包含了TexCoord、Normal等。如果你的UI使用了自定义Shader,并且需要传递额外的顶点数据(比如UV2用于特效),你必须在这里勾选对应的通道(如TexCoord1, TexCoord2)。否则,这些数据将无法被传递到Shader中,导致渲染错误。我就曾踩过这个坑,一个花了半天写的流光Shader在UI上毫无效果,最后排查才发现是这里没设置。

性能优化核心——Canvas合批与动静分离:UGUI的合批是自动的,但前提是元素在同一个Canvas下,且满足合批条件。一个常见的性能陷阱是,一个频繁变化的UI元素(如血量数字)会迫使它所在的整个Canvas下的所有元素都进行重构和重绘,即使其他元素是静态的。解决方案是动静分离

  • 静态Canvas:放置背景、边框、常驻图标等不变化的元素。
  • 动态Canvas:单独放置需要频繁更新、播放动画的元素,如血条、技能CD图标、飘字等。 这样,动态Canvas的重绘不会波及静态Canvas,从而大幅减少不必要的计算。

3.2 RectTransform:掌握锚点才算入门

很多新手觉得UI布局难,问题大多出在对RectTransform的锚点理解不透彻。RectTransform决定了UI矩形的位置、大小和与父级的关联方式。

锚点(Anchors)是四个小三角形,它定义了本UI矩形的四个边与父矩形对应边的相对关系。它不是“固定”一个点,而是定义了一种关联规则

  • 锚点重合于一点(如左上角):此时PosX, PosY表示本矩形轴心点(Pivot)距离锚点那个固定点的偏移量。WidthHeight是绝对值。适合需要固定大小的按钮、图标。
  • 锚点拉伸(四个锚点分别位于父矩形的四个角):此时PosX, PosY, Width, Height的含义都变了!它们分别表示本矩形左、上、右、下四个边距离父矩形对应边的距离。这时,你调整Left,就是调整左边距;调整Right,就是调整右边距。这是实现自适应布局的关键,比如一个对话框,你希望它距离屏幕左右各50像素,高度为屏幕一半,就可以用这种模式轻松设置。

一个快速上手的技巧:在Scene视图选中UI元素,按住Shift+Alt键,再按键盘上的方向键(上、下、左、右、居中),可以快速将锚点对齐到父物体的各种位置,这是最快捷的布局方式。

3.3 事件系统的交互与扩展

UGUI的事件系统是一个典型的观察者模式应用。最常用的交互组件是Button。但Button本身只是一个集成了ImageText的预制体,其核心是Button组件,它实现了IPointerClickHandler等接口。

自定义事件交互的两种方式:

  1. 组件挂载法:让你的脚本实现相应的事件接口,如IPointerClickHandler,IDragHandler。Unity会自动在拥有EventSystem的场景中调用这些接口方法。这是最干净、面向对象的方式。
    public class CustomButton : MonoBehaviour, IPointerClickHandler { public void OnPointerClick(PointerEventData eventData) { Debug.Log($"Clicked by {eventData.button} at {eventData.position}"); // 你的点击逻辑 } }
  2. 事件触发器(EventTrigger)组件:这是一个通用组件,允许你在Inspector中为多种事件类型(点击、进入、退出、拖拽等)动态添加响应函数。这种方式更灵活,无需修改脚本,适合快速原型或由策划配置简单事件。但过度使用会使逻辑分散,不利于维护。

关于输入:Unity的新输入系统(Input System Package)已经逐渐成为主流。它比旧的Input Manager更强大、更灵活,支持复杂的输入动作映射和跨平台处理。要让UGUI与新输入系统协同工作,你需要将EventSystem上的Input ModuleStandalone Input Module替换为Input System UI Input Module。这个切换过程基本无缝,但要注意新输入系统对于触控和手柄的支持需要额外的配置。

4. 从零构建一个复杂的UI界面:以游戏设置面板为例

理论讲得再多,不如动手做一遍。我们以创建一个典型的游戏“设置”面板为例,串联起UGUI的核心工作流。这个面板包含标题、音量控制滑块、图形质量下拉菜单、确认和取消按钮,并且需要适配不同屏幕分辨率。

4.1 结构与布局搭建

  1. 创建Canvas:在Hierarchy中右键 -> UI -> Canvas。设置Render ModeScreen Space - OverlayUI Scale ModeScale With Screen Size,参考分辨率设为1920x1080。这个模式能确保UI在不同分辨率下按比例缩放。
  2. 创建背景面板:在Canvas下创建空物体,命名为Panel_Settings,添加Image组件作为背景。设置其锚点为“拉伸”(Stretch),然后将其Left,Top,Right,Bottom都设为100,这样面板就会距离屏幕四边各100像素。
  3. 添加标题:在面板内创建TextMeshPro - Text (UI)对象(推荐使用TextMeshPro,效果远优于旧版Text)。命名为Text_Title,输入“游戏设置”。调整字体、大小、颜色,并将其锚点设置为顶部水平居中。
  4. 创建音量控制组
    • 创建一个空物体Group_Audio,作为容器。
    • 在容器内创建一个TextMeshPro对象,显示“主音量”。
    • 在文字右侧创建一个Slider。你需要精细调整SliderRectTransform:将其锚点设置为左右拉伸,这样LeftRight属性就代表距离父容器左右边的距离,宽度就能自适应。将Min Value设为0,Max Value设为1。
    • 复制一份,创建“音乐音量”和“音效音量”控制组。
    • 使用Vertical Layout Group组件挂在Group_Audio上,并适当设置Spacing,可以自动排列这三个控制项,无需手动计算位置。
  5. 创建图形质量下拉菜单:创建Dropdown - TextMeshPro对象。在它的Options列表里,添加“低”、“中”、“高”、“极高”等选项。
  6. 创建按钮:创建两个Button - TextMeshPro对象,分别命名为Btn_ConfirmBtn_Cancel,修改文字。将它们放入一个水平布局组(Horizontal Layout Group)中,以实现自动水平居中排列。

通过以上步骤,一个结构清晰、布局自适应的设置面板骨架就完成了。大量使用布局组(Layout Group)能极大减少手动调整位置的工作量。

4.2 逻辑绑定与数据持久化

界面摆好了,接下来要让它能用。

  1. 音量控制逻辑
    • 创建一个C#脚本SettingsManager,挂载在Canvas或一个专门的管理器物体上。
    • 在脚本中声明Slider类型的公共变量masterVolumeSlider,musicVolumeSlider,sfxVolumeSlider,并在Inspector中拖拽赋值。
    • 为每个SlideronValueChanged事件添加监听:
      void Start() { // 从PlayerPrefs加载保存的值 float savedVolume = PlayerPrefs.GetFloat("MasterVolume", 1f); masterVolumeSlider.value = savedVolume; // 监听变化 masterVolumeSlider.onValueChanged.AddListener(OnMasterVolumeChanged); } void OnMasterVolumeChanged(float value) { AudioListener.volume = value; // 全局音量控制(简化示例) PlayerPrefs.SetFloat("MasterVolume", value); }
    • 对于音乐和音效,你需要关联到具体的AudioSource或音频管理器。
  2. 图形质量逻辑:监听DropdownonValueChanged事件,根据选中的索引调用QualitySettings.SetQualityLevel(index)
  3. 按钮逻辑:为“确认”按钮的onClick事件绑定一个方法,该方法保存所有设置(PlayerPrefs.Save())并关闭面板。为“取消”按钮绑定关闭面板但不保存的方法。

实操心得:数据持久化不要只依赖PlayerPrefs。对于复杂的设置结构,建议序列化为JSON或Binary文件,或者使用ScriptableObject来存储。PlayerPrefs适合存储简单的键值对,且在不同平台(如WebGL)有存储限制。

4.3 动画与视觉反馈

静态的UI是乏味的。为面板的打开和关闭添加动画能显著提升体验。

  1. 使用Animator:为Panel_Settings添加Animator组件。
  2. 创建动画控制器:在Project窗口创建Animator Controller,并拖给Animator
  3. 制作动画:双击打开Animator Controller,在Parameters中创建Bool型参数IsOpen。创建两个状态:ClosedOpen。右键Make Transition创建它们之间的双向过渡,并将过渡条件设置为IsOpen
  4. 录制动画:选中Panel_Settings,打开Animation窗口。确保选中Open状态,点击录制,在第0帧将面板的Scale设为(0,0,0),在第15帧将Scale设为(1,1,1)。可以加上轻微的弹性效果。为Closed状态录制一个从(1,1,1)到(0,0,0)的动画。
  5. 代码控制:在打开面板的代码里,调用animator.SetBool(“IsOpen”, true);关闭时设为false

对于按钮,可以为其添加简单的PointerEnterPointerExit事件,来改变颜色或缩放,提供悬停反馈。这可以通过EventTrigger组件快速实现,或者写在自定义的按钮脚本里。

5. UGUI性能深度优化与高频问题排查

5.1 性能瓶颈分析与工具使用

UGUI的性能问题通常体现在CPU端(重建)和GPU端(过度绘制)。

  1. CPU瓶颈 - Canvas重建:这是最常见的性能杀手。当UI元素的属性(如位置、颜色、文本内容)发生变化时,会标记其所在的Canvas为“需要重建”。重建过程包括网格重建(Rebuild)和合批(Batch)。频繁重建会导致CPU峰值。
    • 诊断工具:使用Unity Profiler的UIUI Details模块。重点关注Canvas.SendWillRenderCanvases的耗时,它代表了Canvas重建的总开销。查看其子项,找出是哪个Canvas或哪个具体的UI组件(特别是Text)引发了重建。
    • 优化策略
      • 动静分离:如前所述,这是首要原则。
      • 避免每帧更改Text.text:例如,血量数字。如果必须每帧更新,考虑使用对象池管理一个数字文本集合,只更新数字本身,而不是销毁再创建。或者,对于频繁变化的数值,可以间隔几帧更新一次。
      • 谨慎使用Layout GroupLayout Group在自身或子物体变化时会触发布局计算,可能引起连锁重建。对于静态布局,在编辑好后可以移除Layout Group组件,或者添加Content Size Fitter后也尽量移除。
  2. GPU瓶颈 - 过度绘制与Draw Call
    • 过度绘制:指一个像素被多次绘制。UI层叠越多,过度绘制越严重。可以通过Scene视图的Overdraw渲染模式(需在渲染设置中开启)来查看。
    • Draw Call:使用Frame Debugger工具查看每一帧的绘制调用。UGUI的合批情况在这里一目了然。
    • 优化策略
      • 使用图集(Atlas):将多个小图片打包到一张大图集中,这样使用这些图片的UI元素可以合批。Unity有自带的Sprite Atlas功能。
      • 减少透明区域:图片中不必要的透明区域会增加填充率负担。让美术尽可能裁剪图片。
      • 合并层级:尽量让使用相同材质(图集)的UI元素在Hierarchy中连续排列,中间不要插入使用不同材质的元素,这会打断合批。

5.2 高频问题与实战解决方案

以下是我在项目中反复遇到的典型问题及其解决方法:

问题现象可能原因解决方案
UI点击无响应1. 场景中无EventSystem对象。
2. UI元素上无Graphic组件(如Image)或Raycast Target未勾选。
3. 有更大范围的透明UI(如全屏遮罩)拦截了事件,且其Raycast Target为true。
4. Canvas的Render ModeWorld Space,但Graphic RaycasterBlocking Objects设置不当。
1. 检查Hierarchy,确保存在EventSystem
2. 为需要交互的UI添加Image(可设为完全透明)并确保Raycast Target为true。
3. 检查上层UI,关闭不必要的Raycast Target
4. 根据需求调整Blocking Objects或使用Physics Raycaster
文字模糊或锯齿1. 使用了旧版Text组件,渲染质量差。
2.TextMeshPro的字体纹理分辨率过低,或Atlas Resolution设置太小。
3. Canvas的Render ModeWorld Space,且Canvas的ScaleDynamic Pixels Per Unit设置不当。
1.全面切换到TextMeshPro (TMP)。这是解决字体问题的根本。
2. 在TMP Font Asset Creator中导入字体时,提高Atlas Resolution(如1024)。
3. 对于World Space UI,调整Dynamic Pixels Per Unit或使用Reference Pixels Per Unit
UI在特定分辨率下错位1. RectTransform的锚点设置错误,未使用自适应布局。
2. Canvas的UI Scale Mode设置不当。
1. 复习3.2节,使用拉伸锚点配合边距进行布局。
2. 对于需要严格比例缩放的UI,使用Scale With Screen Size模式并设定合适的Reference Resolution。对于需要保持像素感的游戏(如像素风),可使用Constant Pixel Size
滚动列表(ScrollRect)卡顿1. 列表内元素过多,即使不可见也在参与渲染和布局计算。
2. 列表元素结构复杂,重建开销大。
1.使用对象池。这是必须的!自己实现或使用Asset Store的插件(如Unity UI Extensions中的RecyclingListView)。
2. 简化列表项的结构,减少子物体和组件数量。
合批失败,Draw Call过高1. 使用不同图集或材质的UI元素交错排列。
2. UI元素层级中插入了带有MaskRectMask2D的组件。
3. 使用了Canvas Group并改变了Alpha(会打断合批)。
1. 在Hierarchy中重新排序,让相同材质的元素连续排列。
2.Mask会打断合批,如果可能,用RectMask2D代替,它对合批更友好。
3. 注意Canvas Group的Alpha变化对合批的影响,必要时将需要改变透明度的部分分离到独立Canvas。

5.3 关于TextMeshPro的特别注意事项

TMP是UGUI文本的工业标准,但它也有一些“坑”:

  • 字体图集溢出:如果你的游戏使用了大量不同字号、字重的文字,可能会导致动态添加的字符挤满默认图集,新字符无法渲染(显示为方块)。需要在TMP Settings中增大Dynamic Atlas的尺寸,或者预先在字体资源中包含所有需要的字符。
  • 材质实例化:TMP默认会为每种字体效果组合创建独立的材质实例。如果大量文本使用相同的字体但不同的颜色,会导致材质实例增多。可以考虑使用Font Material属性来共享材质,或者通过脚本动态修改顶点颜色来实现变色,而不是直接改color属性。
  • 中文支持:对于中文项目,务必在导入字体时,在Character Set中选择Custom Characters,并手动填入或从文本文件导入所有需要用到的汉字,以确保它们被打包进图集。否则,运行时动态添加会非常消耗性能。

6. 进阶:UI架构与框架设计思考

当UI系统变得庞大时,如何组织代码就成了关键。直接在每个按钮的OnClick事件上挂载方法会很快导致代码混乱、难以维护。这里分享几种常见的UI架构模式。

1. MVC/MVP模式:这是一种经典分离模式。以MVP为例:

  • Model:数据层,存储UI需要显示的数据(如玩家金币数、设置选项值)。
  • View:就是我们的UGUI GameObject,它持有各个UI组件的引用(Text,Slider,Button等),但只负责显示和接收输入,不处理业务逻辑。
  • Presenter:中间人,从Model获取数据,更新View;接收View的输入事件,处理逻辑,并更新Model。 这种模式结构清晰,易于单元测试,但会引入较多的类和接口,对于小型项目可能显得繁琐。

2. 基于事件总线的消息驱动模式:这是我在中型项目中更偏爱的一种松耦合方式。核心是一个全局的、单例的EventDispatcher或使用C#的event/Action

  • UI控件(View)在发生交互时,不直接调用逻辑代码,而是抛出一个事件(如“OnSettingsConfirmClicked”)。
  • 负责业务逻辑的模块(如SettingsManager)订阅这个事件。
  • 逻辑处理完后,如果需要更新UI,再抛出另一个事件(如“OnVolumeChanged”),由UI层自己订阅并更新显示。 这种方式极大地降低了模块间的直接依赖,UI可以独立开发,逻辑模块也可以方便地替换。

3. 使用UniTask/Rx(响应式编程)处理异步UI流:现代游戏UI充满了异步操作:等待网络请求、播放动画序列、等待用户选择。传统的回调地狱(callback hell)让代码难以阅读。UniTask提供了强大的异步/等待支持,可以让UI逻辑像写同步代码一样清晰。

// 使用UniTask等待一个确认弹窗的结果 public async UniTask<bool> ShowConfirmDialogAsync(string message) { var dialog = Instantiate(confirmDialogPrefab); var comp = dialog.GetComponent<ConfirmDialog>(); comp.SetMessage(message); // 等待用户点击“是”或“否” var result = await comp.WaitForDecisionAsync(); Destroy(dialog); return result; } // 在逻辑中清晰调用 if (await uiManager.ShowConfirmDialogAsync(“是否保存?”)) { SaveGame(); }

Rx(Reactive Extensions)则擅长处理数据流,可以将一个数据源(如玩家的实时血量)自动地、声明式地绑定到多个UI显示组件上,无需手动在每个数据变化的地方去更新UI。

选择哪种架构,取决于项目规模和团队习惯。对于个人或小团队,从简单的“管理器+事件”模式开始,保持代码整洁即可。当界面超过几十个时,就需要认真考虑引入更系统的架构了。

我个人在实际项目中的体会是,没有银弹。UGUI本身是一个强大的工具,但把它用好的关键在于理解其底层原理(如合批、重建),并在此基础上建立适合自己项目的代码组织和资源管理规范。性能优化往往不是靠某个神奇的设置,而是靠对细节的持续关注和对工具的熟练使用。每次在Profiler里找到那个导致卡顿的Text组件,或者通过调整锚点让界面完美适配了新机型,那种成就感,正是驱动我们不断深入钻研的动力。最后再分享一个小技巧:为你项目中的通用UI组件(如按钮、标签、弹窗背景)建立一套标准的Prefab和样式规范,并让团队所有人都遵守,这在长期开发中节省的时间将是巨大的。

← 返回列表