概述
后来发现Mesh不能序列化,转换为向量数组后。然后再调了实机,将Loading的总时间调出来看了,结果如下:正常界面MeshBake
也就是负优化。
Loading.ReadObject变慢的原因是,将Mesh存入Prefab后导致Prefab变大,所以解析Prefab的序列化时间变长了。
Instantiate变慢的原因是。在执行Awake前,它需要先Copy。
最后把生成网格阶段节约下的时间都给浪费了。
不能保存Mesh只能存向量数组多出来的转换时间倒不多,本来速度还有至少2倍的优势的……
不过,Instantiate的Copy部分的消耗倒是可以将网格存成单独的文件类来回避。
所以,我后来又把生成的Mesh都单独放在了另一个自定义Asset文件里(不能把Graphic也传了,否则它指向的不是Instantiate之后的Grpahic),搞的很麻烦。编辑器里能运行,但是打包后显示:A script behaviour (script unknown or not yet loaded) has a different serialization layout when loading.这样的迷之错误,google了也有其他人问但没有任何解答,或者这就是Unity的序列化被人称作“没法用”的原因。
考虑到即使做好了也不会有多少改观,很可能还是负优化,所以就放弃了。
最后的源文件:
虽然这方法失败了,还是给点这个过程中得出的结论吧:
1.UI界面事先用异步的方式后台加载,然后显示时再执行Instantiate,还是可以减少一半的加载卡顿的。而且你还可以通过将asset.SetActive(false) ,再Instantiate的方式,将Instantiate.Produce,Instantiate.Copy预先执行(此时界面没有初始化也没有显示),最后在需要界面的时候再gameObject.SetActive(true),正式显示,就只需要承担Awake时建立网格的代价了。
IEnumerator PreLoading()
{
var request = Resources.LoadAsync("Canvas1");
yield return request;
//异步加载是基本不会影响主线程卡顿的,不管主线程有多繁忙。
//你只需要注意大纹理,但大纹理只有图集,通常早就加载好了。
//避免分界面的大纹理即可。DrawCall没那么重要。
GameObject asset = request.asset as GameObject;
asset.SetActive(false);
GameObject go = GameObject.Instantiate(asset);
//在disable下不完全的Instantiate,界面实际并没有初始化也不会显示。
//但这个过程是在主线程上执行的,会导致主线程卡顿,所以务必多物体分帧执行。
//大预制可分拆多份。
//实在处理不好,就只能延后到正式初始化那里一起卡了。但先LoadAsync还是会好很多。
}
go.SetActive(true);//需要时再正式初始化
这个异步加载,是在你界面空闲时候,预测你可能新打开的界面,提前加载的意思。并不是说打开的时候再执行这个流程,那样只会更慢。
当然你也可以选择在切换场景的等待时间内进行同步的预加载。有这么做的,但负责得讲,体验极差(从战斗回到UI界面的等待时间和进入战斗差不多)
2.将界面分成多预制加载。不需要的组件先disable,然后再逐步激活有一定缓解,但还是逃不掉Instantiate.Copy,彻底的分离还是得多预制。但如果用前者已经足够,也可以不用像后者这么麻烦。
3.文字描边也是个热点,少用,或者用Shader方式的描边,或者改写Text在内部实现描边。下面有写。但是由于前面的负担太重,文字这边的改善占比有限。
4.如果Unity能够提供功能,让Instantiate.Produce,Instantiate.Copy可以在子线程进行,可以大大缓解卡顿压力,实现上是容易的,但是需要改变现在的预制实例化逻辑,以他们的尿性估计没戏。预制复制这里不解决,他们怎么解决UI显示的加载问题都是杯水车薪,就如同我这次做的事一样。
—————————————————————————————————————————
以下原文:
测试场景
Before
After(都没开DeepPofiler,Profile干扰应该不大)
代码下载
原理很简单,在编辑期间获取UI生成出来的网格数据并保持下来,然后在运行期间代替正真的UI直接显示。
我最终没有将多个UI的网格合并,而是单独保存,然后禁用原UI组件,接管UI原本对应的CanvasRenderer。这样带来的以下几个好处:
1.不需要用编辑器脚本处理预制删掉原UI,随时可用,也用不着生成一堆仅用来多材质分批的CanvasRenderer了。
2.工作流简单。在Bake之后依然可以正常编辑界面,似乎Bake这件事并不存在。而且可以随时切换单个UI的Bake状态,也不需要改变原来的UI层级。
3.运行期间,可以随时低消耗地将组件变回原来的样子,恢复之前的一切功能。所以可以直接对整个界面做Bake,然后需要界面变化时,用代码解除相应UI组件的bake状态即可。
4.运行期间,即使不解除bake,也可以控制组件的位置缩放和显隐(显隐需要执行MeshBake组件的Render令其更新,可以指定更新这一个组件),理论上也可以通过扩展给予更多Bake期间的属性变化,比如变色。
5.代码变得很简单,很容易看懂。
核心代码
//利用反射让组件Rebuild一次
var methodInfo = graphic.GetType().GetMethod("UpdateGeometry", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (methodInfo != null)
methodInfo.Invoke(graphic, null);
//这时界面网格会临时存在于workerMesh,将它的内容复制走即可。
CopyTo(workerMesh,bake.mesh)
//最后显示的时候直接
renderer.SetMesh(bake.mesh);
用法:
将MeshBake.cs挂在窗体的根节点上。当然你也可以随便挂在其他方便的地方,支持嵌套。
MeshBake本身是个Graphic,所以你挂的节点至少不能有其他UI组件。
然后把里面需要Bake的UI组件enable设置为false。
然后就行了。
虽然界面看上去没变化,照样能编辑,但现在负责绘制的是MeshBake,这个被禁用的组件只是一个数据容器而已。
MeskBake会把Bake过的组件显示出来。另外你也可以点上面的SetAllImage按钮将子级的全部Image设置为disable,这样就都被收纳到Bake的范围里了。是否Bake的依据就是组件是否enable。
所以enable这个属性就不能用了,如果你希望隐藏组件,可以用graphic.canverRender.cull属性,也可以直接将gameObject禁用。
当然,运行期间未Bake的组件enable还是可以随便改的,和以前没区别。
运行期间,如果你将Bake的组件enable设置回true,它就不再受MeshBake的控制了,爱干嘛干嘛。
BatchAll按钮基本没卵用。Batch操作会在界面组件变化时执行,如果你嫌太卡,可以把相应代码注释了,用这个按钮手工更新。
这个做法最大的问题是,当图集变化时,网格当然不会跟着更新。所以你需要在打包前打开一次界面,或者手动执行界面内的MeshBake的BakeAll()。可以用打包脚本解决。
另外,Text当然也可以放在里面,但显然只能是静态文本。
不能处理动态文本挺亏的,因为文本其实才是性能热点。尤其是描边后的文本。可以看我这篇
后面提供的描边Shader和将描边内置到Text内的做法都能成倍提高文字初始化速度,前面那两功能可以无视。
当然,用的是静态文本就可以用这个MeshBake了。静态文本也不是完全不能考虑的吧?多语言改改我的代码,保存多份Mesh也能实现的。
MeshBake的绘制操作有一定消耗,它的Rebuild规则和Graphic一样,改这个组件宽高和enable,换父级的时候执行,平时基本不会激活的,需要的时候可以执行Render激活(也做了只更新选定组件的功能)。这个你们也可以改。
这东西刚做出来而已,没有经历过项目的实际考验。但是原理一看就明白,风险也就小,再考虑到其效能和使用成本……
看看吧。
不过不知道整个合并到一个Mesh会不会效能更高一点,虽然个人觉得没戏。还是说直接不用Canvas?
最后
以上就是唠叨白开水为你收集整理的instantiate 卡顿严重_MeshBake——极大减少UI的Instantiate耗时的全部内容,希望文章能够帮你解决instantiate 卡顿严重_MeshBake——极大减少UI的Instantiate耗时所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复