优化uGUI中的SetParent的调用开销

概述

当前项目是一个类似scratch那种积木式的编程工具,编辑器用Unity的uGUI实现,但是对于大型的的工程(600多个block,每个block大概有5、6个GameObject),原有的实现在加载工程时很慢,profiler中查看,发现有很大一部分时间花在了RectTransform.SetParent调用上,这几天花了点时间,优化了这部分调用。

原因

RectTransform.SetParent调用,会触发许多其他的调动,比如Graphic.OnBeforeTranformParentChange,由于工程比较大,场景中有大量的GameObjects,许多SetParent调用导致了加载性能的降低。

原有的工程加载步骤为

  1. 实例化所有GameObjects
  2. Load保存的数据
  3. 恢复GameObjects之间的引用,此时会调用SetParent重新恢复父子关系

由于最初序列化数据结构设计的问题,无法在实例化时知道parent是谁,所以导致Instantiate调用时不能传入parent参数来减少不必要的开销。

解决方法

想到的一个方法是不依赖于Transform的父子关系,而是手动计算所有的坐标,这样我们在实例化后就不需要调用SetParent,就不会有UI的开销了。

为了保持当前代码基本结构不变,即使用本地坐标的地方可以依然指定本地坐标,引入了一个逻辑坐标系的概念(LogicTransform)。逻辑坐标系的层级结构同原来使用unity的Transform建立的层级结构,主要的区别在与我们自己计算整个层级中的坐标,而不是unity代劳。加上我们的ui不需要旋转,只涉及缩放和平移,实际计算代码很简单。下面是一个GameObjects的层级,其中黑色箭头组成的逻辑层级,红色箭头组成的unity的Transform的层级。

LogicHierarchy and UnityHierarchy

每个LogicTransform有一个对应的Transform,两者之间满足如下关系

  1. LogicTransform.localScale == Transform.localScale
  2. Transform在逻辑层级中本地坐标(非localPositon),等于LogicTransform.worldPosition

采用这种实现后,工程加载的时间从最初的30s降低到了15s(依然很慢)。

优化ReapplyDrivenProperties

Profiler中还发现,RectTransform.reapplyDrivenProperties也很费时,查看了uGUI的源码发现,这个部分开销来源于LayoutRebuilder在该事件处理中,调用了MarkForLayoutRebuild所致。由于我们的block布局完全是手动的,所以这部分开销也可以省掉。但是LayoutRebuilder的事件处理是私有的,所幸可以通过反射将事件处理函数替换掉。替换后的逻辑如下

static void ReapplyDrivenProperties(RectTransform driven)
{
	if (driven != null && ((1 << driven.gameObject.layer) & layersToIgnore != 0))
	{
		return;
	}
	else
	{
		// call the original event handler in LayoutRebuilder
		originalHandler(driven)
	}
}

这里将不需要自动layout的game objects分配到特殊的layer上避免开销。这步优化带来的提升不是很明显,大概节省了1.5s左右的时间。

优化SetActive调用

有些元素的显示和隐藏通过SetActive来完成。由于每次调用会触发OnEnable/OnDisable,在game objects相对较多的情况下,开销也不小,所以后来使用嵌套Canvas的方法来解决。改动后,加载速度有所提升。

结论

  1. 大量调用RectTransform.SetParent对性能有负面影响
  2. 避免不必要的SetActive,使用嵌套Canvas也可以提升部分性能
  3. 设计良好的Save数据结构应该可以解决重新SetParent带来的开销
### Unity UGUI 中设置物体显示在 Image 前面 为了使某个物体能够显示在 UGUI 的 `Image` 组件之前,可以通过多种方法来实现这一目标。以下是几种常见的方式: #### 使用 Canvas 排序层 通过调整不同 UI 元素所属的 `Canvas` 的排序层 (Sorting Layer),可以控制这些元素之间的前后关系。每一个 `Canvas` 都有一个名为 “Render Mode” 的属性,当其设为 Screen Space - Overlay 或者 World Space 时,可通过修改 `sortingLayerName` 和 `orderInLayer` 属性来进行更细致的层次管理[^3]。 对于希望置于最前的对象,应该将其放置于具有较高优先级的排序层中;而对于那些应当位于背景位置的对象,则应分配较低级别的排序层。此外,还可以在同一排序层内部利用 `orderInLayer` 来进一步微调各个对象间的相对次序。 #### 应用 BakeMesh 技术处理粒子效果 针对特定类型的视觉特效如粒子系统,采用 BakeMesh 方法可以让它们如同普通的 UGUI 控件那样被管理和排列。这不仅简化了层级管理工作流程,还允许粒子系统响应遮罩操作并与其他图形组件和谐共存[^1]。 具体做法是在想要前置展示的粒子系统的父节点下附加一个叫做 `UIParticle` 的自定义脚本实例,从而使得该粒子系统能够在渲染过程中自动转换成网格数据,并融入到标准的 UGUI 渲染管线之中。 #### 调整 Draw Call 合批策略优化性能 考虑到效率因素,在设计复杂的用户界面时也需要注意减少不必要的绘图调用次数(Draw Calls)。由于 UGUI 默认按照层级结构进行分组和合批,因此保持相似材质和纹理资源尽可能集中布置有助于提高整体表现力的同时降低开销[^2]。 ```csharp // 示例代码:创建一个新的Canvas用于特殊需求下的控件布局 using UnityEngine; using UnityEngine.UI; public class SpecialCanvasManager : MonoBehaviour { public void CreateSpecialCanvas(Transform parent){ GameObject canvasObj = new GameObject("SpecialCanvas"); Canvas specialCanvas = canvasObj.AddComponent<Canvas>(); specialCanvas.renderMode = RenderMode.ScreenSpaceOverlay; // or other modes as needed GraphicRaycaster raycaster = canvasObj.AddComponent<GraphicRaycaster>(); RectTransform rectTransform = canvasObj.GetComponent<RectTransform>(); rectTransform.SetParent(parent); // Set sorting order here to ensure it appears above/below others specialCanvas.sortingOrder = desiredOrderValue; } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值