项目优化策略
遮挡剔除(Occlusion  Culling)
当场景中包含大量的模型时,势必会造成渲染效率的降低,如果使用遮挡剔除(Occlusion Culling)技术,可以使得那些被阻挡的物体不被


渲染,从而达到提高渲染效率的目的。
在默认的渲染管线中也会根据摄像机的视见体的范围对场景模型进行剔除操作,在视见体以外的物体不被渲染,但是在视见体中的物体会以


离摄像机最远的物体开始渲染,接着逐渐渲染靠近摄像机的物体,后渲染的物体会覆盖先前渲染的物体,该种技术一般被称为视锥体剔除


(FrustumCulling)。锥体剔除只剔除摄像机视角范围外的物体而对于被包含在视见体中而由被其他物体遮挡的物体还是会进行渲染。而遮


挡剔除(Occlusion Culling)技术是同时剔除一个在视见体内但是被其他物体所遮挡住的物体,此时该物体也不会被渲染。


使用遮挡剔除时,需要进行手动设置。该技术的基本原理是在场景空间中创建一个遮挡区域,该遮挡区域由单元格(Cell)组成,每个单元


格构成了整个场景的遮挡区域的一部分,这些单元格会把整个场景拆分成多个部分。
当摄像机能够看到该单元格时,表示该单元格中的物体会被渲染出来,而被其他单元格挡住的不被摄像机看到的单元格中的物体将不会被渲


染。


层级细节(LOD)
层级细节(LOD)全称为Level  of  Detail。它是根据物体在游戏画面中所占的像素多少(所占游戏视图的百分比)来调用不同复杂度的模


型,简单地理解就是同一个物体离摄像机比较远时使用复杂度低的模型,当物体离摄像机比较近时使用复杂度高的模型。这也是一种优化游


戏渲染效率的方法。
在第三方建模软件中制作好各个层级(不同复杂程度)的模型。并按照复杂程度自高向低地为模型命名为“模型名字_LOD0”、“模


型名字_LOD1”等等,最后的数字序号越低,表示复杂程度越高,这样的命名规则使得Unity3D能够自动为模型添加LOD组(LODGroup)。在导


出为FBX之前,需要这两个模型的位置重合,其他的导出设置,把包含了两个层级的FBX模型文件导入到Unity3D之后,Unity3D会与普通模型


导出一致。根据模型的命名方式自动添加LODGroup。
把该模型拖到场景中,选择该模型,此时在Scene窗口中会显示该模型目前所处的级别。选择该模型,在Inspector窗口中可以看到


该模型自动添加了LODGroup组件,该组件便是用于控制模型LOD的组件。拖动场景中的摄像机或者在该组件面板中拖动摄像机图标,可以看到


该模型的级别变化。


渲染途径  (Rendering  Path)
 Unity  提供不同的渲染途径(Rendering  Path)。这些渲染途径用于决定灯光和阴影在场景中的计算方法,不同的渲染途径具有不同的性


能特性和渲染效果。
在Unity3D中,提供了三种渲染途径,分别是顶点光照(VertexLit)、前向渲染(Forward  Rendering)和延时光照(Deferred Lighting)



渲染途径的选择可以通过在Player  Settings中来设置,或者为场景中的不同摄像机设置不同的渲染路径,同一个场景中的不同摄像机可以


设置不同的渲染途径。


延时光照是具有最高保真度的光照和阴影的渲染途径。如果场景中有很多实时灯光,最好使用延时光照。但它需要一定水平的硬件支持,目


前移动设备上不支持该渲染途径。


使用延迟光照的主要优点是对于能影响物体的光线数量没有上限;完全采用以每像素的方式进行光线计算,这等于意味着场景中全部的灯光


将以正常的光照模式作用于物体的材质上,从而保证了灯光计算的一致性;所有光线都可以使用灯Cookie;所有的光线都能产生阴影;光照


计算的开销与屏幕的光线尺寸成正比,因此不用担心光线所照射的物品的数量;但是它也有它的缺点,比如没有实时抗锯齿支持;不能处理


半透明物体,也不能用在那些使用前向渲染的物体之上;不能取消物体的接受阴影属性(Receive  Shadow),也就是说物体将永远接受其他


物体所投射的阴影;灯光组件中的Cull  Mask属性设置也会失效。


使用延迟光照时需要注意影响它的计算性能的因素:分别是被照亮的物体在屏幕上的像素数量和投射阴影的灯光的数量。延迟光照中实时光


线的开销和光线照亮的像素的数量成正比,而不取决于场景的复杂性。
前向渲染路径(Forward  Rendering  Path)是基于着色器的渲染途径。它支持逐像素计算光照(包括法线贴图和灯光Cookies)以及来自一


个平行光的实时阴影。
  顶点光照(Vertex  Lit)  是最低保真度的光照、不支持实时阴影的渲染路径。支持顶点光照渲染途径只对所有物体渲染一次(或称为


一个通道Pass,或者一个渲染阶段),所有光源的照明都是在物体的顶点上进行计算的。这对于需要发布到比较旧的机器上或者受限制的移


动平台上是非常合适的。由于顶点光照对硬件要求不高,所以它是最快的渲染途径并且据有最广泛的硬件,但是由于所有的光照都是在顶点


层级上计算的,因此此渲染途径不支持大部分的逐像素渲染效果:如阴影、法线贴图、灯光遮罩、高精度的高光。


Unity3D事件函数调用顺序
在Unity3D的脚本当中,提供了一些按照预定顺序执行的事件函数。熟悉这些事件函数的调用时机,才能把合适的代码放在合适的事件函数中


,通过对这些函数进行重写来覆盖默认的函数功能。下图展示了这些事件函数的调用时机。


Awake函数:
也被称为唤醒函数,当一个脚本实例被载入的时候调用,它的调用时机先于Start函数。该方法一般用于游戏开始之前初始化引用或设置游戏


状态,与对象构造函数功能相似。函数在整个脚本的生命周期内只被调用一次,而且它是在场景中所有的对象被实例化之后才被调用,因此


可以在该函数中与其他游戏对象进行对话或者使用GameObject中寻找场景对象相关函数如FindWithTag等来寻找场景中的对象,而且一般在该


函数中进行脚本或者对象间的引用设置。场景中所有对象脚本中的Awake方法的调用顺序是随机的,也就是说没有按照某种规则先执行哪个对


象的Awake函数。这里需要注意的是,Awake函数并不是对象的构造函数,而且不能在该函数中执行协同程序(coroutine)。


Start函数:
称为开始函数,它是在第一次执行Update函数之前,Awake函数执行之后被调用。一般Awake用于初始化对象或者脚本、组件之间的引用,而


Start函数用于做数值的初始化设置。而且它同awake一样,在整个脚本生命周期中只被调用一次。在该函数中执行协同程序,用于调整程序


执行的节奏,比如等待一个音频素材的导入之后再执行下面的代码等等。


Update函数:
也被称为更新函数。该函数游戏运行时每一帧之前被调用一次,是用于更新每帧游戏逻辑数据(比如角色的位置更新)的最常用函数。该函


数的调用频率时基于游戏目前的帧速率的,所以其调用频率是由当前游戏的运行速度来决定。


LateUpdate 函数
更新函数。它在Update函数调用之后被调用,它也是每帧被调用一次。在Update()中执行的任何计算都会在LateUpdate()开始之前完成。


LateUpdate()的一个常见应用就是第三人称控制器的相机跟随。如果把角色的移动和旋转放在Update()中,那么就可以把所有相机的移动旋


转放在LateUpdate()。这是为了在相机追踪角色位置之前,确保角色已经完成移动。


FixedUpdate函数:
FixedUpdate()比Update()函数调用的更频繁,它的调用频率是基于整个游戏的固定定时器的。当帧速率较低时,它每帧可能被调用多次,如


果帧速率比较高,它有可能就不会被调用了。
所有的物理计算和更新都发生在FixedUpdate()之后。当在FixedUpdate()中计算物体移动时,不需要乘以Time.deltaTime(当然要


获得最后一次调用FixedUpdate所用的时间,也可以用Time.deltaTime。)。因为FixedUpdate()是基于可靠的定时器的,不受帧速率的影响


。这里需要注意的是,处理Rigidbody相关物理运算时,需要用FixedUpdate来代替Update。例如:给刚体加一个作用力时,必须在


FixedUpdate里应用作用力,而不是在Update中,因为物理模拟计算的频率与帧更新的调用频率不同。


OnApplicationFocus函数:
也被称为应用程序聚焦函数。现在的操作系统都是多任务多窗口运行系统,也就是说可以同时打开多个应用程序,当前被激活的应用程序也


可以成为被聚焦(激活),其他的应用程序失焦。例如现在窗口中打开了网页浏览器,Word,当你激活页面浏览器时,Word就失去焦点,当


切换到Word软件时,页面浏览器失焦,而Word软件被激活。当玩家从其他的应用程序聚焦到当前游戏时,OnApplicationFocus函数会被调用





OnApplicationPause函数:
也被称为应用程序暂停函数。当游戏暂停时在当前帧更新之后被调用。一般是当程序失焦的时候被调用。对于使用Time.scaleTime = 0的方


法来暂停游戏,该函数不会被调用。


OnApplicationQuit函数:
也称为应用程序退出函数。在应用退出之前所有的游戏对象都会调用这个函数。当游戏退出时,有可能要对游戏进行一些善后的处理,比如


可以在该函数中做数据永久化保存的工作。


游戏对象当前状态的事件函数。
在Unity3D中,游戏对象的状态可以分为实例化、初始化、激活、注销、销毁等。游戏对象的实例化由游戏对象的构造函数来完成,


一般的游戏对象其实例化的过程不用人工参与(有时只是为构造函数提供属性值而已),游戏对象实例化之后,其初始化过程由Awake函数和


Start函数来完成,而对于激活、注销和销毁等状态,则会分别调用一下四个函数:
OnEnable函数:也被称为激活函数。当游戏对象从注销状态转到激活状态时被调用。
OnDisable函数:也被称为注销函数。当游戏对象从激活状态转到注销状态时被调用。
OnDestroy函数:也被称为对象销毁函数,当场景中的对象被销毁时而且所有帧更新之后被调用(也就是在对象存在的最后一帧)。


该函数一般用于相应Destroy函数或者场景关闭时。使用该函数,可以对对象销毁前做善后工作。OnDestroy不能用于协同


程序。
Reset函数,也被称为重置函数。Reset是在用户点Inspector面板的Reset按钮或者首次添加该组件时被调用。此函数只在编辑模式


下被调用。Reset最常用于在检视面板中给定一个最常用的默认值。


统计数据分析器 (Profiler)
游戏的运行是一个实时处理的过程,因此在开发游戏的过程中,需要时时刻刻对游戏的运行性能进行监视和优化,这样才能够在游


戏效果与游戏运行效率之前取得平衡。
Unity3D提供了一套实时监视游戏运行效率的分析器,通过该分析器可以获得游戏当前运行时所占用的各种资源的百分比以及这些资


源的运行效率,进而进行有针对性的性能优化。而且利用该分析器还能够测试在目标真机上的性能。


对游戏运行的性能分析可以通过两种方式:
1.在Game窗口的左上角点击【State】统计按钮,此时会在该窗口中显示目前游戏的渲染效率情况(渲染数据统计窗口)。
2.是深度分析器(Profiler)窗口,该窗口通过主菜单的【Window】->【Profile】来打开。


Graphics:TimeperframeandFPS
处理和渲染一个游戏画面的需要多少时间以及帧速率,每秒渲染帧数(FPS)。这个值只包括游戏视图更新和渲染的帧不包括在编辑器中绘制


场景视图,检视窗口和其他仅编辑器进程的时间。
DrawCalls:
批处理后网格绘制调用的总数。该值越大,说明场景中的单独模型越多,每次网格调用都会消耗一定的计算机资源。
Savedbybatching
每个绘制调用被添加到批处理的数量。批处理是引擎试图结合多个物体渲染进行一次描绘调用的过程,以此降低CPU开销。为了确保良好的批


处理,应该尽可能多的在不同物体之间共享材质。
TrisandVerts:绘制的三角形和顶点的数目。数目越多,渲染需要处理的时间越长。
UsedTextures:绘制该帧使用的纹理数和它们使用的内存。
Rendertexutresandswitches:渲染出来的贴图以及每帧切换激活纹理的次数。
Screen:屏幕的大小,连同其抗锯齿级别和内存的使用率。
VRAMusage:当前显存(VRAM)的使用情况的大约范围,同时显示显卡有多少显存。
VBOTotal
顶点缓冲区对象总数。不同的模型需要创建新的VBO。在某些情况下缩放对象需要创建额外的VBO。在静态批处理的情况下,多个模型对象可


以共享相同的VBO。
ShadowCasters:投射阴影的数量。
VisibleSkinnedMeshesandAnimations:渲染蒙皮网格的数量和正在播放的动画的数量。
Network:网络连接数量。


分析器提供了游戏运行性能的详细分析,通过该窗口可以查看每帧的CPU、GPU、渲染、内存、音频、物理模拟、脚本调用等资源的占用率,


而且可以找到哪些游戏素材在某一帧占用的资源百分比。这些数据将被绘制在Profiler窗口右边的时间轴上(不同功能的资源占用率以不同


颜色表示),在时间轴上可以使用鼠标左键定位某一帧的分析数据,更细致的数据显示在该窗口的下方。


CPU使用率区域。CPU使用率区域显示游戏每一帧在CPU上的资源占用率。。CPU使用率分别显示了渲染(Rendering)、脚本(Scripts)、物


理运算(Physics)、垃圾回收器(GarbageCollector)、同步处理(VSync)和其他杂(Other)


GPU使用率区域。它与CPU使用率区域相似,只是它描述的是GPU当前的使用情况。GPU使用率面板分别显示了不透明处理(Opaque)、透明处


理(Transparent)、阴影/深度处理(Shadows/Depth)、延迟预处理(Deferred  PrePass)、延迟光照处理(Deferred  Lighting)、后


期处理(PostPrecess)和其他杂项(Other)目前的情况。


渲染区域。该区域显示渲染的统计数据。该区域显示了当前帧的绘制调用(Draw  Calls)、三角形数量(Triangles)和顶点数量


(Vertices)。下方的窗口与Game窗口中的RenderingStatics  窗口一样。


内存区域。内存区域显示了当前的内存使用情况。内存区域显示了已分配的内存数(Total  Allocated)、材质内存使用(Texture  Memory


)、多边形数(Mesh  Count)、材质数(Material  Count)和对象数(Object  Count)的数据。


好的性能,是很多游戏成功的关键。接下来介绍Unity3D官方建议的优化方法。


方法一: 降低绘制调用(Draw Call)数量。
随着图形卡性能的提高,处理大量多边形数据的能力也在增长。比如现
在一个具有100个三角面的物体和渲染一个具有1500个三角面的物体所需
要的渲染开销是差不多的。当处于摄像机视见体内,而且添加了网格渲染
器(Mesh Renderer)组件的对象才会产生渲染的开销,而空的游戏对象
并不会产生渲染开销。但当模型是分开独立时,从程序把一个模型的数据
经过CPU传输到GPU并命令GPU进行绘制时(称为一个Draw Call,也就
是绘制调用过程),会产生大量的CPU开销。


Unity3D在运行时可以将一些物体对象进行合并,也就是把多个物体打包再使用一个Draw Call来渲染他们,这一操作被称为“批处


理”(Batching)。一般来说,Unity3D批处理的物体越多,便会得到越好的渲染性能。Unity3D中内建的批处理机制所达到的效果要明显强


于使用第三方建模软件使用共享贴图减少Draw Call。
Unity3D判断对哪些物体对象进行批处理,一般是根据这些物体是否具有共同的材质和贴图,也就是说,拥有相同材质的物体才可以进行批处


理。因此,如果想要得到更好的批处理效果,需要在场景中尽可能地复用材质到不同的物体上。只合并两个物体而没有共享材质,也是不会


启用批处理操作。如果想有效地合并,你需要确保网格结合后,只使用一种材质。


如果需要通过脚本来访问复用材质属性,那么如果使用Renderer.material来改变贴图将会造成一份材质的拷贝。因此,一般应该使用


Renderer.sharedMaterial 来保证材质的共享状态。


动态批处理:Unity3D内置的批处理对共享贴图的动态物体也同样有效。只是需要注意的地方有:由于批处理动态物体需要在每个顶点上进行


一定的开销,所以动态批处理仅支持小于900顶点的网格物体。


不要使用缩放。分别拥有缩放大小(1,1,1) 和(2,2,2)的两个物体将不会进行批处理。统一缩放的物体不会与非统一缩放的物体进行批处理。


使用不同材质的物体将会不能被批处理。


多通道的shader会妨碍批处理操作。比如,几乎unity中所有的着色器在前向渲染(Forward Rendering)中都支持多个光源,因此为它们开


辟多个通道,所以对批处理有影响。


使用预设生成的对象会自动地使用相同的网格模型和材质,因此会被批处理。


静态批处理。相对而言,静态批处理操作允许引擎对任意大小的几何物体进行批处理操作来降低绘制调用(只要这些物体不移动,并且拥有


相同的材质)。因此,静态批处理比动态批处理更加有效,应该尽量地使用它,因为它需要更少的CPU开销。


为了更好地使用静态批处理,需要明确指出哪些物体是静止的,并且在游戏中永远不会移动、旋转和缩放。可以通过在Inspector窗口中将


Static复选框勾选即可。


方法二:在前向渲染途径(Forward Rendering Path)模式下减少像素灯的数量。
根据影响物体的光源的不同,前向渲染途径用单个或多个通道来渲染物体。在前向渲染中,光源本身也会根据他们的设置和强度受到不同级


别的对待


方法三:使用层消隐距离来优化场景。
在一些游戏中,可能需要将小物件剔除,以减少绘图调用的数量。例如,在足够远的距离,大型建筑物仍然可见,小石块和碎片可以隐藏掉


。要做到这一点,可以小物件放入一个单独的层(separate layer)中,并使用Camera.layerCullDistances函数来设置每一层的消隐距离。


方法四:注意阴影的数量和质量。
如果游戏的发布目标平台是台式机,那么需要注意实时阴影的设置;产生实时阴影一般开销是较大的。如果不正确使用,它们可能造成大量的


性能开销。阴影影响渲染速度的主要因素有:产生实时阴影的灯光数量、投射阴影和接收阴影的物体数量(在物体Mesh Renderer组件中设置


)、阴影是硬阴影(Hard Shadows,速度快,边缘锐利)还是软阴影(Soft Shadows,速度慢,边缘柔和)、阴影的分辨率以及阴影的柔和


度。这些属性基本可以在灯光组件属性面板中设置。如果有需要的话,把一些阴影烘焙在光照贴图上是提高渲染效率的一种方法(此时灯光


需要设置成“静态”static物体)。最后还有一个设置可以提高阴影的渲染效率,便是在“质量设置”(Quality Settings)面板中的


Shadow Distance属性上设置阴影的显示距离,该距离是根据当前摄像机作为参考的,当可以生成阴影的地方与当前摄像机之间的距离超过该


值时,将不生成阴影。


方法五:优化模型几何体。
在渲染流水线中,模型的数据量越大,需要对这些数据进行处理的时间也会越长,当然随着渲染技术的发展,处理模型数据的数量也在提升


。但是,经量地使用优化的模型可以使得游戏运行更加有效率。那么对模型的优化主要是顶点、三角形面片数目不要太多;


方法六:使用贴图压缩优化。
尺寸越小、压缩比率越高的贴图,占用的内存空间也会降低,也可以降低对它的渲染处理时间,同时也会减少游戏文件的体积。修改贴图的


尺寸以及压缩格式可以通过贴图的属性面板来设置。最后在整个场景中尽量减少贴图的数量。


方法七:  蒙皮动画模型优化。
蒙皮动画主要针对添加骨骼的模型,对这些模型的优化也对渲染效率起到不可低估的提升作用。只使用一个蒙皮网格渲染器。在Unity中每个


角色仅使用一个蒙皮网格渲染器(skinned mesh renderer)来绘制。


使用尽可能少的材质。应该尽可能地减少网格所用材质的数量。除非想使用不同的着色器来实现不同部位的材质效果(例如,角色身体和眼


睛使用不同的着色器)。但是,大多数情况下,每个角色使用2-3个材质就够了。


尽可能减少骨骼数量。中型的PC游戏每个角色一般使用15-60个骨骼。骨骼越少,性能越好。一般用30个骨骼就可以在台式机上


(Desktopplatforms)获得很好的表现效果,同时在手机平台(Mobile Platforms)上质量也相当不错。理想的情况是,在手机平台


(Mobile Platforms)上单角色的骨骼不要超过30个,在台式机( Desktop platforms)上也尽量只用30个左右的骨骼。


方法八:物理性能优化。
如果对于台式机,对于稍微复杂一些的物理模拟运算来说是绰绰有余的,但是如果是开发移动终端的游戏,那么就需要更加注意物理性能的


优化了。调整“固定时钟步调”(Fixed Timestep,该时间间隔与帧速率无关,用于控制物理计算和执行FixedUpdate事件的频率)设置


(Time manager,在主菜单中选择【Edit】->【Project Settings】->【Time】。


这里顺便介绍一下 ,在这个面板中的最后一个属性“时间缩放因子”(Time Scale),如果该值为1,表示按照正常时钟运行游戏,当该值


为0时,游戏暂停运行,如果设置成2,游戏运行时间将加快两倍,当为0.5时,运行时间减慢到一半),以减少对物理更新所花费的时间。 


增加时间步长将减少CPU开销,但物理模拟的精度会下降。通常情况下,为增加速度而降低精度是可以接受的折中方案。


设置第二个属性中的“最大允许时钟步调”(Maximum AllowedTimestep,物理计算和FixedUpdate() 执行不会超过该指定的时间)在0.1-


0.125范围之内,使得在最坏的情况下封顶物理花费的时间。


网格碰撞盒比基本碰撞盒需要更高的性能开销,因此应该尽量少使用。通过把基本碰撞盒作为物体的子对象,来尽可能地近似网格的形状。


子碰撞盒将作为单一复合碰撞盒共同控制其父级的刚体。车轮碰撞盒不是严格意义上的固体物体的碰撞,以及布料模拟,都会造成很高的CPU


开销。


物理模拟计算的开销取决于场景中非休眠刚体和碰撞盒的数量以及碰撞盒的复杂度。在游戏测试运行的时候,可以在场景中使用分析器来跟


踪当前有多少个物理物体在起作用。


方法九:优化脚本性能
减少固定的增量时间。设置fixedTimeStep值在0.04~0.067秒之间(也就是每秒15~25帧之间)。这降低了FixedUpdate被调用和物理引擎执


行碰撞检测和刚体更新的频率。如果为主角色添加了刚体,可以在刚体组件启用“插值”(interpolation)来平滑降低固定增量时间步。


减少GetComponent的调用。使用GetComponent或内置的组件访问函数将带来明显的性能开销。可以通过获取一次组件的引用并指定给变量来


避免这个操作(有时也被称为"缓存"的引用)。
 避免分配内存。除非确实需要,否则应该尽量避免实时生成新的对象,因为当它们不再使用时,为这些废弃的对象进行垃圾回收会造成一定


的开销。可以通过使用数组结构来减少垃圾收集。