首页 / 资讯中心 / 文章详情

UGUI转DOTS实战:将传统UI系统接入ECS架构的完整方案

UGUI转DOTS实战:将传统UI系统接入ECS架构的完整方案 ★ FEATURED ARTICLE
简介UGUIDots 是一套面向数据技术栈DOTS的 UI 解决方案专为希望在 Unity 中兼顾 UGUI 工作流与 ECS 性能的开发者设计。它并非替代 Unity 原生 UI而是在其基础上扩充让现有 UI 设计流程基本保持不变同时适配 Scriptable Render Pipeline并针对安卓、Windows 64 位、macOS、Linux 64 位等平台提供低开销支持适合具备一定 DOTS 与 ECS 基础的中高级开发者研究使用。资源包共 245 个文件约 186KB其中 69 个 cs 脚本承载核心逻辑与控件实现12 个 asmdef 划分模块程序集16 个 md 文档提供说明另有 hlsl、shader、mat 等渲染相关文件及 meta、json 等配置项结构清晰。目前已有 329 人学习。通过该库读者可了解如何将 UGUI 与实体组件系统、渲染管线结合掌握面向数据的 UI 架构思路与模块划分方式为移动端和桌面端项目提供可参考的低开销 UI 实现路径。1. 当UGUI撞上DOTS一个让老UI系统跑进ECS架构的转换方案如果你正在用Unity的DOTS技术栈做项目大概率遇到过这个尴尬局面逻辑层已经全面ECS化System跑得飞快但UI层还挂在传统的UGUI上——Canvas重建、Graphic Raycaster、Layout Rebuild这些操作全在主线程上跑跟Job System和Burst编译出来的高性能逻辑层格格不入。UGUIDOTS这个项目就是冲着这个痛点来的它尝试把UGUI的组件体系转换成兼容DOTS的数据导向结构让UI元素也能以Entity的形式被System驱动而不是继续依赖MonoBehaviour那套生命周期。适合谁已经上手Entity Component System、想进一步把UI也纳入DOTS管线的开发者或者正在评估DOTS能否覆盖完整项目技术栈的团队。它不是一个开箱即用的UI框架更像一座桥——把UGUI的Image、Text、Button这些概念映射到ECS世界里。2. 架构拆解UGUI组件如何映射到Entity与System2.1 核心映射思路从GameObject到Entity传统UGUI的层级是GameObject树每个UI元素挂载RectTransform、CanvasRenderer、Image或Text等组件。UGUIDOTS的做法是遍历这棵树把每个UI节点转换成一个Entity原本的组件数据拆成多个IComponentData。比如RectTransform的anchoredPosition、sizeDelta、anchorMin、anchorMax会被拆进一个叫RectTransformData的组件里Image的sprite引用、color、raycastTarget则进入ImageData。这样做的好处是数据在内存里连续排列System可以用IJobEntity批量处理而不是像原来那样每个Graphic单独走一遍CanvasUpdateRegistry。转换的入口通常是一个Authoring脚本挂在Canvas根节点上。它在Convert阶段递归遍历子节点为每个UI元素创建Entity并挂载对应的组件。常见做法是用Unity的IConvertGameObjectToEntity接口或者写一个专门的Baker。下面是一个简化的转换逻辑示意// 挂在Canvas根节点上的Authoring脚本 public class UGUIDotsAuthoring : MonoBehaviour, IConvertGameObjectToEntity { public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem) { // 递归遍历所有子UI节点 var graphics GetComponentsInChildrenGraphic(true); foreach (var graphic in graphics) { // 为每个Graphic创建独立Entity var uiEntity conversionSystem.CreateAdditionalEntity(graphic); // 提取RectTransform数据 var rect graphic.rectTransform; dstManager.AddComponentData(uiEntity, new RectTransformData { AnchoredPosition rect.anchoredPosition, SizeDelta rect.sizeDelta, AnchorMin rect.anchorMin, AnchorMax rect.anchorMax, Pivot rect.pivot }); // 根据Graphic类型挂载不同数据组件 if (graphic is Image img) { dstManager.AddComponentData(uiEntity, new ImageData { Color img.color, RaycastTarget img.raycastTarget, SpriteIndex RegisterSprite(img.sprite) // 精灵图统一注册 }); } else if (graphic is Text txt) { dstManager.AddComponentData(uiEntity, new TextData { Content txt.text, FontSize txt.fontSize, Color txt.color }); } } } }这段代码的关键点在于每个Graphic变成独立Entity后原本靠Transform父子关系维持的布局层级需要用Parent组件或自定义的UIRelationship组件来重建。参数上AnchoredPosition和SizeDelta直接照搬但AnchorMin/Max在ECS里需要配合一个布局计算System来使用不能指望Unity自动帮你算——这是转换后最容易翻车的地方。2.2 System驱动布局计算与渲染提交Entity建好之后接下来是System接管。UGUIDOTS通常会把UI处理拆成几个System布局计算System、渲染数据准备System、以及最终的渲染提交System。布局计算System负责根据Anchor和Pivot算出每个元素的最终屏幕坐标这一步可以用IJobEntity并行处理因为每个元素的计算互不依赖——前提是你已经用Parent组件建立了层级关系并且按层级深度排序。渲染数据准备System则把ImageData、TextData里的内容转换成Mesh所需的顶点和UV数据。这里有个关键设计所有UI元素的Mesh数据会写进一个DynamicBuffer最终由渲染System一次性提交。相比UGUI每个Graphic单独一个CanvasRenderer的做法这种方式减少了Draw Call的提交次数但代价是你得自己管理Mesh的顶点数上限和Buffer容量。// 布局计算System的简化结构 [BurstCompile] public partial struct UILayoutSystem : ISystem { [BurstCompile] public void OnUpdate(ref SystemState state) { // 按层级深度排序后并行计算 var layoutJob new UILayoutJob { CanvasSize SystemAPI.GetSingletonCanvasData().Size }; state.Dependency layoutJob.ScheduleParallel(state.Dependency); } } [BurstCompile] public partial struct UILayoutJob : IJobEntity { public float2 CanvasSize; void Execute(ref RectTransformData rect, in AnchorData anchor, in ParentData parent) { // 根据父节点位置和锚点计算最终位置 float2 parentPos parent.CachedPosition; float2 parentSize parent.CachedSize; float2 anchorPos parentPos parentSize * rect.AnchorMin; float2 finalPos anchorPos rect.AnchoredPosition; rect.ComputedPosition finalPos; rect.ComputedSize rect.SizeDelta; } }参数说明CanvasSize来自Canvas的ReferenceResolutionAnchorData是转换时从RectTransform提取的锚点信息ParentData里缓存了父节点的计算后位置和尺寸。注意这里用ScheduleParallel的前提是每个Entity的布局计算不依赖同一帧内其他Entity的结果——如果存在嵌套布局组HorizontalLayoutGroup之类就得改成按层级串行或者用多Pass处理。2.3 渲染管线对接从Entity到屏幕像素最后一步是把计算好的UI数据送到屏幕上。UGUIDOTS一般不会直接操作底层图形API而是借助Unity的Graphics.DrawMeshInstanced或者BatchRendererGroup来提交。常见做法是创建一个专门的UIRenderSystem在OnUpdate里收集所有可见UI元素的Mesh数据合并成一个大Mesh然后用一个Material通常是UI/Default的变体一次性画出来。这里有个取舍如果UI元素数量少几十个合并Mesh的开销可以忽略但如果上千个元素每帧合并的CPU开销可能比UGUI的Canvas重建还大。所以UGUIDOTS的渲染System通常会做脏标记——只有位置、颜色、文本内容变化的元素才重新生成顶点数据其余复用上一帧的Buffer。3. 跑起来环境配置与最小可运行示例3.1 依赖版本与项目设置UGUIDOTS依赖Unity的Entities包和Burst编译器。根据项目正文和常见实践你需要Unity 2021.3 LTS或更新版本Entities 1.0以上如果是0.x版本API差异较大需要对应调整。Package Manager里至少要有com.unity.entities、com.unity.rendering.hybrid、com.unity.burst、com.unity.collections。另外Project Settings里要把Scripting Backend切到IL2CPP或者至少开启Burst编译否则Job的性能优势体现不出来。一个容易忽略的点Entities 1.0之后Baker和IConvertGameObjectToEntity的用法有变化旧版教程里的GameObjectConversionSystem在1.0里已经废弃。如果你拿到的UGUIDOTS代码是基于0.x写的升级到1.0需要把转换逻辑迁移到Baker体系。常见做法是写一个UGUIDotsBaker继承Baker 在Baker里用CreateAdditionalEntity创建UI Entity。3.2 最小场景搭建步骤先建一个空场景然后按下面步骤操作在Hierarchy里右键创建UI Canvas把Canvas的Render Mode设为Screen Space - Overlay。在Canvas下创建几个Image和Text作为测试元素随便摆几个位置。把UGUIDotsAuthoring脚本挂到Canvas根节点上。创建一个空GameObject挂上UGUIDotsSettings脚本如果有的话指定默认字体和Sprite Atlas。进入Play模式打开Entity DebuggerWindow Entities Hierarchy确认UI元素已经变成Entity并且挂载了RectTransformData、ImageData等组件。如果Entity Debugger里看不到Entity检查两点一是Canvas上是否有Convert To Entity组件旧版需要新版Baker自动处理二是Authoring脚本是否在Convert阶段正确执行——可以在Convert方法里加Debug.Log确认遍历到了子节点。3.3 验证渲染输出跑起来之后如果Game视图里UI没显示先看Console有没有Burst编译错误或Job安全警告。常见问题是Mesh Buffer容量不够——默认的DynamicBuffer可能只分配了几百个顶点UI元素一多就溢出。解决办法是在UIRenderSystem的OnCreate里用EntityManager.SetBufferCapacity或者直接在Authoring里给Canvas Entity挂一个足够大的Buffer。另一个验证点是Draw Call数量。用Frame Debugger看UI部分的Draw Call如果UGUIDOTS工作正常所有UI元素应该合并成1到2个Draw Call而不是像UGUI那样每个不同Material的Graphic各占一个。如果Draw Call没降下来检查Material是否统一——所有UI元素必须用同一个Material实例才能合批。4. 避坑与排查转换过程中最容易翻车的几个点4.1 锚点计算错位现象转换后UI元素位置全部偏移或者缩放后不跟随屏幕边缘。原因RectTransform的AnchorMin/Max在UGUI里是相对父节点的归一化坐标但转换到ECS后如果ParentData里缓存的父节点尺寸没有及时更新锚点计算就会基于错误的父尺寸。特别是Canvas本身作为根节点时它的尺寸来自Screen.width/height而不是RectTransform.rect。解决在布局System里Canvas根Entity的尺寸要单独从Screen或Camera取不能走通用的ParentData逻辑。另外AnchorMin和AnchorMax相等时点锚点和不等时拉伸锚点要分开处理拉伸情况下SizeDelta的含义是相对于锚点区域的偏移量不是绝对尺寸。4.2 文本渲染变成方块现象Text组件的文字全部显示为方块或空白。原因UGUIDOTS转换时只保存了文本内容和字号但字体图集的UV信息没有正确传递。UGUI的Text组件依赖Font和FontTextureECS里需要把字符对应的UV坐标预先算好写进Buffer。解决在转换阶段就把文本预渲染成字符UV数组或者用一个专门的TextMeshPro转DOTS的方案。如果项目里文本量不大常见做法是每个字符生成一个QuadUV从字体图集里取——但这会显著增加顶点数需要权衡。4.3 Job安全警告导致System不执行现象Console里刷InvalidOperationException提示Job不能访问某组件。原因布局计算Job里访问了ParentData但ParentData可能被其他Job同时写入触发安全系统报错。解决给ParentData加[NativeDisableParallelForRestriction]或者把布局计算拆成两个Pass——先串行更新父节点缓存再并行计算子节点。更稳妥的做法是用ComponentLookup在Job里只读访问父节点数据避免直接依赖。4.4 渲染顺序错乱现象UI元素重叠关系不对后创建的Image盖住了先创建的Text。原因ECS里Entity的创建顺序不等于渲染顺序UGUI原本靠Hierarchy里的SiblingIndex决定谁在前谁在后转换后这个信息丢失了。解决在转换时把SiblingIndex写进一个SortOrder组件渲染System按SortOrder排序后再提交Mesh。如果UI层级复杂还需要考虑Canvas的Sorting Layer和Order in Layer这些都要在转换阶段提取并保存。4.5 性能不升反降现象换成UGUIDOTS后帧率反而比原生UGUI低。原因UI元素数量少的时候比如不到50个ECS的调度开销和Mesh合并开销超过了UGUI的Canvas重建开销。DOTS的优势在大批量数据处理上UI元素太少时反而吃亏。解决先做Profiler对比确认瓶颈在System调度还是Mesh合并。如果元素确实少可以考虑混合方案——静态UI用UGUI动态大量元素比如列表、背包格子用UGUIDOTS。不要为了DOTS而DOTS。5. 二次开发从能跑到好用的改造思路把UGUIDOTS跑通只是第一步真正用到项目里还得做几件事。第一是事件系统对接。UGUI的Button、Toggle靠EventSystem和Raycaster处理点击ECS里没有这套东西。常见做法是写一个UIRaycastSystem在Job里做屏幕坐标到UI元素的命中检测命中后往EventBuffer里写事件再由业务System消费。命中检测可以用空间划分加速比如按UI元素的包围盒建一个BVH避免每帧遍历所有元素。第二是文本动态更新。如果UI里有频繁变化的文本倒计时、分数每帧重新生成字符UV开销很大。我一般会做一个字符缓存池把常用字符的UV预先算好存进BlobAsset运行时只更新顶点位置和颜色不重新计算UV。这样文本更新的开销能降到接近零。第三是跟现有UGUI混用。完全抛弃UGUI不现实很多第三方插件还依赖它。一个可行的方案是让UGUIDOTS只接管高频更新的UI部分比如战斗HUD、伤害数字静态界面继续用UGUI。两者通过Canvas的Sorting Layer隔离互不干扰。验证改造是否成功我习惯用三个指标Frame Debugger里的UI Draw Call数、Profiler里UIRenderSystem的耗时、以及Entity Debugger里UI Entity的数量是否稳定不随帧数增长。如果Draw Call没降、System耗时超过2ms、或者Entity数量每帧都在涨说明改造引入了新问题得回头查。从那以后我每次做DOTS UI改造都强制先跑一遍最小场景——一个Canvas、两个Image、一个Text确认转换、布局、渲染三条链路都通了再往项目里搬。这个习惯帮我省了不少调试时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站