2017-11-30 22:31:53 xj025 阅读数 166
  • Unity3D移动端实战经验分享

    主要是围绕资源加载效率的优化,文本文件加载,比如xml序列化读取,protobuf文件序列化,以及消息事件封装及应用,shader的优化及运用,移动端实时阴影的绘制。

    22491 人正在学习 去看看 姜雪伟

参考资料:ShadowGun

https://blogs.unity3d.com/cn/2012/03/23/shadowgun-optimizing-for-mobile-sample-level/

具体有点内容:

1,布料,使用mesh网格顶点计算实现

2017-04-03 14:49:17 BoBoWang1991 阅读数 2986
  • Unity3D移动端实战经验分享

    主要是围绕资源加载效率的优化,文本文件加载,比如xml序列化读取,protobuf文件序列化,以及消息事件封装及应用,shader的优化及运用,移动端实时阴影的绘制。

    22491 人正在学习 去看看 姜雪伟

浅谈Unity3D项目优化
作为一个入行不足三年的攻城狮来讲,讲引擎中的优化,经验确实不足,unity3D引擎作为一款侧重移动端游戏开发引擎来讲,优化游戏是确实有必要的,毕竟他要适配所有机型的前提下又要保证游戏画面的清晰度,特效的绚丽多彩,所以国内大部分游戏公司都要喜欢招收既会码代码又会做优化的复合人才。
废话不多说,直接上方案。
性能优化
1.CPU优化
CPU优化

1.DrawCalls
新版Unity也可以叫DrawCall,定义:Unity每次在准备数据并通知GPU渲染的过程称为一次DrawCall,显示在工程中就是Batches的大小,Batches越大,画面越是卡顿,不流畅。Batches即是DrawCall的大小
通常来说,如果是在pc端发布的游戏,DrawCall应该控制在2000以内,如果是要发布到移动端,那么DrawCall最大不能超过300,越低越好,当然在保证画面质量的前提下。那么如何降低DrawCall呢,我在这里说两种最常用的方法,第一种:静态批处理Batching Static,批处理相同材静态独享或者相同纹理的静态对象,直接上图。
把所有静态对象全部设置为BatchingStatic
2.动态批处理
即是批处理非静态对象,但是要求苛刻,稍作修改就会批处理不成功,我建议大家使用静态批处理
动态批处理
物理组件
1.设置一个合适的FixedTimestep.
FixedTimestep
Editor-ProjectSetting-Time,打开可以根据需求动态设置。
2.不要使用MeshCollder,从优化角度来看,尽量减少物理组件的使用。
MeshCollider顾名思义,即是网格碰撞器,如果非要使用,建议大家使用Meshbaker网格合并,合并碰撞器,这一点我设计的有些少发一个链接,大家可以参考一下。http://blog.csdn.net/akof1314/article/details/41347079
3.GC(Garbage Collection垃圾回收)
处理器-调度->GC回收垃圾。
在我们做需要大量生成和销毁游戏对象的游戏时,如FPS类游戏,如果频繁实例化Instantiate和Destroy,则会大量调度GC,从而严重消耗CPU性能,在这里,我就可以使用对象池模式来减少GC的调度,何为对象池呢,对象池就是存放需要被反复调用资源的一个空间,当一个对象会大量生成的时候,如果每次都销毁创建,会非常浪费时间,通过对象池把暂时不用的对象放到一个池子中(也就是一个集合中),当下次要重新生成这个对象的时候,先去池子中查找一下是否有可用的对象,如果有就直接拿出来使用,如果没有,就重新创建,利用空间换时间来达到游戏的告诉运行,减少GC的调度。大家可以参考我对象池的链接,学习一下。
链接http://blog.csdn.net/bobowang1991/article/details/68961877
另外降低GC的方案如下,1减少New产生对象的次数,2,使用公共的对象(静态成员),3.将String换成StringBundle.都可以减少GC的调度,4尽量不用foreach,而是用for,foreach会涉及到迭代器的使用,每一次循环所产生的迭代器会带来24bytes的垃圾,循环10次,就是240bytes。5.不要直接访问gameobject的tag属性,访问物体的tag属性会在堆上额外的分配空间。6.LinQ->连接数据库。
代码质量
1.不要频繁的使用GetComponent,尤其在循环中。
2.善于使用OnBecameVisible()he1OnBecameInVisible().
3.使用内建的数组,如Vecter.Zero而不是new Vector3(0,0,0).
4.善于使用ref关键字
5.将经常需要使用的属性查询缓存起来。
6.不要频繁使用Instantiate和Destroy,使用对象池。
7.习惯性的将暂时不用的GameObject设置为非激活,即使把如图所示
GPU的优化
两种方案:1.减少绘制的数目,2.优化显存带宽。
如何减少绘制的数目
1.保持材质的数目尽可能少,这使得unity更容易进行批处理。
2.使用纹理图像,来代替一些列单独的小贴图。
3.如使用了纹理图像和共享材质,使用Render.SharedMaterial代替Render.material。
4.使用光照纹理,Lightmap,而非实时灯光,这一点尤为重要,但是操作起来不难,我在这里着重讲解。
首先,如果是一个加了大量的灯光和粒子特效,即使是一个小小的塔防游戏,DrawCall也会达到5000以上甚至更高,这是为什么呢,是因为灯光和粒子特效需要GPU实时渲染,灯光和粒子特效的数量越多,DrawCall也就越高。但是我们场景中的灯光是不需要实时渲染的,如果你场景中的灯光很多时,就只需要下面的操作就可以大大降低DrawCall,优化GPU。
首先打开unity中的Windows面板-Light-选择Lights,然后把Baking选择为Baked类型,右下角Auto勾去掉,选择Build等待烘焙完毕即可,如下图所示
下面正在烘焙
大家需要实际操作一下,当然需要实时渲染的灯光不要要烘焙,问题不大。
5.使用LOD,LOD即是远的看不清的物体的细节忽略,当靠近他时才让渲染他的细节,说白了就是物体在远处时,虽然我们的肉眼看不清,但是相机还是在渲染他的所有细节,这是不需要的,此时我们可以使用LOD,把精细的模型换成粗模,一次来减少GPU的渲染,具体操作如下图所示。
LOD Group
添加细模型
6.遮挡剔除。
1.当场景中包含大量模型时,造成渲染效率的降低(即帧速率FPS的降低),采用遮挡剔除技术,可以使得那些被阻挡的物体不被渲染提高渲染效率
2.原理:在场景空间中创建一个遮挡区域,该遮挡区域是有单元格(Cell)组成;每个单元格是构成整个场景遮挡区域的一部分,这些单元格会把整个场景拆分成多个部分,当摄像机能够看到该单元格时,表示该单元格的物体会被渲染出来,其他的不去渲染
3.遮挡剔除步骤:
(1)场景
(2)除了主角,摄像机,直线光,地面。把层级视图中的所有游戏对象标记为“遮挡静态”(Occluder Static/Occludee Static)
(3)执行菜单命令“Windows”—>“Occlusion Culling”,弹出“遮挡剔除”面板,单击下方的“Back”按钮,开始烘焙
(4)单击“遮挡剔除”(Occlusion)窗口的“Visualizatior”,运行程序
4.若场景中存在大量的小“物件”,使用“层消隐距离”来优化场景
(1):原理:较远距离将小物件剔除,减少绘图调用的数量
(2):方法:将小物件放入一个单独的层(Separate Layer)中,并使用Camera.main.layerCullDistances函数设置层的消隐距离
(3)例如:void Start(){
Float[] distances=new float[32];
//这里定义的数组下标“8”表明第8层
distances[8]=10;
Camera.main.layerCullDistances=distances;
}
当摄像机距离这些小物件超过10m的时候,就不可见了
7.使用mobile版的shader。
具体操作入下,新建一个mateiral,inspector-Shader-Mobile-选择需要的类型,适合移动端的发布,pc端也可以使用,包括天空盒也可以使用。
选择mobile版的shader类型
优化显存带宽
1.OpenGL ES 2.0使用ETC1格式压缩等等,在打包设置哪里都有这些选项,点击选择即可。
2.MipMap技术
MipMap是把图片处理成预先计算好的比较小的图集,在在屏幕尺寸变小时调用处理好的小图,以此来节约GPU渲染,但是缺点是会增加33%的图集内存,因为原图还要保存。
MipMapMipMap图面处理效果图
Unity3D内部的优化
1.Resources 资源不用的要删除,纹理,网格,音频,组件等等。
2.在CPU压力不是很大的时候,重置GameObject,组件等占用的内存。
3.在打AssetBundle的时候,可以考虑bundle的压缩等等。
以上为本人近两年工作中所用到的所有优化技术方案,希望对大家有所帮助,谢谢。

2015-06-10 13:13:19 yxriyin 阅读数 1204
  • Unity3D移动端实战经验分享

    主要是围绕资源加载效率的优化,文本文件加载,比如xml序列化读取,protobuf文件序列化,以及消息事件封装及应用,shader的优化及运用,移动端实时阴影的绘制。

    22491 人正在学习 去看看 姜雪伟

       NGUI中有一个控件,UIGrid,例如背包就会用这个实现。

       当物品很多的时候,UIGrid打开就会很卡,网上也有人提出了优化的方法,但我认为全部重写有点小题大做,只要找到卡的原因就好了。

经过分析,有两个原因:

       1.资源没有复用,这个其实非常简单,只要你自己弄一个对象池,然后不断复用grid就可以了。

       2.只能一个个添加gird,没有批量添加方法,这个也很简单,自己写一个批量添加:

       public void AddChildren (List<Transform> trans, Vector3 scale)
    {
        BetterList<Transform> list = GetChildList();
        for(int i = 0; i < trans.Count; i++)
        {
            trans[i].name = i.ToString();
            list.Add(trans[i]);
        }
        ResetPosition(list, scale);
    }


当你解决以上两个问题之后,你会发现性能大幅提升,但随之会有一个问题,这个问题倒不是UIGrid特有的,而是NGUI的老毛病。

当你在同一帧修改控件的父亲时,控制的UIPanel会变成第一个父亲的panel.这其实并不是我们想要的。因为会引起绘制的不正确。

例如gird里面,明明clip的却不显示,反而在clip外面的显示。整个显示都错乱了。你只要注意复用gird的时候,不要将物体移到其他panel下面。

否则你只能自己手动修复panel了。


这样之后,从1s左右的时间缩短到了300ms左右,非常不错。

2014-01-04 09:26:35 cjc_hoderxx123 阅读数 741
  • Unity3D移动端实战经验分享

    主要是围绕资源加载效率的优化,文本文件加载,比如xml序列化读取,protobuf文件序列化,以及消息事件封装及应用,shader的优化及运用,移动端实时阴影的绘制。

    22491 人正在学习 去看看 姜雪伟

Unity3D 移动开发代码优化

1. 尽量避免每帧处理

比如:

function Update() { DoSomeThing(); }

可改为每5帧处理一次:

function Update() { if(Time.frameCount % 5 == 0) { DoSomeThing(); } }

2. 定时重复处理用 InvokeRepeating 函数实现
比如,启动0.5秒后每隔1秒执行一次 DoSomeThing 函数:

function Start() { InvokeRepeating("DoSomeThing", 0.5, 1.0); }

3. 优化 Update, FixedUpdate, LateUpdate 等每帧处理的函数
函数里面的变量尽量在头部声明。
比如:

function Update() { var pos: Vector3 = transform.position; }

可改为

private var pos: Vector3; function Update(){ pos = transform.position; }

4. 主动回收垃圾
给某个 GameObject 绑上以下的代码:

function Update() { if(Time.frameCount % 50 == 0) { System.GC.Collect(); } }

5. 运行时尽量减少 Tris 和 Draw Calls
预览的时候,可点开 Stats,查看图形渲染的开销情况。特别注意 Tris 和 Draw Calls 这两个参数。
一般来说,要做到:
Tris 保持在 7.5k 以下
Draw Calls 保持在 20 以下

6. 压缩 Mesh
导入 3D 模型之后,在不影响显示效果的前提下,最好打开 Mesh Compression。
Off, Low, Medium, High 这几个选项,可酌情选取。

7. 避免大量使用 Unity 自带的 Sphere 等内建 Mesh
Unity 内建的 Mesh,多边形的数量比较大,如果物体不要求特别圆滑,可导入其他的简单3D模型代替。

8. 优化数学计算
比如,如果可以避免使用浮点型(float),尽量使用整形(int),尽量少用复杂的数学函数比如 Sin 和 Cos 等等

减少固定增量时间

将固定增量时间值设定在0.04-0.067区间(即,每秒15-25帧)。您可以通过Edit->Project Settings->Time来改变这个值。这样做降低了FixedUpdate函数被调用的频率以及物理引擎执行碰撞检测与刚体更新的频率。如果您使用了较低的固定增量时间,并且在主角身上使用了刚体部件,那么您可以启用插值办法来平滑刚体组件。

 

减少GetComponent的调用

使用 GetComponent或内置组件访问器会产生明显的开销。您可以通过一次获取组件的引用来避免开销,并将该引用分配给一个变量(有时称为"缓存"的引用)。例如,如果您使用如下的代码:

function Update () {

transform.Translate(0, 1, 0);

}

通过下面的更改您将获得更好的性能:

var myTransform : Transform;

function Awake () {

myTransform = transform;

}

function Update () {

myTransform.Translate(0, 1, 0);

}

 

避免分配内存

您应该避免分配新对象,除非你真的需要,因为他们不再在使用时,会增加垃圾回收系统的开销。您可以经常重复使用数组和其他对象,而不是分配新的数组或对象。这样做好处则是尽量减少垃圾的回收工作。同时,在某些可能的情况下,您也可以使用结构(struct)来代替类(class)。这是因为,结构变量主要存放在栈区而非堆区。因为栈的分配较快,并且不调用垃圾回收操作,所以当结构变量比较小时可以提升程序的运行性能。但是当结构体较大时,虽然它仍可避免分配/回收的开销,而它由于"传值"操作也会导致单独的开销,实际上它可能比等效对象类的效率还要低。

 

最小化GUI

使用GUILayout 函数可以很方便地将GUI元素进行自动布局。然而,这种自动化自然也附带着一定的处理开销。您可以通过手动的GUI功能布局来避免这种开销。此外,您也可以设置一个脚本的useGUILayout变量为 false来完全禁用GUI布局:

function Awake () {

useGUILayout = false;

}

 

使用iOS脚本调用优化功能

UnityEngine 命名空间中的函数的大多数是在 C/c + +中实现的。从Mono的脚本调用 C/C++函数也存在着一定的性能开销。您可以使用iOS脚本调用优化功能(菜单:Edit->Project Settings->Player)让每帧节省1-4毫秒。此设置的选项有:

Slow and Safe – Mono内部默认的处理异常的调用

Fast and Exceptions Unsupported –一个快速执行的Mono内部调用。不过,它并不支持异常,因此应谨慎使用。它对于不需要显式地处理异常(也不需要对异常进行处理)的应用程序来说,是一个理想的候选项。

 

优化垃圾回收

如上文所述,您应该尽量避免分配操作。但是,考虑到它们是不能完全杜绝的,所以我们提供两种方法来让您尽量减少它们在游戏运行时的使用:

如果堆比较小,则进行快速而频繁的垃圾回收

这一策略比较适合运行时间较长的游戏,其中帧率是否平滑过渡是主要的考虑因素。像这样的游戏通常会频繁地分配小块内存,但这些小块内存只是暂时地被使用。如果在iOS系统上使用该策略,那么一个典型的堆大小是大约 200 KB,这样在iPhone 3G设备上,垃圾回收操作将耗时大约 5毫秒。如果堆大小增加到1 MB时,该回收操作将耗时大约 7ms。因此,在普通帧的间隔期进行垃圾回收有时候是一个不错的选择。通常,这种做法会让回收操作执行的更加频繁(有些回收操作并不是严格必须进行的),但它们可以快速处理并且对游戏的影响很小:

if (Time.frameCount % 30 == 0)

{

System.GC.Collect();

}

但是,您应该小心地使用这种技术,并且通过检查Profiler来确保这种操作确实可以降低您游戏的垃圾回收时间。


如果堆比较大,则进行缓慢且不频繁的垃圾回收

这一策略适合于那些内存分配 (和回收)相对不频繁,并且可以在游戏停顿期间进行处理的游戏。如果堆足够大,但还没有大到被系统关掉的话,这种方法是比较适用的。但是,Mono运行时会尽可能地避免堆的自动扩大。因此,您需要通过在启动过程中预分配一些空间来手动扩展堆(ie,你实例化一个纯粹影响内存管理器分配的"无用"对象):

function Start() {

var tmp = new System.Object[1024];

// make allocations in smaller blocks to avoid them to be treated in a special way, which is designed for large blocks

for (var i : int = 0; i < 1024; i++)

tmp[i] = new byte[1024];

// release reference

tmp = null;

}

游戏中的暂停是用来对堆内存进行回收,而一个足够大的堆应该不会在游戏的暂停与暂停之间被完全占满。所以,当这种游戏暂停发生时,您可以显式请求一次垃圾回收:

System.GC.Collect();


另外,您应该谨慎地使用这一策略并时刻关注Profiler的统计结果,而不是假定它已经达到了您想要的效果。

2017-06-15 15:40:58 wetest_tencent 阅读数 440
  • Unity3D移动端实战经验分享

    主要是围绕资源加载效率的优化,文本文件加载,比如xml序列化读取,protobuf文件序列化,以及消息事件封装及应用,shader的优化及运用,移动端实时阴影的绘制。

    22491 人正在学习 去看看 姜雪伟

作者:陈星百,腾讯移动客户端开发 工程师

商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处。

原文链接:http://wetest.qq.com/lab/view/315.html

WeTest 导读

做了大概半年多VR应用了,VR由于双眼double渲染的原因,对性能的优化要求比较高,在项目的进展过程中,总结了一些关于移动平台上Unity3D的性能优化经验,供分享。


一、移动平台硬件架构

移动平台无论是Android 还是 IOS 用的都是统一内存架构,GPU和CPU共享一个物理内存,通常我们有“显存”和“内存”两种叫法,可以认为是这块物理内存的所有者不同,当这段映射到cpu,就是通常意义上的内存;当映射到gpu,就是通常意义上的显存。并且同一段物理内存同一时刻只会映射到一个device。

图片描述

即使是在同一物理内存上 ,之前的openGL ES规范中CPU和GPU之间的内存是不能共享的,vertex和texture的buffer是需要拷贝的。后面出来的vulkan 与IOS的metal 可以共享内存。

了解了移动平台的硬件架构,就知道了 1)CPU 2) 带宽 3) GPU 4) 内存 都有可能成为移动平台3D应用性能瓶颈。

二、移动平台3D应用的画面渲染过程

1、CPU通过调用绘制命令(称为一次Draw Call)来告诉GPU开始进行一个渲染过程的。一个Draw Call命令会指向本次绘制需要渲染的信息,这些信息包括:顶点数据、纹理数据、shader参数(光照模型、法线方向、光照方向等)等,简单地说就 画什么,用什么画,怎么画。
图片描述

2、GPU接收到Draw Call命令之后就会开始进行一次单元渲染,关于GPU的单元渲染的过程是这样的(简单示意图):
图片描述

1)从显存中取出拷贝的顶点数据和光照模型。

2)通过顶点处理器(Vertex Processor)对顶点数据进行一系列的变换和光照处理,包括裁剪处理。tips: 简单的想想,游戏中的各个物体的坐标都是参照游戏中的世界坐标系的,而实际显示的画面是玩家视角或者摄像机视角,这中间就有许多坐标系的转换。这些活就需要顶点处理器来做,最终我们得到了我们所需要视角的画面。

3)到这一步,画面还只是一些多边形,而实际显示在屏幕上的是一个个像素,这里就需要(光栅处理器)Rasterizer进行光栅化(Rasterization),从而将画面变成一个像素图,把所有的顶点对应到一个一个的像素位置。

4)对这些像素进行上色,通过片元处理器(Fragment Processor)中的像素着色器(Pixel Shader)按照shader光照模型,根据纹理对应位置颜色,计算元颜色,再经过深度计算、透明测试计算出每个像素的最终颜色。

5)把结果输出到图像缓存中,全部完成后拿去显示。

三、Unity3D应用性能优化之CPU

CPU的优化非常重要,CPU的表现直接决定了VR应用的帧率,应用的耗电量,发热量。我们来看看相比于普通的app,VR应用的CPU都承担了什么责任:a、业务逻辑 b、网络通信 c、I/O操作 d、drawcall e、physic逻辑 f、GC内存回收 g、垂直同步等待。

业务逻辑、网络通信、I/O操作

这一块的优化和普通的app差不多。

关于业务逻辑:有些不同的是Unity脚本中有一类update方法(Update、FixedUpdate、OnGUI等),这一类方法是在每帧刷新的时候调用的,是比较影响每帧耗时的,为了严格控制这一部分的执行时间,需要注意的以下几点:
a、尽量不要再Update函数中做复杂计算,如有需要,可以隔N帧计算一次,对于纯数学计算,可以开辟新线程来计算(Unity 为什么一般避免使用多线程, 实际上大多数游戏引擎也都是单线程的, 因为大多数游戏引擎是主循环结构, 逻辑更新和画面更新的时间点要求有确定性, 如果在逻辑更新和画面更新中引入多线程, 就需要做同步而这加大了游戏的开发难度。UnityEngine绝大多数类是不支持子线程的,所以一般只有纯数学计算才会用到子线程去计算。)

b、关闭所有在update类中执行log的打印操作(Unity中一次log打印有时长达7ms,Profiler数据)。

c、不在update类方法调用Getcomponent、SendMessage、FindWithTag这几个耗时较长的方法。

d、不在update类方法中使用临时变量。

关于网络通信、I/O操作:这些普通app的优化和注意点没有什么很大区别,有一点是,Unity工程中使用了资源动态加载,有些资源是保存在服务器端的,在有必要的时候才会通过网络load下去加载。这个资源动态加载需要注意一个问题:由于网络通信过程,CPU总是处于等待的状态,一般资源下载是多线程同时操作,为了尽快上屏显示资源(在这个工程中是一些图片和英雄的3D模型),但是资源有可能是在同一个帧周期中下载完毕的,如果直接加载的话,可能会出现Camera瞬时渲染过多三角形面,造成渲染时间(Camera.Render()函数执行时间)过长,,卡顿的现象。所以这里要注意,网络下载可以多线程多任务同时下载,但是在Unity主线程,要避免出现同时加载大型模型和大纹理的情况,最好使用队列的方式,保证一帧只渲染一个3D模型。

关于GC

为什么要把GC放在CPU这一部分?虽然GC是用来处理内存回收的,但是却增加了CPU的开销(GC一次开销可长可短,有时长达100ms)。因此对于GC的优化目标就是尽量少的触发GC。

图片描述

首先我们要知道所谓的GC是Mono运行时的机制,而非Unity3D游戏引擎的机制,所以GC也主要是针对Mono的对象来说的,而它管理的也是Mono的托管堆。 明白了这一点,你也就明白了GC不是用来处理引擎的Assets(贴图,音效,模型等等)的内存释放的,因为U3D引擎也有自己的内存堆而不是和Mono一起使用所谓的托管堆。其次我们还要清楚什么东西会被分配到托管堆上?对,就是引用类型。引用类型包括:用户自定义的类,接口,委托,数组,字符串,Object.而值类型包括:几种基本数据类型(如:int,float,bool等),结构体,枚举,空类型。所以GC的优化也就是代码的优化。

那么GC什么时候会触发呢?两种情况:
a、当我们的堆的内存不足时,会自动调用GC来回收内存。
b、手动的调用GC,用System.GC.Collect(),一般情况下,不建议手动去手动进行内存回收,因为容易出现问题。

检查整个工程代码,关于减少GC这一方面的优化经验总结大概如下:
1、字符串连接的处理。因为将两个字符串连接的过程,其实是生成一个新的字符串的过程。而之前的旧的字符串自然而然就成为了垃圾。而作为引用类型的字符串,其空间是在堆上分配的,被弃置的旧的字符串的空间会被GC当做垃圾回收,可以使用StringBuilder来解决(注意:C#没有StringBuffer,Java里才有!!String 在进行运算时(如赋值、拼接等)会产生一个新的实例,而 StringBuilder 则不会。所以在大量字符串拼接或频繁对某一字符串进行操作时最好使用 StringBuilder,不要使用 String)。

2、尽量不要使用foreach,而是使用for。foreach其实会涉及到迭代器的使用,而据传说每一次循环所产生的迭代器会带来24 Bytes的垃圾。那么循环10次就是240Bytes。

3、不要直接访问gameobject的tag属性。比如if (go.tag ==“human”)最好换成if (go.CompareTag (“human”))。因为访问物体的tag属性(每次Object.name也会分配39B的堆内存.)会在堆上额外的分配空间。如果在循环中这么处理,留下的垃圾就可想而知了。

4、不要实例化(Instantiate)和(Destroy)对象,事先建好对象池,以实现空间的重复利用。

5、在某些可能的情况下,可以使用结构(struct)来代替类(class)。这是因为,结构变量主要存放在栈区而非堆区。因为栈的分配较快,并且不调用垃圾回收操作,所以当结构变量比较小时可以提升程序的运行性能。但是当结构体较大时,虽然它仍可避免分配/回收的开销,而它由于”传值”操作也会导致单独的开销,实际上它可能比等效对象类的效率还要低。所以要注意选择。

6、场景切换时,可以主动进行垃圾回收(调用System.GC.Collect()),从而及时去除游戏中已经不必要地内存占用。

Draw Call 的优化

前面说过了,DrawCall是CPU调用底层图形接口的操作。比如有上千个物体,每一个的渲染都需要去调用一次底层接口,而每一次的调用CPU都需要做很多工作,那么CPU必然不堪重负。但是对于GPU来说,图形处理的工作量是一样的。

我们先来看看Draw Call对CPU的消耗大概是一个什么级别的量:
NVIDIA 在 GDC 曾提出,25K batchs/sec 会吃满 1GHz 的 CPU,100%的使用率。有一个公式可以和清楚得计算出在给定的CPU资源 与 帧率的情况下,最多能有多少个DrawCall。
DrawCall_Num = 25K * CPU_Frame * CPU_Percentage / Framerate。
DrawCall_Num : DrawCall数量
CPU_Frame : CPU 工作频率(GHz单位)
CPU_Percentage:CPU 分配在DrawCall这件事情上的时间率(百分比)

Framerate:希望的游戏帧率

比如说我们使用一个高通820,工作频率在2GHz上,分配10%的CPU时间给DrawCall上,并且我们VR要求60帧,那么一帧最多能有83个DrawCall(由于双camera的存在,单眼DrawCall只能保证在41个以内)。其实,google官方的建议是单眼DrawCall不多于50个。

所以对DrawCall的优化,主要就是为了尽量解放CPU在调用图形接口上的开销。所以针对drawcall我们主要的思路就是每个物体尽量减少渲染次数,多个物体最好一起渲染。那么DrawCall次数的优化有哪些方案呢?

DC Batching(DC批处理)
batch即批处理,DrawCall batching即DC的批处理,即把多次DrawCall合并成一次DrawCall的方案。

Dynamic Batching 动态批处理
Unity引擎对于使用相同材质的物体会自动进行批处理,相同材质意味着shader完全一样,这一部分主要是要注意那些破坏这一特性的人为因素,比如说:
1、批处理动态物体需要在每个顶点上进行一定的开销,所以动态批处理仅支持小于900顶点的网格物体,如果你的着色器使用顶点位置,法线和UV值三种属性,那么你只能批处理300顶点以下的物体(如果在这基础上还使用了UV2,则只能批处理180顶点以下的物体);请注意:属性数量的限制可能会在将来进行改变。

2、使用不同的缩放比例的物体,unity将无法对这些物体进行批处理。比如(1,1,1)和(1,2,2)就不会动态批处理,但是(1,1,1)和(2,2,2)会动态批处理。

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

4、多通道的shader会中断批处理操作(为了达到特殊的渲染目的,可能某个物体要多遍渲染.这是就要多个通道)。

5、在脚本中动态地指定了物体的材质,也不会进行批处理。

Static Batching 静态批处理
动态批处理虽然是自动的,但是限制非常多,不小心就会打破批处理,所以unity在专业版中还提供了静态批处理,静态批处理要求是想批处理的物体一定是static的,静态的,不会改变位置和旋转角度以及缩放的,且必须材质一致。其原理是把物体的网格进行合并,变成一个静态的更大的网格物体,再使用一个统一的材质进行渲染。

知道了它的原理,它的某些坑就比较清晰了:
1、在一个平行光、环境光下,没有问题,但是如果你使用了多个平行光,点光源,聚光灯这种复杂的光源去照射物体,那么静态批处理就会被打断。(项目中就遇到过,因为两边有两排英雄模型,所以场景中使用了两个不同平行光,场景中勾选的static物体并没有被合并drawcall,经过一番折磨才找到原因)。

2、如果静态批处理前有一些物体共享了相同的网格,那么每一个物体都会有一个该网格的复制品(本来unity只会保留一份,但是静态批处理会生成新的一个大网格,所以会保留所有物体的网格,最后合并),即一个网格会变成多个网格被发送给GPU。这样会造成内存的使用变大,需要注意这个问题,但是一般场景中使用相同网格的物体会比较少。

3、对于那些shader相同,纹理不同导致的不同材质无法进行批处理的物体(比如项目中的场景环境,基座,地面,其实都使用了unity自带的standard shader)可以通过纹理合并的方法来使得它们可以被静态批处理。这就引发了下面的事情:
BUS总线带宽
CPU完成一次DrawCall,除了需要发一个DrawCall的命令之外,还需要把内存中顶点数据、纹理贴图、shader参数通过bus总线拷贝到内存分配给GPU的显存之中,注意这是拷贝,不是指针传递,速度不快。如果一次drawcall传递的数据过大,带宽成为了瓶颈,那就会大大影响效率(其它的DrawCall无法出发,GPU又处于闲置)。这种情况最有可能出现在为了减少DrawCall,疯狂的合并纹理上。在项目中,UI的DrawCall调用占了很大一部分,也会最难优化的,为了减少drawcall ,我们把UI模块的静态部分(一些UI的底板,背景等不会发生变化的)全部合并成了一个纹理,最后导致了DrawCall下降了,但是帧率却也下降了,内存使用也增加了,原因就是这个。在项目中,不会同时出现的元素不要打包到一起,保证单张合并纹理不大于1024*1024一般就不会有问题了(王者荣耀最大纹理限制在了256*256)。

DrawCall的优化大概就是这些,优化的目标其实是往一个目标上靠,cpu的DrawCall命令刚刚好能被GPU消化,不要让CPU等待(带宽限制),也不要让GPU闲置。如果即使做到了这个,应用帧率还是上不去,那么就只能去削减场景,做有损优化了。

Physics

Unity内置NVIDIA PhysX物理引擎,来模拟物理世界的一些效果,比如说重力、阻力、弹性、碰撞这些,其中使用了一些内置的组件来实现这些模拟,用的比较多的如:刚体(Rigidbody) 各种碰撞器(Collider) 恒力 (Constant Force) 物理材质(Physic Material)铰链关节(Hinge Joint)弹簧关节(Spring Joint)。

unity除了提供了一些重要的组件之外,在unity脚本中的生命周期中提供了一个专门为物理计算的刷新方法:
FixedUpdate()。FixedUpdate跟Update的区别在于,这两个函数处于不同的“帧循环”中,FixedUpdate处于Physics循环中,而Update不是。所以这两个函数的使用也有了不同。Update的执行受场景GameObject的渲染的影响,三角形的数量越多,渲染所需要的时间也就越长。FixedUpate的执行则不受这些影响。所以,Update每个渲染帧之间的间隔是不相等的,而Fixedupdate在每个渲染帧之间的时间间隔是相等的。由于关系到物理模拟,所以一般涉及到物理组件,都需要放在Fixedupdate中进行计算。那么关于physics,一般的优化手段都有哪些呢?下面是一些经验及总结:

1、将物理模拟时间步间隔设置到合适的大小。 Fixed Timestep是和物理计算有关的,所以若计算的频率太高,自然会影响到CPU的开销。同时,若计算频率达不到软件设计时的要求,有会影响到功能的实现,所以如何抉择需要具体分析,选择一个合适的值,一般大于16ms,小于30ms。可以通过Edit->Project Settings->Time来改变这个值。

2、谨慎使用网格碰撞器(Mesh Collider),过于消耗性能,一般使用更简单的碰撞器,或者使用基本几何碰撞器合并的组合碰撞器。在这个项目中,把所有的网格碰撞体都抛弃了,都换成了box collider。

3、真实的物理(刚体)很消耗,不要轻易使用,尽量使用自己的代码(数学计算)模仿假的物理。

4、最小化碰撞检测请求(例如ray casts和sphere checks),尽量从每次检查中获得更多信息。
项目中涉及到物体的组件很少,关于physic的优化肯定还有很多可以说的,需要再去学习了。

VSync

简单地说,这是CPU优化的最直接的一个方法。

科普:VSync垂直同步又称场同步(Vertical Hold),垂直同步信号决定了CRT从屏幕顶部画到底部,再返回原始位置的时间。从CRT显示器的显示原理来看,单个像素组成了水平扫描线,水平扫描线在垂直方向的堆积形成了完整的画面。显示器的刷新率受显卡DAC控制,显卡DAC完成一帧的扫描后就会产生一个垂直同步信号(决定于屏幕的刷新率)。我们平时所说的打开垂直同步指的是将该信号送入显卡3D图形处理部分,从而让显卡在生成3D图形时受垂直同步信号的制约(注意是制约)。

如果我们选择等待垂直同步信号(也就是我们平时所说的垂直同步打开),那么在游戏中或许强劲的显卡迅速的绘制完一屏的图像,但是没有垂直同步信号的到达,显卡无法绘制下一屏,只有等垂直同步的信号到达,才可以绘制。这样FPS自然要受到操作系统刷新率运行值的制约。而如果我们选择不等待垂直同步信号(也就是我们平时所说的关闭垂直同步),那么游戏中作完一屏画面,显卡和显示器无需等待垂直同步信号就可以开始下一屏图像的绘制,自然可以完全发挥显卡的实力。但是不要忘记,正是因为垂直同步的存在,才能使得游戏进程和显示器刷新率同步,使得画面更加平滑和稳定。

取消了垂直同步信号,固然可以换来更快的帧率,但是在图像的连续性上势必打折扣。

四、Unity3D应用性能优化之GPU

一般人说DC的优化占了unity3D软件优化的三分天下,那么GPU的优化也占了三分天下。在了解GPU优化都有哪些着手点之前,我们先了解一下GPU在3D软件渲染中做了啥事:
图片描述

顶点着色器

GPU接收顶点数据作为输入传递给顶点着色器。顶点着色器的处理单元是顶点,输入进来的每个顶点都会调用一次顶点着色器。(顶点着色器本身不可以创建或销毁任何顶点,并无法得到顶点与顶点之间的关系)。顶点着色器是完全可编程的,它主要完成的工作有:坐标变换和逐顶点光照。 坐标变换:就是对顶点的坐标进行某种变换—把顶点坐标从模型空间转换到齐次裁剪空间。顶点的多少直接决定了三角形面的多少,也直接决定了GPU的渲染流水线的工作量,所以减少顶点数是一个比较重要的优化点。那么减少顶点怎么操作呢,又有哪些途径?
1、优化基本几何体
3D软件都是从模型制作开始,在设计师建模的时候就要想到应该尽可能地减少顶点数,一些对于模型没有影响、或是肉眼非常难察觉到区别的顶点都要尽可能去掉。比如在项目中,对于用户背后的环境模型,一些树木和石头,视频背面永远无法看见的神庙,能削减的都已经削减了。

2、使用LOD(Level of detail)技术
LOD技术有点类似于Mipmap技术,不同的是,LOD是对模型建立了一个模型金字塔,根据摄像机距离对象的远近,选择使用不同精度的模型。它的好处是可以在适当的时候大量减少需要绘制的顶点数目。它的缺点同样是需要占用更多的内存,而且如果没有调整好距离的话,可能会造成模拟的突变。

3、使用遮挡剔除(Occlusion culling)技术
遮挡剔除是用来消除躲在其他物件后面看不到的物件,这代表资源不会浪费在计算那些看不到的顶点上,进而提升性能。刚才神庙后面的剔除就属于手动的遮挡剔除。

遮挡剔除是一个PRO版才有的功能, 当一个物体被其他物体遮挡住而不在摄像机的可视范围内时不对其进行渲染。遮挡剔除在3D图形计算中并不是自动进行的。因为在绝大多数情况下离 camera 最远的物体首先被渲染,靠近摄像机的物体后渲染并覆盖先前渲染的物体(这被称为重复渲染”overdraw”). 遮挡剔除不同于视锥体剔除. 视锥体剔除只是不渲染摄像机视角范围外的物体而对于被其他物体遮挡但依然在视角范围内的物体,则不会被剔除. 注意当你使用遮挡剔除时,视锥体剔除(Frustum Culling)依然有效。

中间操作

1、曲面细分着色器:是一个可选的着色器,主要用于细分图元。

2、几何着色器:是一个可选的着色器,可用于执行逐图元的着色操作,或者被用于产生更多的图元。

3、裁剪:这一阶段是可配置的。目的是把那些不在视野内的顶点裁剪掉,并剔除某些三角形图元的面片。部分在视野内的图元需要做裁剪处理,在裁剪边缘产生新的顶点和三角形进行处理。

4、屏幕映射:这一阶段是可配置和编程的,负责把每个图元的坐标(三维坐标系)转换成屏幕坐标(二维坐标系)。

5、三角形设置:开始进入光栅化阶段,不再是数学上点了,而会把所有的点都映射到屏幕的具体像素坐标上,计算每条边上的像素坐标而得到三角形边界的表示方式即为三角形设置。

6、三角形遍历:这一阶段会检查每个像素是否被一个三角风格所覆盖。如果覆盖的话,就会生成一个片元(一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。

这些状态包括了屏幕坐标、深度信息,及从几何阶段输出的顶点信息,如法线和纹理坐标等。),这样一个查找哪些像素被三角形覆盖的过程就是三角形遍历。

片元着色器

片元着色器的输入就是上一阶段对顶点信息插值得到的结果,更具体点说,是根据从顶点着色器中输出的数据插值得到的。而这一阶段的输出是一个或者多个颜色值。这一阶段可以完成很多重要的渲染技术,如纹理采样,但是它的局限在于,它仅可以影响单个片元。片元着色器是比较花时间的,因为它是最终颜色的计算者,在某些情况下,例如复杂灯光环境下,片元着色器会出现GPU流水线主要的拖后腿的存在。为了让片元着色器的计算更加快,我们需要从很多方面进行提前的优化:
1、尽量减少overdraw
片元着色器最容易拖后腿的情况就是,overdraw!和Android app的开发一样,就是同一个像素点绘制了多次,某些情况会造成计算力的浪费,增加耗电量。前面提到的遮挡剔除有减少overdraw非常有用。在PC上,资源无限,为了得到最准确的渲染结果,绘制顺序可能是从后往前绘制不透明物体,然后再绘制透明物体进行混合。但是在移动平台上,对于不透明物体,我们可以设置从前往后绘制,对于有透明通道的物体(很多UI纹理就是含有透明通道的),再设置从后往前绘制。unity中shader设置为“Geometry” 队列的对象总是从前往后绘制的,而其他固定队(如“Transparent”“Overla”等)的物体,则都是从后往前绘制的。这意味这,我们可以尽量把物体的队列设置为“Geometry” 。对于GUI,尤其要注意和设计师商量,能用不透明的设计就用不透明的,对于粒子效果,也要注意不要引入透明值,多半情况下,移动平台的粒子效果透明值没有作用。

2、减少实时光照
移动平台的最大敌人。一个场景里如果包含了三个逐像素的点光源,而且使用了逐像素的shader,那么很有可能将Draw Calls提高了三倍,同时也会增加overdraws。这是因为,对于逐像素的光源来说,被这些光源照亮的物体要被再渲染一次。更糟糕的是,无论是动态批处理还是动态批处理(其实文档中只提到了对动态批处理的影响,但不知道为什么实验结果对静态批处理也没有用),对于这种逐像素的pass都无法进行批处理,也就是说,它们会中断批处理。

所以当你需要光照效果时,可以使用Lightmaps,提前烘焙好,提前把场景中的光照信息存储在一张光照纹理中,然后在运行时刻只需要根据纹理采样得到光照信息即可。当你需要金属性强(镜面)的效果,可以使用Light Probes。当你需要一束光的时候,可以使用体积光去模拟这个效果。

3、不要使用动态阴影
动态阴影很酷,但是对于片元着色器来说是灾难,阴影计算是三角投影计算,非常耗性能。如果想要阴影,可以使用 a、简单的使用一个带阴影的贴图 b、烘焙场景,拿到lightmaps c、创建投影生成器的方法 d、使用ShadowMap的方法(目前还没有研究)。

4、尽量使用简单的shader
a、建议尽量实用Unity自带mobile版本的(built-in)Shader,这些大大提高了顶点处理的性能。当然也会有一些限制。
b、自己写的shader请注意复杂操作符计算,类似pow,exp,log,cos,sin,tan等都是很耗时的计算,最多只用一次在每个像素点的计算,还有有些除法运算尽量该能乘法运算等。
c、避免透明度测试着色器,因为这个非常耗时,使用透明度混合的版本来代替。
d、浮点类型运算:精度越低的浮点计算越快。
e、不要在Shader中添加不必要的Pass.

五、Unity3D应用性能优化之内存

unity中有两类内存,一个是Mono托管的内存(相当于DVM的内存),一个是Unity3D使用的资源类类型的内存(Texture、Mesh这种)。

Mono内存

1、尽量不要动态的Instantiate和Destroy Object,使用Object Pool。

2、不要动态的产生字符串,使用字符串的直接拼接,使用System.Text.StringBuilder代替。

3、Cache一些东西,在update里面尽量避免search,如GameObject.FindWithTag(“”)、GetComponent这样的调用,可以在Start中预先存起来。

4、尽量减少函数调用栈,用x = (x > 0 ? x : -x);代替x = Mathf.Abs(x)。

5、定时重复处理用 InvokeRepeating 函数实现。

6、减少GetComponent的调用,使用 GetComponent或内置组件访问器会产生明显的开销。您可以通过一次获取组件的引用来避免开销,并将该引用分配给一个变量(transform用的最多)。

7、使用内置数组,内置数组是非常快的。ArrayList或Array类很容易使用,你能轻易添加元件。但是他们有完全不同的速度。 内置数组有固定长度,并且大多时候你会事先知道最大长度然后填充它。内置数组最好的一点是他们直接嵌入结构数据类型在一个紧密的缓存里,而不需要任何额外 类型信息或其他开销。因此,在缓存中遍历它是非常容易的,因为每个元素都是对齐的。

Unity3D类的内存

这类内存包括
1、AssetBundle
Unity3D 里有两种动态加载机制:一个是Resources.Load,另外一个通过AssetBundle,其实两者区别不大。 Resources.Load就是从一个缺省打进程序包里的AssetBundle里加载资源,而一般AssetBundle文件需要你自己创建,运行时 动态加载,可以指定路径和来源的。

AssetBundle运行时加载:

(1)来自文件就用CreateFromFile(注意这种方法只能用于standalone程序,就不提了)。

(2)也可以来自Memory,用CreateFromMemory(byte[]),这个byte[]可以来自文件读取的缓冲,www的下载或者其他可能的方式。其实WWW的assetBundle就是内部数据读取完后自动创建了一个assetBundle而已,Create完以后,等于把硬盘或者网络的一个文件读到内存一个区域,这时候只是个AssetBundle内存镜像数据块,还没有Assets的概念。

下图是AssetBundle的加载卸载示意图:
图片描述

AssetBundle是如何加载的呢?用AssetBundle.Load(同Resources.Load) 这才会从AssetBundle的内存镜像里读取并创建一个Asset对象,创建Asset对象同时也会分配相应内存用于存放(反序列化)。异步读取用AssetBundle.LoadAsync,也可以一次读取多个用AssetBundle.LoadAll。

AssetBundle如何释放呢?

AssetBundle.Unload(flase)是释放AssetBundle文件的内存镜像,不包含Load创建的Asset内存对象。

AssetBundle.Unload(true)是释放那个AssetBundle文件内存镜像和并销毁所有用Load创建的Asset内存对象。

2、Texture
对于IOS选择使用 PVRTC压缩格式的,对于Android选择ETC压缩格式的,纹理可以节省大量内存和读取速度快,但是会有所降低图像的质量。

2D纹理如果没有必要不要使用mimap(会约增加33%的内存开销),曾经在IOS上吃过亏。3D模型的纹理一般是需要mimap的,但是如果确定了3D模型距离摄像机的距离,在GPU分析器上确定了unity使用的纹理,就可以保留,关闭mimap(比如项目中的avatar)。

3.Mesh
有Mesh合并和Mesh压缩(坑比较多,不建议使用)。

4.Particle
粒子效果只要记住使用之后及时释放销毁就行。

针对手游的性能优化,腾讯WeTest平台的Cube工具提供了基本所有相关指标的检测,为手游进行最高效和准确的测试服务,不断改善玩家的体验。

目前功能还在免费开放中。,欢迎点击链接:
http://wetest.qq.com/product/cube 使用。

没有更多推荐了,返回首页