Unity高阶-项目优化-批处理,代码优化

在屏幕上渲染物体,引擎需要发出一个绘制调用来访问图形API(iOS系统中为OpenGL ES)。每个绘制调用需要进行大量的工作来访问图形API,从而导致了CPU方面显著的性能开销。

Unity在运行时可以将一些物体进行合并,从而用一个绘制调用来渲染他们。这一操作,我们称之为“批处理”。一般来说,Unity批处理的物体越多,你就会得到越好的渲染性能。

Unity中内建的批处理机制所达到的效果要明显强于使用几何建模工具(或使用Standard Assets包中的CombineChildren脚本)的批处理效果。这是因为,Unity引擎的批处理操作是在物体的可视裁剪操作之后进行的。Unity先对每个物体进行裁剪,然后再进行批处理,这样可以使渲染的几何总量在批处理前后保持不变。但是,使用几何建模工具来拼合物体,会妨碍引擎对其进行有效的裁剪操作,从而导致引擎需要渲染更多的几何面片。

材质

只有拥有相同材质的物体才可以进行批处理。因此,如果你想要得到良好的批处理效果,你需要在程序中尽可能地复用材质和物体。

如果你的两个材质仅仅是纹理不同,那么你可以通过 纹理拼合 操作来将这两张纹理拼合成一张大的纹理。一旦纹理拼合在一起,你就可以使用这个单一材质来替代之前的两个材质了。

如果你需要通过脚本来访问复用材质属性,那么值得注意的是改变Renderer.material将会造成一份材质的拷贝。因此,你应该使用Renderer.sharedMaterial来保证材质的共享状态。

动态批处理

如果动态物体共用着相同的材质,那么Unity会自动对这些物体进行批处理。

动态批处理操作是自动完成的,并不需要你进行额外的操作。

提醒:

1、 批处理动态物体需要在每个顶点上进行一定的开销,所以动态批处理仅支持小于900顶点的网格物体。

2、 如果你的着色器使用顶点位置,法线和UV值三种属性,那么你只能批处理300顶点以下的物体;如果你的着色器需要使用顶点位置,法线,UV0,UV1和切向量,那你只能批处理180顶点以下的物体。

3、请注意:属性数量的限制可能会在将来进行改变。

4、 不要使用缩放尺度(scale)。分别拥有缩放尺度(1,1,1)和(2,2,2)的两个物体将不会进行批处理。

5、 统一缩放尺度的物体不会与非统一缩放尺度的物体进行批处理。

使用缩放尺度(1,1,1)和 (1,2,1)的两个物体将不会进行批处理,但是使用缩放尺度(1,2,1)和(1,3,1)的两个物体将可以进行批处理。

6、 使用不同材质的实例化物体(instance)将会导致批处理失败

7、拥有lightmap的物体含有额外(隐藏)的材质属性,比如:lightmap的偏移和缩放系数等。所以,拥有lightmap的物体将不会进行批处理(除非他们指向lightmap的同一部分)。

8、 多通道的shader会妨碍批处理操作。比如,几乎unity中所有的着色器在前向渲染中都支持多个光源,并为它们有效地开辟多个通道。

9、预设体的实例会自动地使用相同的网格模型和材质。

静态批处理

相对而言,静态批处理操作允许引擎对任意大小的几何物体进行批处理操作来降低绘制调用(只要这些物体不移动,并且拥有相同的材质)。因此,静态批处理比动态批处理更加有效,你应该尽量低使用它,因为它需要更少的CPU开销。

为了更好地使用静态批处理,你需要明确指出哪些物体是静止的,并且在游戏中永远不会移动、旋转和缩放。想完成这一步,你只需要在检测器(Inspector)中将Static复选框打勾即可,

使用静态批处理操作需要额外的内存开销来储存合并后的几何数据。在静态批处理之前,如果一些物体共用了同样的几何数据,那么引擎会在编辑以及运行状态对每个物体创建一个几何数据的备份。这并不总是一个好的想法,因为有时候,你将不得不牺牲一点渲染性能来防止一些物体的静态批处理,从而保持较少的内存开销。比如,将浓密森里中树设为Static,会导致严重的内存开销。

重构

一:重复的代码

    在程序中出现两次以上的程序结构,应该进行重构:

    1:在一个函数中出现重复的结构(如:多个if语句),就要考虑优化算法,使用更简洁、高效的写法。

    2:同一个类中出现两次以上相同结构的代码,则提取出来作为一个函数。

    3:两个互为兄弟的子类之间含相同代码,则先提取出来作为独立函数,然后上推到父类中。

    4:两个互为兄弟的子类之间含相似代码,则先把相同部分提取作为函数1,不同部分作为函数2,然后在函数3中调用函数1、函数2,并把相同签名的函数上推到父类。【这样,由于子类各自重写了函数1、2,那么子类的函数3产生的结果就不同。此法名为“塑造模版函数”】

    5:两个毫不相关的类中重复代码,则把重复代码提取到一个新的独立类中,然后在原来的类中通过新类进行调用。(例如:提取到工具类中)

    二:过长函数

    函数是功能的基本单元,一个函数一个尽量承担一个职责。如果一个函数中,做了多步工作,则应该进行重构:

    1:我们在编写函数时,如果需要用注释来说明某一块代码时,则应该优先考虑把这部分代码作为一个函数来定义,并且通过函数名来说明其用途;在重构长函数时,这也是特征之一 —— “函数中哪里需要用注释说明其用途,则尝试提取出来作为独立函数,用函数名表达其用途”

    2:对已有长函数进行分解:以单一功能为指标,提取每一部分代码进独立函数,最后原函数只需通过一系列调用语句,引用被提取出去的函数即可。

    3:长函数中的临时变量:在原函数中,如有使用一些临时变量来接收某个函数调用结果的,则把这些临时变量直接用函数调用语句代替。

    4:过长函数参数列:过长函数参数列表是函数调用出错的主要原因,可以新建一个参数类,把参数作为类成员,而调用时只需传递一个参数类对象即可。

    5:有太多临时变量和参数不能替代或提取:使用函数对象法:新建一个新的类,在其中通过一个成员变量,引用原来的类;把原函数中用到的临时变量、参数,全部作为类成员字段;定义一个函数,通过使用成员字段,实现与原函数一样的功能;最后,将原函数改造为:新建函数功能类对象(this,原临时变量,参数 作为构造参数),调用功能函数,并把结果返回。

    6:条件表达式改造:用于if语句的判断表达式往往是造成代码可读性下降的原因之一,某些判断语句用到的数据需要通读上下文才能理解。可以将条件语句提取出来,作为一个独立函数,通过函数名表达其判断内容,而函数內根据判断语句返回true 或 false 即可。

    7:循环语句改造:循环语句块同样可以承担单一职责,因此可以提取出来作为独立函数,函数名表达其用途。

    三:过大的类

    类是面向对象而设计的,如果一个类中包含了太多与其本身无关的功能时,就要考虑重构:

    1:将与本类无关的变量、函数,提取出来作为一个新类;

    2:如果提取出来的变量、函数,适合作为一个子类,则使用提取子类法;


    四:过长参数列表

    1:如果参数是某个函数的调用结果,则直接使用函数调用语句作为参数;

    2:如果某几个参数是属于某一个类的字段,则使用该类的对象作为参数,以保持对象的完整性;

    3:剩下的杂乱无章、缺乏归属对象的参数,则为它们制造一个类,用以容纳这些参数,以参数类对象作为函数参数。

    五:发散式变化【一个类受多种变化影响】

    类的设计要有可扩展性,并且修改要容易进行。如果一个类需要引入不同变化时,对于每种变化,需要修改多个地方,则需要进行重构:

    以变化为基本单元,对于某一种变化,所引起的修改,提取到一个新的类中,使得每种变化都分别对应于一个类而进行。

    六:霰弹式变化【一个变化,影响多个类】

    如果有一种变化,需要在多个不同类中进行修改,则需要进行重构:

    根据这个变化所引起的修改,把它们全部提取到一个类中。

    七:放错位置

    如果一个类中,有函数对另一个类的内容调用颇多,则需要进行重构:

    1:如果是一个函数过多调用另一个类的数据,则把该函数“搬移”到被那个类去;

    2:如果是一个函数中部分代码过多调用另一个类中数据,则先把该部分代码提炼为独立函数,然后再搬移。

    八:零散数据

    如果有一些数据,常常在一起出现,例如:作为函数参数经常出现,则需要进行重构:

    把这些零散的数据提炼到一个新的类中,以对象为单位来组织、使用这些数据。

    九:替换基本数据类型

    对于某些小规模、少字段的信息,虽然可以用几个基本数据类型来表达出来,但是这些零散数据一旦分开使用,就让人摸不清用途。所以需要重构:

    1:用小对象组合零散数据:例如:带有数值与货币种类的money类、由start和end字段组成的range类等,类名清晰易懂。

    2:不影响类行为,只用于表示某内容的类型码替换:

    3:影响类行为的类型码替换:


    十:switch语句块重构

    将switch语句提炼到独立函数,尽量用多态来取代case判断语句的基本数据类型,最后把该函数上推到父类中去。

    十一:平行继承

    如果有两个继承体系,体系一的子类用途与体系二的子类用途相似,则需要重构:

    在体系一中引用体系二子类实例,将原子类中的函数改写为调用体系二子类实例中函数。

    十二:冗余类

    如果有一些子类、独立类,没有承担明确的用途,那么就需要重构:

    1:如果是没明确职责的子类,则折叠继承体系——把子类内容合并到父类去,取消子类。

    2:如果是没明确职责的独立类,则将其内容合并到最频繁调用它的类中去。

    十三:过长的调用链

    如果存在一个类调用类2,类2调用类3...造成一长串调用关系,则需要重构:

    1:隐藏委托关系:把 类1对象.类2对象字段.getXX() 形式的代码,在类1中进行封装,定义  get类2XX()  函数,在函数中通过类2对象.getXX()调用,并把结果返回。


    十四:去掉中间人

    1:  过度委托(一类中超过一半方法需要靠委托类来调用其他类方法):则隐藏中间人,去掉委托类,让调用者直接与负责的对象打交道。

    2:如果只有少数函数需要委托类来调用其他类方法:则把这些函数放进调用端,直接用调用端.XX()调用即可。

    3:如果委托类还有其他行为,则使用“继承取代委托”,继承实际负责类作为子类,从而扩展原对象的行为,又可以调用原对象的方法。

    十五:访问私有

    如果两个类之间彼此过多访问private内容,则需要重构:

    1:把经常需要互相调用的内容提取到一个新的类中,在新类中光明正大地直接调用;

    2:子类可以独立成类:则用委托取代继承。将子类作为一个独立的类来定义,在其中使用一个原父类对象进行内容调用。

    十六:异曲同工的内容

    如果有功能相同的函数、代码,则将它们进行统一。

    十七:为原有类库添加函数

    如果需要在原有的类库基础上添加新函数,可以使用继承原类库的功能类,在子类中添加新函数,在程序中使用自定义的子类即可。

    十八:类中的字段封装

    类中的字段应该保持私密性:

    1:普通字段封装:将public改为private,并定义public修饰的setter/getter函数。

    2:集合字段封装:对于集合类型的字段,定义public修饰的remove/add函数,在函数中通过集合字段本身调用remove\add操作。

    3:只读字段:对于一些定义了之后就不再修改的字段,我们应该在类的构造函数中进行赋值,然后只提供getter函数,隐藏setter函数。


    十九:拒绝继承

    如果一个子类只需要父类中的少数内容,那就应该用委托取代继承,避免在子类中无谓地实现父类的接口。


    二十:注释过多

    注释可以增强代码可读性,但是注释也为我们指明了重构的方向。

    1:需要用注释来说明一个代码块的用途时,尝试将其提取为独立函数,用函数名来表达用途;

    2:需要注释来说明某种条件、状态时,使用断言。

优化

尽量不要频繁的instantiate和destroy object,使用object pool

少使用临时变量,特别是在Update OnGUI等实时调用的函数中。

如果没有必要每帧都处理,则可以每隔几帧处理一次

Ref可以多使用,传参会多一次拷贝

Queue或者Stack来代替List

可以使用for就不使用foreach,foreach产生的迭代器可以产生GC以影响性能。

避免频繁地Find、GetComponent。

使用内建的常量,例如Vector3.zero等等

使用gameObject.CompareTag(“XXX”)而非gameObject.tag

使用消耗更小的运算:例如1/5使用1*0.2来代替

Distance和magnitude

GetComponent不要在Update使用。

GameObject.Find()不要在Update

背景音乐选择mp3压缩格式,尽量不超过100s,尽量不要提前拖拽到场景内

你可能感兴趣的:(Unity高阶-项目优化-批处理,代码优化)