Unity之工作总结.docx
- 文档编号:7636971
- 上传时间:2023-01-25
- 格式:DOCX
- 页数:7
- 大小:23.31KB
Unity之工作总结.docx
《Unity之工作总结.docx》由会员分享,可在线阅读,更多相关《Unity之工作总结.docx(7页珍藏版)》请在冰豆网上搜索。
Unity之工作总结
Unity之工作总结
Unity都会自己生成一套格式,并且打包的时候不会用文件夹下面的格式,而是Unity自己格式。
如果你用的UITexture你可以对每一张图来修改格式,比如颜色数比较少的图片可以使用16bit,如果没有透明,可以使用pvr或者etc这样图片会小很多。
如果是UISprite只要有透明的就必须使用RGBA32要不然UI会很难看。
除去UITextuer和Atlas的图片之外(因为有透明),其余的贴图必须是2的幂次方。
因为只有2的幂次方图片,并且没有透明通道的才会被压缩。
Ios会被压缩成pvr格式,Andriod会压缩成etc格式。
压缩之后会小很多。
人物贴图场景题图特效贴图一定要是2的幂次方。
贴图透明通道分离,压缩格式设为ETC/PVRTC最初我们使用了DXT5作为贴图压缩格式,希望能减小贴图的内存占用,但很快发现移动平台的显卡是不支持硬件解压DXT5的。
因此对于一张1024x1024大小的RGBA32贴图,虽然DXT5可将它从4MB压缩到1MB,但系统将它送进显卡之前,会先用CPU在内存里将它解压成4MB的RGBA32格式(软件解压),然后再将这4MB送进显存。
于是在这段时间里,这张贴图就占用了5MB内存和4MB显存;而移动平台往往没有独立显存,需要从内存里抠一块作为显存,于是原以为只占1MB内存的贴图实际却占了9MB!
所有不支持硬件压缩格式都有相同的问题。
解决方案:
现在Andriod硬件最广泛支持的时Etc,IOS上支持的时PVRTC。
但这两种格式都是不带透明(Alpha)通道的,因此我们需要将每张原始贴图的透明通道分离出来,写进另一张贴图的红色通道里,这两张特图都采用Etc/Pvrtc压缩。
渲染的时候将两张贴图都送进显存。
同时我们修改了NGUI的shader在渲染时第二张贴图的红色通道写入到第一张贴图的透明通道里恢复原来的颜色:
1、fixed4frag(v2fi):
COLOR
2、{
3、fixed4col;
4、col、rgb=tex2D(_MainTex,i、texcoord)、rgb;
5、col、a=tex2D(_AlphaTex,i、texcoord)、r;
6、returncol*i、color;} 这样,一张4MB的1024x1024大小的RGBA32原始贴图,会被分离并压缩成两张0、5MB的ETC/PVRTC贴图(我们用的是ETC/PVRTC4bits)。
它们渲染时的内存占用则是2x0、5+2x0、5=2MB。
lDrawCall当两个renderQueue相邻的DrawCall使用了相同的贴图、材质和shader实例时,这两个DrawCall就可以合并。
但需要注意的是DrawCall合并不见得会提高性能,有时反而会降低性能如果是UIGeometry为了渲染绘制准备数据,那么UIDrawCall其实是定义了渲染绘制需要的基本组件。
这里拿煮菜做个比喻帮助理解:
UIGeometry好比为煮菜准备食材,UIDrawCall好比是煮菜的工具(锅,炉子等),UIPanel就是大厨了决定着什么时候该煮菜,UIWidget(UILabel,UISprite和UITexture)是这道菜怎么样的最终呈现。
UIWidget分别用UpdateGeometry和WriteToBuffers对UIGeometry的ApplyTransform和WriteToBuffers进行封装调用,ApplyTransform是根据UIPanel的坐标调整Vertices的坐标,WriteToBuffers将UIGeometry的Vertices,UVs和Colors添加进UIPanel的Vertices,UVs和Colors的BetterList中。
这里还有一个细节就是:
UIGeometry中的Vertices,UVs和Colors的BetterList的buffer什么时候得到,因为这些都是UIWidget或其子类的信息,所以在UIWidget的子类UILabel,UISprite和UITexture中OnFill函数生成UIGeometry的BetterList的buffer。
UIWidget这个脚本中的两个函数:
WriteToBuffers,OnFill,UpdateGeometry。
WriteToBuffers和OnFill这两个函数都是将Vertices,UVs和Colors等add进参数的List中去,查看WriteToBuffers的调用出发现其参数是UIPanel的Vertices,UVs和Colors,而OnFill的参数是UIGeometry的Vertices,UVs和Colors。
WriteToBuffers只是对UIGeometry的封装调用,也就是说将UIGeometry的Vertices,UVs和Colors等信息add进UIPanel的对应List中。
UIGeometry的脚本,一直有一个疑问:
UIGeometry的Vertices,UVs和Colors的List没有看到add方法的执行,只有在WriteToBuffers被add。
直到看到了OnFill才恍然大悟,虽然UIWidget的OnFill是虚函数,没有具体实现,看了下UISprite重写的OnFill函数,就是把Vertices,UVs和Colors添加到UIGeometry的Vertices,UVs和Colors中。
所有UI组件的Vertices,UVs和Colors都汇集到UIPanel的Vertices,UVs和Colors去了,然后UIPanel指定给UIDrawCall渲染就行了UIWidget的一些实现细节,MakePixelPerfect():
对gameObject的localPosition和locaScale进行微调和纠正。
SetDirty():
调用UIPanel的SetDirty()对组件的变更进行重建(rebuilt)。
UiwidgeUIGeometry&UIDrawCall的关系:
UIWidget中有两个变量UiDrawCallmDrawcall和UIGeometrymGeo的vertsuvscols的BetterList,然后UiWidget的UpdateGeometry函数对UIGeometry的ApplyTransform()和WriteToBuffer()
调用进行更新、每一个UIwidget都有一个UIGeometry但是不都有一个UIDrawCall,而是通过Batch合并达到减少DrawCall的数量,UiDrawCall是有UiPanel生成的。
DrawCall的数量优化根据上述描述可以得出一个结论:
使用相同material的连续的Uiwidget(UILableUiSprite)公用一个UIDrawcall。
通过这个结论我们可以得到一个解决DrawCall过多的问题,UIPanel生成DrawCall时是Fill()方法。
Fill方法对UiWidget、list进行检测把使用相同Material的连续的Uiwidget合并生成一个DrawCall,UIWidget、List的排序是根据UiWidget的Depth进行的。
所以解决方案有两种:
1、修改UiWidget(UiLableUIwidget)的Depth,限定UIwidget、List的排序
2、重写Uiwidget的CompareFunc()
方法。
(重写UIWidget的CompareFunc也是可以的,按照Material的name优先排序,只有当material一样是才考虑depth进行排序:
)
3、无重叠时自动重排。
绘制顺序按照Hierarchy采用第二种方式减少DrawCall:
MaterialleftMat=left、material;MaterialrightMat=right、material;if(leftMat==rightMat){if(left、mDepth
=null&rightMat!
=null)
returnstring、Compare(leftMat、name,rightMat、name);if(leftMat!
=null)return-1;if(rightMat!
=null)return1;return(leftMat、GetInstanceID() -1: 1;夹层问题: 因为Material使用的Shader使用了透明,这样就不能做深度测试,也就是Mesh的“深度”是不影响的,这样最终的显示就跟Shader的renderQueue有关了,即renderQueue越大,显示的越靠前面(重叠的图层,renderQueue越大,越靠前)。 当然现在只有增加一个DrawCall(如多使用一个UIPanel或用另外一个Material)来做到Material的夹层效果。 特效层级: 粒子系统的渲染顺序列默认为3000而NGUI的渲染顺序默认是从3000开始,当有嵌套的Panel或者Deoth更高的panel时,NGUI的渲染顺序会高于3000解决方案: 1、修改Ngui中的panel脚本中的默认RenderQueue调整到3000以下,这样就不会挡住粒子特效。 当窗口显示在特效上面时把窗口的RenderQueue调整到3000以上,就解决了。 2、使用另外一个摄像机显示特效,但是Ui窗口切换时不太好控制 3、修改例子特效的shader中的RenderQueue的值(需要考虑特效中的层级关系)一级界面二级界面、、浮动窗口 1、不同图集项目中做到复杂一些的界面,经常会用到多个图集,以技能界面为例,项目中常用的图片放到共用图集中,这是一个图集,技能界面本身独有的元素,比如跟技能职业相关的背景,算作第二个图集,还有一些技能图标,图标单独归类到一个图集中,再一个就是字体的图集。 基本一个界面如此分法,最多需要4个图集。 NGUI的图集之间的处理,默认是靠调整控件的Z值来区分的,但是这里他可以调整同一个图集每个一个控件的Z值,其实不是很好。 经常会出现图层相互遮挡的情况,尤其对于控件比较多的界面,一段时间回过来再修改界面的时候,整个要崩溃。 解决方案: 在UIPanel中,为每一个Material添加一个layer的变量,当同一图层靠depth来决定前后关系,不同图层靠layer来决定前后关系,在绘制UIDrawCall的时候,根据layer对跟节点做一定偏移。 这样就能从Z值中解放出来。 如果大家也有碰到图层的问题,可以参考这样的做法,以此种方法来处理图层关系,简单,做过项目的圈套UI,还未有不能解决的情况。 lUI自适应ScalingStyle的作用是制定UiRoot的缩放类型,如果是PixelPerfect,MinimumHeight和MaximumHeight才起作用,scalingstyle选择的是Pixelperfect要对MinimumHeight和MaximumHeight进行设置。 (如果是PixelPerfect缩放类型,当屏幕的分辨率大于MaximumHeight,则以MaximumHeight为基础缩放,反之,如果屏幕分辨率小于MinimumHeight则以MinimumHeight为基础进行缩放。 例如,如果屏幕高度为1000,而设置的MaximumHeight值为800,则UI界面整体放大为原来的1000/800= 1、25倍。 )FixedSize: 跟ManualHeight有关FixedSizeOnMobiles: 跟ManualHeight有关只是针对IOS和Android上的判断,也就是说只有IOS和Android平台下FixedSizeOnMobiles才起作用、FixedSize或FixedSizeOnMobiles,则缩放只以ManualHeight为参考,屏幕分辨率的高度值不同于此设置值时,则根据其比例(即ScreenHeight/ManualHeight)对整棵UI树的进行“等比”缩放(宽度的缩放比也是此比例值)。 注释: 如果设置FixedSize,UIWidget、height(以UiRoot默认值进行高度缩放)是不会改变的,不管实际屏幕分辨率的像素是多少,AnchorStretch的背景图片高度始终是manualHeight、UIRoot下的UIWidget的height参数一直都是实际的值UIRoot是基于高度进行缩放的如何做以宽度适配。 UIRoot是基于高度放缩的,即放缩的比例是以高度为参考的,所以UIRoot有一个manualHeight的参数。 那么对于横版游戏显然不行,要是能实现基于宽度放缩。 所以可以通过设置一个“manualWidth”的参数来做。 比如我们项目中使用的是1024作为UI的宽屏尺寸,通过换算设置manualHeight的值: intheight=Mathf、Max(2,Screen、height);manualHeight=Screen、height*1024/Screen、width;//基于宽度的屏幕分辨率自适应注释: UIRoot其实就做了一件事情: 根据Screen、height和UIRoot、activeHeight的比例来调整UIRoot的loaclScal,从而保证UIWidget(UISprite,UILabel)可以按照其本身的大小进行设置,而不用经过复杂的换算过程。 l资源分离打包与加载资源分离打包与加载是最有效的减小安装包体积与运行时内存占用的手段。 一般打包粒度越细,这两个指标就越小。 但打包粒度也并不是越细就越好。 如果运行时要同时加载大量小bundle,那么加载速度将会非常慢时间都浪费在协程之间的调度和多批次的小I/O上了。 此需要有策略地控制打包粒度。 一般只分离字体和贴图这种体积较大的公用资源。 l关闭贴图的读写选项Unity中导入的每张贴图都有一个启用可读可写(Read/WriteEnable)的开关,对应的参数是TextureImporter、isReadable。 选中贴图后可在ImportSetting选项卡中看到这个开关。 只有打开这个开关才可以对贴图使用Textuer2 D、GetPixel,读取或改写贴图资源的像素,但这就需要系统在内存中保留一份贴图的拷贝,以供Cpu访问,但是一般游戏运行过程中不会有这样的需求,因此我们对所有贴图都关闭这个开关,只在中做贴图导入后处理(比如对原始提贴图分离透明通道)时需要打开这个选项。 这样,上文提到的1024x1024大小的贴图,其运行时的2MB内存占用又可以少一半,减小到1MB。 Texture图片空间和内存占用分析纹理大小影响: 可以将其他(非二的幂---比如怪物头顶的血条和伤害跳字放在同一个Panle上,并且这些Panle上只有这些控件,其余基本不变的控件放在别的控件Panle上,这样两类控件就隔开到不同的DrawCall在不同的Panle中,当一个控件发生变化而导致DrawCall重建时,就不需要遍历那些没有变化的控件。 因为在美术设计上,一段时间内在变化的控件总是少数,所以优化效果分明显,节省的CPU占用率能达到25%。 l优化锚点内部逻辑,使其只在必要时更新在上一点优化了Panel的DrawCall重建效率之后,我们发现NGUI锚点自身的更新逻辑也会消耗不少CPU开销。 即使是在控件静止不动的情况下,控件的锚点也会每帧更新(见UIWidget、OnUpdate函数),而且它的更新是递归式的,使CPU占用率更高。 因此我们修改了NGUI的内部代码,使锚点只在必要时更新。 一般只在控件初始化和屏幕大小发生变化时更新即可。 不过这个优化的代价是控件的顶点位置发生变化的时候(比如控件在运动,或控件大小改变等),上层逻辑需要自己负责更新锚点。 l降低贴图分辨率这一招说白了其实就是减小贴图素材的尺寸。 比如对一张在原画里尺寸是100x80的贴图,我们将它导入Unity后会把它缩小到50x40,即缩小两倍。 游戏实际使用的是缩小后的贴图。 不过这一招是必然会显著降低美术品质的,美术立马会发现画面变得更模糊,因此一般不到程序撑不住的时候不会采用。 l界面的延迟加载和定时卸载如果一些界面的重要性较低,并且不常被使用,可以等到界面需要打开显示的时候才从bundle加载资源,并且在关闭时将自己卸载出内存,或者等过一段时间再卸载。 不过这个方法有两个代价: 一是会影响体验,玩家要求打开界面时,界面的显示会有延迟;二是更容易出bug,上层写逻辑时要考虑异步情况,当程序员要访问一个界面时,这个界面未必会在内存里。 因此目前为止我们仍未实施该方案。 目前只是进入一个新场景时,卸载上一个场景用到但新场景不会用到的界面。 l避免频繁调用GameObject、SetActive我们游戏的某些逻辑会在一帧内频繁调用GameObject、SetActive,显示或隐藏一些对象,数量达到一百多次之多。 这类操作的CPU开销很大(尤其是NGUI的UIWidget在激活的时候会做很多初始化工作),而且会触发大量GC。 后来我们改变了显示和隐藏对象的方法让对象一直保持激活状态(activeInHierarchy为true),而原来的SetActive(false)改为将对象移到屏幕外,SetActive(true)改为将对象移回屏幕内。 这样性能就好多了。 lNGUI性能消耗点汇总使用LinkedList BetterList是一个数组LinkedList 对链表的操作要快于对数组的操作。 NGUI节点查找非常耗时。 l代码中非必要的堆分配1) 我们应避免使用foreach循环: 一般建议是避免使用foreach循环,尽量使用for或者while循环,我在Unity论坛遇到很多人提到这个建议。 这背后的原因咋一看似乎是合理的,foreach只是语法封装,因为编译器处理代码的流程大体是下面这样: foreach(SomeTypesinsomeList)s、DoSomething();转换为: using(SomeType、Enumeratorenumerator=this、someList、GetEnumerator()){while(enumerator、MoveNext()){SomeTypes=(SomeType)enumerator、Current;s、DoSomething();}}每次使用foreach时都会创建一个enumerator对象,一个System、Collections、IEnumerator的接口实例。 但是创建在堆栈上还是堆上是都可能的几乎所有的System、Collections、Genric(list 2) 应该避免使用闭包和LINQ吗? 匿名方法和lambda表达式会引起内存泄露吗? 答案是: 这取决于C#编译器,有两种区别很大的方式来处理intresult=0;voidUpdate(){for(inti=0;i<100;i++){System、Func 但是Mono只在第一次调用Update()方法时分配堆内存(在我的系统上只用了52字节),在之后的帧也没有更多的堆分配操作。 这是怎么了? 使用代码反编译器(将会在下篇文章解释)可以看见C#编译器只是简单的把myFunc替换为类的一个静态System、Func 这个字段的名字很奇怪但是也有一些意义: f__am$cache1(不同系统上可能会有差别),也就是说,托管方法只分配一次,然后就被缓存了现在我们在托管定义方式上做一些小小的改变: System、Func =>p*i++;通过把“p”替换为“i++”,我们已经局部定义方法转变成一个真的闭包(闭包是函数编程的一大支柱。 它把数据和函数联系到一起,更准确的说是非局部变量在函数之外定义。 )在myFunc中,p是一个局部变量但i是一个非局部变量,属于Update()方法。 C#编译器现在不得不将myFunc转换成可访问、甚至是可修改,包含非局部变量的方法。 它通过声明一个全新的类表示myFunc创建的引用来实现这一功能。 For循环每次执行都要分配一个类的实例,瞬间产生大量的内存泄露(在我的电脑上每帧26KB)闭包概念在C# 3、0的时候被引入主要原因是LINQ。 如果闭包回引起内存泄露3) 协程通过StartCoroutine()运行一个协程,会隐式分配UnityCoroutine类(系统上占用21字节)和Enumerator(占用16个字节),所以在游戏运行时尽量少用StartCoroutine()4) 字符串C#和Unity内存问题必须会提到字符串。 从内存角度,字符串很奇怪,因为,他们是堆分配且不变的。 当你连接两个字符串时(无论是变量还是常量): 运行时不得不分配至少一个新的字符串对象存储新的结果。 String、Concat()有效地通过调用FastAllocateString()分配新对象,但是必定会产生多余的堆分配(上面例子在我的系统上占用40字节)。 如果你需要在运行时修改或者连接字符串,最好使用System、Text、StringBuilder5) 装箱尽量减少装箱6) 库方法各种库方法也会隐式分配内存。 捕捉它们最好的方法是分析。 我之前已经写过的foreach-循环,大多数标准泛型集合不会导致堆分配。 Dictionary 然而,有点神秘地,Dictionary l查找堆分配的两种方式 1、使用Unityprofiler 2、反编译自己的代码在CIL中查找内存分配CIL代码的优势在于堆分配代码不会被隐藏。 相反,完全可以在反编译的代码中找到堆分配的三种指令。 1) newobj 通过构造函数创建一个指定类型的未初始化对象。 如果对象是值类型(结构体等),则在堆栈创建。 如果是引用类型(类等)则在堆上分配。 可以从CIL代码知道对象类型,所以,可以很容易知道在哪分配。 2) newarr<元素类型>: 在堆上创建一个数组。 元素类型在参数中指定。 3) box<值类型标记>: 装箱(传递数据)专用指令,在第一部分已经介绍过。 lUnity脚本执行顺序和编译顺序1) 脚本执行顺序Unity在后台会把每个脚本的Awake、Start、Update、LateUpdate、FixedUpdate等等,所有的方法合并到一起。 然后按照代码的执行顺序进行执行。 编译顺序首先从脚本语言类型来看,Unity3d支持3种脚本语言,都会被编译成CLI的DLL如果项目中包含有C#脚本,那么Unity3d会产生以Assembly-CSharp为前缀的工程,名字中包含”vs”的是产
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Unity 工作总结