unity性能优化规范

和java不同 能用for就不用foreach,因为foreach会使用迭代器并会产生额外几kb的gc。

和findViewById一样 GameObject.Find和Transform.Find尽量少调用 在Awake和Start只获取一次对象或组件。

与java不同 函数名首字母大写。

工具类声明为 static class 静态类。

开启monoDevelop的代码分析功能 并根据提示尽量优化代码。使用Profile工具进行性能分析测试。

有光照的情况下,烘焙场景和物体可以节约计算资源 你把物体模型放进了场景里之后, 引擎会计算光线,光线照到你的物体的表面形成反光和阴影。 如果不烘焙,
游戏运行的时候,这些反光和阴影都是由显卡和CPU计算出来的。你烘焙之后,这些反光和阴影都记录到了你的模型里,变成了新的贴图了,运行的时候,显卡和CPU不需要进行对环境光效果的运算了,节约CPU资源

字符串连接用StringBuilder 。

不要直接访问gameobject的tag属性。比如if (go.tag == “human”)最好换成if (go.CompareTag
(“human”))。因为访问物体的tag属性会在堆上额外的分配空间 。

对比通过方法GetComponent()获取Transform组件,
通过MonoBehavor的transform属性去取,后者更快,但GetComponent又快于Find 。

善于使用OnBecameVisible()和OnBecameInVisible来控制物体的update()函数的执行以减少开销。

使用内建的数组,比如用Vector3.zero而不是new Vector(0, 0, 0)。

在c#里面List速度优于ArrayList 而且类型安全。

资源及代码都尽量继承和复用framework下的。

尝试使用IL2CPP。

设置贴图压缩格式以优化安装包大小及运行效率:iOS上尽量使用PVRTC,Android上使用ETC 或者
选择自动压缩,但需要在显示质量和性能进行权衡,每家的GPU支持不同的压缩格式,但他们都兼容ETC格式,对于透明贴图,我们只能选择RGBA 16bit
或者RGBA 32bit。如果不调用SetPixels或GetPixels函数,最好关闭Read/Write
enable开关,否则会复制一份贴图到内存中,内存占用会大两倍,这个属性的实质是决定了这个贴图的存放位置,是在内存上还是在显存上。(虽说在移动设备上显存和内存是共用的,但数据不直接互通)
如果可读写,那么这个贴图既在显存上,又在内存上,而且还必须得是非压缩的格式(ARGB32,ARGB4444之类)。用getPixel(s),setPixel(s)之类的方法,读取或设置的数据,都是内存上的数据,但其他属性不区分内存还是显存。Apply方法的作用是把这个贴图在内存上的传给显存。Apply方法的第二个参数,决定了这个这个贴图是否还可读写,换种说法就是内存上这部分数据在执行完这个方法之后还要不要。

代码能加namespace的尽量都加namespace。

在ProjectSetting->
Quality中的,关闭垂直同步。可以考虑降低FPS,降低FPS的好处:1)省电,减少手机发热的情况;2)能都稳定游戏FPS,减少出现卡顿的情况。当我们设置了FPS后,再调整下Fixed
timestep这个参数,这个参数在ProjectSetting->Time中,目的是减少物理计算的次数,来提高游戏性能。可以考虑待机时,调整游戏的FPS为1(手动设置帧率可以用Application.targetFrameRate),节省电量。

尽量少使用Update LateUpdate
FixedUpdate,这样也可以提升性能和节省电量。多使用事件(不是SendMessage,使用自己写的,或者C#中的事件委托)。

把大部分图片打成图集减少draw
call,可以用unity自带的工具打http://www.xuanyusong.com/archives/3304也可以用TexturePackerGUI。图集大小最好不要高于1024,否则游戏安装之后、低端机直接崩溃、原因是手机系统版本低于2.2、超过1000的图集无法读取、导致。2.2
以上没有遇见这个情况。注意手机的RAM 与 ROM、小于 512M的手机、直接放弃机型适配。

RectMask2D性能好于Mask。

考虑使用Occlusion Culling遮挡剔除技术,减少绘制。

对象最好复用同一个材质,并且保证缩放一致,unity会自动进行动态批处理,减少draw
call。Lightmap对象由于多了光照烘焙材质,以及有阴影的对象,不会被批处理。对静止的物体勾选static,就会自动静态批处理,比动态批处理更节约资源。但无论是动态批处理还是静态批处理,都是空间换时间的操作,所以会更占内存,需要权衡利弊。

避免使用顶点过多的对象,减少计算量。

避免使用过于复杂的数学函数,且整形类型计算优于浮点型。

使用有规则的碰撞体,比如长方形,球形,胶囊形,效率高于网格碰撞体。

Image组件的RaycastTarget勾选以后会消耗一些效率,为了节省效率就不要勾选它了,不仅Image组件Text组件也有这样的问题。
一般UI里也就是按钮才需要接收响应事件,那么大部分image和text是是不需要开RaycastTarget的。http://www.xuanyusong.com/archives/4006

按界面分离canvas,减少界面刷新,按静态和动态物体分离canvas,减少界面刷新。

若使用了AndroidJavaXXX类则需要使用using语句或者Dispose函数来确保资源会被释放回收

Unity加载资源深度解析

在游戏和VR项目的研发过程中,加载模块所带来的效率开销和内存占用(即“加载效率”、“场景切换速度”等)经常是开发团队非常头疼的问题,它不仅包括资源的加载耗时,同时也包含场景物件的实例化和资源卸载等。在我们看来,该模块的耗时是目前引擎中仅次于渲染的第二大模块。因此,我们认为非常有必要来跟大家分享一下目前加载模块的主要性能问题。

我们通过对提交到www.uwa4d.com网站的大量项目进行分析,认为目前加载模块中最为耗时的性能开销可以归结为以下几类:
资源加载、资源卸载、Object的实例化和代码的序列化等 。今天,我们先为大家带来资源加载中纹理资源的加载性能分析。

这是侑虎科技第48篇原创文章,欢迎转发分享,未经作者授权请勿转载。同时如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群465082844)

资源加载

资源加载是加载模块中最为耗时的部分,其CPU开销在Unity引擎中主要体现在Loading.UpdatePreloading和Loading.ReadObject两项中,相信经常查看Profiler的朋友对这两项肯定毫不陌生了。

Loading.UpdatePreloading,这一项仅在调用类似LoadLevel(Async)的接口处出现,主要负责卸载当前场景的资源,并且加载下一场景中的相关资源和序列化信息等。下一场景中,自身所拥有的GameObject和资源越多,其加载开销越大。

在很多项目中,存在另外一种加载方式,即场景为空场景,绝大部分资源和GameObject都是通过OnLevelWasLoaded回调函数中进行加载、实例化和拼合的。对于这种情况,Loading.UpdatePreloading的加载开销会很小。

Loading.ReadObject,这一项记录的则是资源加载时的真正资源读取性能开销,基本上引擎的主流资源(纹理资源、网格资源、动画片段等等)读取均是通过该项来进行体现。可以说,这一项很大程度上决定了项目场景的切换效率。正因如此,我们就当前项目中所用的主流资源进行了大量的测试和分析,下面我们将分析结果与大家一起分享,希望可以帮到正在进行开发的你。

注意事项:本篇文章的资源性能分析主要是针对移动项目而言,因为目前UWA所测评的项目中,移动游戏/应用占比在90%以上。所以,我们选择首先在移动设备上针对每种资源的加载性能进行分析和总结。

资源加载性能测试代码

以下为我们测试时所使用的测试代码,我们将每种资源均制作成一定大小的AssetBundle文件,并逐一通过以下代码在不同设备上进行加载,以期得到不同硬件设备上的资源加载性能比较。

** **

测试环境

引擎版本:Unity 5.2版本

测试设备:五台不同档次的移动设备(Android:红米2、红米Note2和三星S6,iOS:iPhone 5 和 iPhone 6)

纹理资源

纹理资源是项目加载过程中开销占用最大的资源之一,其加载效率由其自身大小决定。目前,决定纹理资源大小的因素主要有三种:分辨率、格式和Mipmap是否开启。

1. 分辨率 & 格式

分辨率和格式是影响纹理资源加载效率的重要因素,因为这两项的设置对纹理资源的大小影响很大。因此,我们对这两种因素进行了详细的测试:

测试1:相同格式、不同分辨率的加载效率测试

我们选取了两张分辨率为2048x2048的普通纹理资源,并在打成AssetBundle时,将其分辨率最大值分别设置为512x512、1024x1024和2048x20248,纹理格式均设置为ETC1(Android)和PVRTC(iOS)、且关闭Mipmap功能。所以,三组纹理的内存占用分别为256KB、1MB和4MB,其对应AssetBundle大小为156KB、531KB和1.92MB(对于Android平台)、175KB、628KB和2.4MB(对于iOS平台)。Unity
版本为5.2,压缩格式为默认的LZMA压缩。

Android平台测试纹理:

我们在五种不同档次的机型上加载这些纹理资源,为降低偶然性,每台设备上重复进行十次加载操作并将取其平均值作为最终性能开销。具体测试结果如下表所示。

通过上述测试,我们可以得到以下结论:

1、纹理资源的分辨率对加载性能影响较大,分辨率越高,其加载越为耗时。设备性能越差,其耗时差别越为明显;

2、设备越好,加载效率确实越高。但是,对于硬件支持纹理(ETC1/PVRTC)来说,中高端设备的加载效率差别已经很小,比如图中的红米Note2和三星S6设备,差别已经很不明显。

测试2:不同格式、相同分辨率的加载效率测试

我们选取了两张分辨率为1024x1024的普通纹理资源,并在打成AssetBundle时,根据不同平台将其纹理格式分别设置不同格式用于打包。对于Android平台,我们使用ETC1、ETC2、RGBA16和RGBA32四种格式,对于iOS平台,我们使用PVRTC
4BPP、RGBA16和RGBA32三种格式,同时,对于每张纹理均关闭Mipmap功能。所以,三组纹理的内存占用分别为1MB、1MB、4MB 和
8MB(Android平台)/1MB、4MB 和 8MB(iOS平台)。

Android平台测试纹理:

与测试1相同,我们在五种不同档次的机型上重复进行十次加载操作并将取其平均值作为最终性能开销。具体测试结果如下图所示。

Android设备:

** **

iOS设备:

通过上述测试,我们可以得到以下结论:

1、纹理资源的格式对加载性能影响同样较大,Android平台上,ETC1和ETC2的加载效率最高。同样,iOS平台上,PVRTC 4BPP的加载效率最高。

2、RGBA16格式纹理的加载效率同样很高,与RGBA32格式相比,其加载效率与ETC1/PVRTC非常接近,并且设备越好,加载开销差别越不明显;

3、RGBA32格式纹理的加载效率受硬件设备的性能影响较大,ETC/PVRTC/RGBA16受硬件设备的影响较低。

注意事项:这里需要指出的是测试中所使用的ETC1和ETC2纹理均为RGB
4Bit格式,所以对于半透明纹理贴图,需要两张ETC1格式的纹理进行支持(一张RGB通道,一张Alpha通道)。逐一加载两张ETC1格式的纹理,其加载效率要低于RGBA16格式,但可以通过加载方式来进行弥补。这一点我们将在后续文章中进行详细说明。

2. 开启Mipmap功能

开启Mipmap功能同样会增大一部分纹理大小,一般来说,其内存会增加至原始大小的1.33倍。因此,我们对开启Mipmap功能前后的加载性能进行了详细的测试:

测试3:开启/关闭Mipmap功能的加载效率测试

我们仍然使用两张分辨率为1024x1024的普通纹理资源,分别使用ETC1格式、PVRTC格式、RGBA16格式和RGBA32格式(测试所用纹理与测试2相同),并在打成AssetBundle时,一组开启Mipmap功能,一组关闭Mipmap功能。

与测试1相同,我们在五种不同档次的机型上重复进行十次加载操作并将取其平均值作为最终性能开销。具体测试结果如下图所示。

Android平台:

** **

** **

iOS平台:

** **

通过上述测试,我们可以看出: 开启Mipmap功能会导致资源加载更为耗时,且设备性能越差,其加载效率影响越大。

通过以上性能测试,我们对于纹理资源的加载建议如下:

1、严格控制RGBA32和ARGB32纹理的使用,在保证视觉效果的前提下,尽可能采用“够用就好”的原则,降低纹理资源的分辨率,以及使用硬件支持的纹理格式。

2、在硬件格式(ETC、PVRTC)无法满足视觉效果时,RGBA16格式是一种较为理想的折中选择,既可以增加视觉效果,又可以保持较低的加载耗时。

3、严格检查纹理资源的Mipmap功能,特别注意UI纹理的Mipmap是否开启。在UWA测评过的项目中,有不少项目的UI纹理均开启了Mipmap功能,不仅造成了内存占用上的浪费,同时也增加了不小的加载时间。

4、ETC2对于支持OpenGL ES3.0的Android移动设备来说,是一个很好的处理半透明的纹理格式。但是,如果你的游戏需要在大量OpenGL
ES2.0的设备上进行运行,那么我们不建议使用ETC2格式纹理。因为不仅会造成大量的内存占用(ETC2转成RGBA32),同时也增加一定的加载时间。下图为测试2中所用的测试纹理在三星S3和S4设备上加载性能表现。可以看出,在OpenGL
ES2.0设备上,ETC2格式纹理的加载要明显高于ETC1格式,且略高于RGBA16格式纹理。因此,建议研发团队在项目中谨慎使用ETC2格式纹理。

** **

正是由于以上加载效率问题,我们在UWA测评报告中加入了对每个检测到的纹理资源参数的详细展示,以方便开发团队可以快速查看资源的使用情况,只需对相关信息列进行排序,即可定位引发性能问题的具体资源。

** **

说明:以上测试数据为我们所用的测试纹理加载数据,需要指出的是,不同纹理的加载效率是不相同的,因为其内容的不同会造成AssetBundle压缩包大小的不同,进而造成最终加载效率的不同。这里我们给出的具体性能比较,其本意是让大家通过数据直观了解到纹理格式、分辨率和Mipmap功能对加载性能的影响。另外,我们后续会进行更多的测试,以期为大家提供更为普遍的测试结果。

以上为纹理资源在加载时的性能测试。关于加载模块的性能问题,
我们会不断推出网格、音频等其他资源的加载性能分析、资源卸载性能分析、资源实例化性能分析、不同加载方式的性能分析等一系列技术文章
,并对目前UWA所检测过项目的共性问题进行总结,以期让大家对项目的加载效率有更加深入的认知,并提升对加载模块的掌控能力。

如何优化UGUI的ScrollRect

介绍

每个元素知道自己的序号,可以根据需要修改自己的内容、大小等信息。

此外支持了ScrollBar,支持横向、纵向及正反向。

在关闭Mask后可以看到,只有当需要的时候才动态实例化元素,使用完后回收。

最原始版本的代码是@ivomarel的InfinityScroll。我改到后来,基本和原始版没啥相同的了。

原代码使用了sizeDelta作为大小,但是这个在锚点不重合情况下是不成立的

支持了GridLayout

在启动时检查锚点和轴心,方便使用

修复了原代码在往前拖拽会卡顿的问题

优化代码,提升性能

支持反向滑动

支持ScrollBar (在无尽模式下不起作用;如果元素大小不一致会出现滚动条瑕疵)

此外,我修改了Easy Object Pool作为池子,循环利用元素。

警告:
为了解决原始代码回拉卡顿的问题,我直接复制了一份UGUI中的ScrollRect代码,而没有继承。这是因为老的做法是在onDrag里停止并立即启动滚动,而我通过修改两个私有变量保证了滑动顺畅。所有我的代码都用==========LoopScrollRect==========这样的注释包起来,维护起来就像打patch了。

框架思路

和UGUI自带的ScrollRect有所不同,我拆分出了LoopHorizontalScrollRect和LoopVerticalScrollRect两个类,分别代表水平滚动条和水平滚动条。下面我们以LoopVerticalScrollRect为例,水平版本类似。

1. 判定cell大小

LoopScrollRect要解决的核心问题是:如何计算每个元素的大小。这里我使用了Content Size Fitter配合Layout
Element来控制每个cell的长宽,因此对于GridLayout直接取高度,否则取Preferred
Height。需要注意的是,除了元素本身的大小之外,我们还要将padding考虑进去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
protected override float GetSize(RectTransform item)



{



    float size = contentSpacing;



    if (m_GridLayout != null)



    {



        size += m_GridLayout.cellSize.y;



    }



    else



    {



        size += LayoutUtility.GetPreferredHeight(item);



    }



    return size;



}

这个其实也是最核心的一个地方:在能够准确计算格子大小的基础上,后续工作就好实现了。

2. 如何优雅的增删元素

对于每个ScrollRect,其实只需要考虑在头部和尾部是否需要增加或者删除元素。在这里以头部的各种情况为例进行解释,因为在正向滑动情况下,必须保证在修改元素之后整个ScrollRect内容显示一致不跳变;这些情况比尾部处理会麻烦一些。

NewItemAtStart函数实现了在头部增加一个(或一行,针对GridLayout)元素,并返回这些元素的高度;DeleteItemAtStart代表删除头部的一个元素。需要注意的是,在修改头部元素之后要及时修改content的anchoredPosition,这样才能保证整个内容区域不会因为多了或者少了一行而产生跳变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
protected float NewItemAtStart()



{



    float size = 0;



    for (int i = 0; i < contentConstraintCount; i++)



    {



        // Get Element from ObjectPool



    }



    if (!reverseDirection)



    {



        // Modify content.anchoredPosition



    }



    return size;



}



protected float DeleteItemAtStart()



{



    float size = 0;



    for (int i = 0; i < contentConstraintCount; i++)



    {



        // Return Element to ObjectPool



    }



    if (!reverseDirection)



    {



        // Modify content.anchoredPosition



    }



    return size;



}

3. 何时需要增删元素

这里需要有两个概念viewBounds和contentBounds:前者是指ScrollRect本身的大小,一般也对应Mask;后者是指ScrollRect里所有cell组成的内容部分的大小。在这个基础上就简单了:如果contentBounds的最上面比viewBounds的最上面要低,那么尝试在顶部增加元素;如果contentBounds的最上面比viewBounds的最上面高很多,那么尝试删除元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
protected override bool UpdateItems(Bounds viewBounds, Bounds contentBounds)



{



    bool changed = false;



    // cases for NewItemAtEnd/DeleteItemAtEnd



    if (viewBounds.max.y contentBounds.max.y - 1)



    {



        float size = NewItemAtStart();



        if (size 0)



        {



            changed = true;



        }



    }



    else if (viewBounds.max.y < contentBounds.max.y - threshold)



    {



        float size = DeleteItemAtStart();



        if (size 0)



        {



            changed = true;



        }



    }



    return changed;



}

4. 对象池交互

在新建cell和销毁cell的时候,使用对象池来避免内存碎片;同时这里使用了SendMessage来向每个cell发送必须的信息,保证数据的正确性。

private void SendMessageToNewObject(Transform go, int idx)

{

go.SendMessage("ScrollCellIndex", idx);

}

private void ReturnObjectAndSendMessage(Transform go)

{

go.SendMessage("ScrollCellReturn",

SendMessageOptions.DontRequireReceiver);

prefabPool.ReturnObjectToPool(go.gameObject);

}

private RectTransform InstantiateNextItem(int itemIdx)

{

RectTransform nextItem =

prefabPool.GetObjectFromPool(prefabPoolName).GetComponent<RectTransform();

nextItem.transform.SetParent(content, false);



nextItem.gameObject.SetActive(true);



SendMessageToNewObject(nextItem, itemIdx);



return nextItem;

}

5. 滚动条相关

这块我其实是估算的,根据当前的长度和当前元素个数/总个数按照比例缩放,这个在所有cell大小一致的情况下是没有问题的;但是如果大小不一致我就无法得到精确结果,所以会产生一定抖动。我暂时没有更好办法,因为得到的信息就是不够用。

6. 其他细节

我主要遇到了两个坑:

增加或者删除元素之后,有时候需要强行调用Canvas.ForceUpdateCanvases()刷新下。

注意不要在Build Canvas过程中再次修改元素,从而再次触发Build Canvas。

使用示例

以竖直滚动条为例,介绍一下步骤。如果觉得麻烦的话,直接打开DemoScene复制粘贴就好。当然你也可以干掉EasyObjPool,自己控制生成和销毁。

1. 准备好Prefabs

每个物体上需要贴上Layout Element并指定preferred width/height。

贴上一个脚本接受void ScrollCellIndex (int idx) 消息,从而对每个位置的元素根据需要灵活修改。

** **

2. 在Hierarchy里右键,选择UI/Loop Horizontal Scroll Rect或UI/Loop Vertical Scroll
Rect即可。
使用Component菜单里的也是一样的。

Init in Start: 启动时自动调用Refill cells初始化

Prefab Pool: EasyObjPool物体

Prefab Pool Name: 第二步中对应的Cell Prefab名字

Total Count: 总共能有多少物体,范围0 ~ TotalCount-1

Threshold: 两端预留出来的缓存量(像素数)

ReverseDirection: 如果是从下往上或者从右往左拖动,就打开这里

Clear Cells: 清除已有元素,恢复到未初始化状态

Refill Cells: 初始化并填充元素

如果是正向滑动,就设置pivot为1;否则设为0并打开ReverseDirection。我强烈建议你试试在播放状态下修改这些参数。

无尽模式

如果需要无限滚动模式,将totalCount设为负数即可。

其他参考

后来搜了下,发现网上也有人提到过UGUI ScrollRect
优化(<http://blog.csdn.net/subsystemp/article/details/46912479),不过他的策略是监听ScrollRect的value,然后禁用范围外的cell。最后作者也提到改成动态加载策略。这种基于value的做法我不太确认在在滚动前动态添加新元素的时候是否会出现问题。

文末,再次感谢钱康来的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群465082844)。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×