Unity性能优化技巧 纪念首次撸出来的编程题--2020深信服软件测试岗 qml 去除标题栏后 拖动窗口和改变窗口大小 如何舒服地在图书馆用ipad入门深度学习【windows jupyter远程】 力扣Java版个人代码分享-树篇( 107. 二叉树的层次遍历 II) 第十届蓝桥杯省赛java类B组 试题 E:迷宫 (动态规划之回溯法) Unity+罗技G29方向盘+Realistic Car Controller 制作简单的模拟驾驶 2020阿里笔试题解(9.11) 起飞!这份技术点拉满的ELk+Lucene笔记,可能价值百万 好文精选整理--Redis+Nginx+设计模式+Spring全家桶+SQL+Dubbo技术 覆盖全网的微服务架构笔记,看完还不懂你来打我 技术干货:JVM架构体系与GC命令全梳理,建议收藏 跪拜,阿里P9加班到凌晨,硬肝三个月推出这份IT架构运维实践 太厉害了,华为架构师终于整理出SSM+Nginx+Redis+SQL+微服务pdf 膜拜!终于有人总结出Spring+SpringMVC+MyBatis源码层PDF了 开发1-5年的Java程序员,该学习哪些知识实现涨薪30K? 云原生景观:供应层(Provisioning)介绍 vulhub学习笔记-struts2 S2-057 Remote Code Execution Vulnerablity远程代码执行 微服务启动报 Error creating bean with name ‘eurekaAutoServiceRegistration‘ 异常 「信息安全-密码与隐藏技术」RSA加密算法的实现(CPP 实现) 单例模式线程是否安全? DDCTF2020 Writeup 迭代器模式在开源代码中的应用 微信群总是有人发广告?看我用Python写一个自动化机器人消灭他! 极光大数据持续亏损,称风控产品数据涉10亿移动端用户、包括财产消费等信息,对外投资极贷管家 揭秘英飞凌最新安全芯片解决方案:为物联网设备量身定制,小封装易开发 蒙草大数据西乌旗智慧畜牧业系统建设取得新进展 移动转售产业与大数据产业交流座谈会即将召开 19-2!62比24!湖人4大数据碾压对手,夺赛点进西决稳了 Web前端程序员每天的工作都是做什么的?有哪些是必须要做的? 四面楚歌祭利剑:华为再推鸿蒙OS,另辟蹊径进军物联网 InnoDB可重复读隔离级别是如何实现的 腾讯云物联网平台重磅升级:聚合内部能力,辅助更多产业 u校园刷课软件一键答题新视野视听说读写综合训练 css隐藏元素的几种方式及区别 display:none visibility:hidden opacity 元素隐藏 2020-09-10 使用echart完成折线图 wordpress使用memcached缓存数据提高访问速度 U校园刷课软件U校园新视野综合教程视听说答题 vue源码(十三) 数组下标改变值的响应式误区以及实现 非插件自动为WordPress关键词添加链接 SpringBoot详解(一) 从入门到入土 Mysql系列第一讲 mysql基础知识与安装 作为测试工程师,你一定要知道的数据库操作命令大全! mysql-mysql学习详记三&&数据库的备份及恢复&&多表设计&&外键约束 GROUP BY 的内在细节展示!!(不可忽略点!!) LeetCode 178. 分数排名 MySQL数据库管理语句用法(增、删、改、查、授权……) 华为发布物联网实践系列教材 内容覆盖云、管、边、端物联网解决方案 华为发布物联网实践系列教材 内容覆盖云、管、边、端物联网解决方案
您的位置:首页 >物联网 >

Unity性能优化技巧

最近看了B站Uinty官方有关性能优化技巧的视频,自己做一些整理。

视频链接:

Unite Now - (中文字幕)性能优化技巧(上)

Unite Now - (中文字幕)性能优化技巧(下)

 

堆栈(Stack)和堆积(Heap)

我们先来看下Unity内存中重要的两部分,堆栈和堆积,因为只有了解了它们,我们才能知道应该如何优化内存,提高性能。

堆栈:

堆栈是内存中存储函数和值类型的地方。

例如我们调用一个函数A,会将这个函数体与函数收到的参数放入到堆栈中,若在函数A中调用函数B,同样会把函数B存放到堆栈中。当函数B运行结束,会将其从堆栈中移除,然后当A运行结束,把A从堆栈中移除。

因此我们在看Debug信息的时候,就会发现Log里面能够做到一层层的方法回溯,方便我们查看整体的调用过程,这也就是堆栈回溯。

由于是堆栈的结构,因此不会遇到碎片化或是垃圾收集(GC)的问题。但是可能会碰见堆栈溢出的问题,比如调用了太多的函数导致一直push东西进堆栈,占据越来越多的内存空间,导致堆栈溢出。

 

堆积:

 

堆积是内存中另一个区域,要比堆栈大,我们将所有的引用类型存放在这。通常我们每创建一个新的对象,会在堆积中找到下一个足够存放的空位置,将其存储。但是当我们销毁对象后,内存空间不会马上释放出来,而是标记成未使用,之后垃圾收集器会释放这部分空间。

对象实例化和摧毁的过程其实很慢,所以我们要尽可能地避免在堆积中配置内存的行为。如果我们需要的内存比之前已经配置好的还多,在放不下的情况下,堆积会膨胀,并且每次都增长两倍,且不会再缩回去,过大的堆积就会影响到我们游戏的性能。当我们在堆积中释放了一些占用空间小的对象,而后添加一些占用空间大的对象时,由于前面释放的空间不足以存放下,就会导致这些空间空出来,使得内存的使用情况就变得断断续续起来,这也就是内存的碎片化,同样降低我们的游戏性能。

 

垃圾收集(GC)的原理:每一次GC,都会遍历堆积上所有的对象,找到需要释放的东西,然后将其释放。

假如游戏玩到一半,GC必须要释放数十或数百个游戏对象的内存,那么这会对你的游戏过程造成一个负载峰值,我们要避免这样的负载峰值。

 

编程过程中的一些优化建议

1.选择合适的数据结构

数据结构,也就是Array,List和Dictionary等,例如在Array或List中使用索引的成本很低,那么就适合要经常通过索引读取的情况。而要频繁增加和移除对象时,使用Dictionary是最合适的。

 

2.对象池

在游戏程序中,创建和销毁对象事很常见的操作,通常会通过 Instantiate 和 Destroy 方法来实现,如果频繁的进行这些操作,GC的时候会导致负载很重,因为会有大量的已摧毁对象的存在,不仅会造成CPU的负载峰值,还可能导致堆积碎片化。因此我们可以使用对象池来处理这类问题。

使用对象池时需要注意,要决定对象池的大小,以及一开始要产生多少数量的对象在池中。因为如果你需要的对象数量多过池中现有的,就必须将对象池变大,扩的太大可能造成浪费,扩的小可能又造成频繁的添加。

 

3.Scriptable Objects

假设我们有一个控制敌人的组件,名叫Enemy,代码如下:

public class Enemy : MonoBehaviour{public float maxSpeed;public float attackRadius;}

这个组件挂载在每个敌人身上,但是其中这两个浮点数(maxSpeed 和 attachRadius)的数值都是不变的。那么当场景中存在很多的敌人时,每次生成敌人的时候,这些数据就会重复一份。

所以即使所有数据都一样,这两个浮点数还是重复的出现在有此脚本的对象上。所以建议改用Scriptable Objects,这样就只会耗费一组这样数据的内存,代码如下:

public class EnemyConfiguration : ScriptableObject{public float maxSpeed;public float attackRadius;}public class Enemy : MonoBehaviour{public EnemyConfiguration enemyConfiguration;}

 

4.变量or属性

通常我们为了封装安全性,开发时会选择使用属性(getter/setter),而属性本质上是函数的调用,前面提到调用函数时,会在堆栈上分配内存,因此调用属性也是如此。当调用多次时,花费在堆栈中的时间就会增加。当然了,一般来说问题不大,但是如果在使用频繁的循环体中使用属性,可能就需要针对性的优化。

我们可以通过宏命令进行处理,例如在开发时使用属性,发布版本时使用变量,如下:

#if DELELOPMENT_BUILDint m_health;public int health { get => m_health; }#elsepublic int health;#endif

 

5.Resources目录

当项目被构建时,所有名为Resources的文件夹中的所有Asset和Object都会合并到同一个序列化文件中。这个序列化文件中还含有元数据(Metadata)和索引(Indexing)信息。同时加载Resources文件这一操作无法跳过,它会在应用程序启动显示不可交互的启动画面(Splash Screen)时执行,即使里面很多资源我们此时都没有用到,这就会直接影响游戏的启动时间,同时也会占用很大的内存。

所以建议直接弃用Resources,使用AssetBundle,以更有效的方式管理资源的载入和卸载。(也可以试试Addressable资源系统)

 

6.删除空的Unity事件

Monobehaviour中的Start,Update这些方法即使是空的,也会带来些微的性能消耗,因此若为空,就删除它们。

 

7.避免在Awake和Start中添加大量的逻辑

这对游戏启动很重要,Unity会在Awake和Start方法执行后渲染第一个画面,某些情况可能会导致启动画面或是载入画面需要花更长的时间渲染,因为你必须等每个游戏对象都完成Awake和Start的执行。(游戏启动时,黑屏太久,可能会被退审)

 

8.缓存一些Hash值

在我们想要在运行时修改动画或者材质的时候,可以使用下面方法来实现

animator.SetTrigger("Idle");material.SetColor("Color", Color.white);

这类方法往往也可以通过索引来作为参数,使用字符串只是能显示的更加直观,但是当我们传递字符串时,程序内部会进行一些处理,频繁调用的话可能就会造成性能的消耗。因此我们可以先找到对应的索引,并将其缓存起来,供后续使用,如下:

int idleHash = Animator.StringToHash("Idle");animator.SetTrigger(idleHash);int colorId = Shader.PropertyToID("Color");material.SetColor(colorId, Color.white);

 

 

9.层次结构

某些情况下,场景中的物体可能有很深的嵌套结构,当我们对父节点的GameObject进行坐标转换时,就会产生OnTransformChanged事件,这消息会传递给该GameObject下所有子对象,即使这些对象没有任何渲染组件(也就是我们看不见任何变化),造成一些不必要的转换运算,包括平移,旋转和缩放。

此外,较深的结构也会导致在GC时,花费更多的时间在层级结构间遍历。

 

10.Accelerometer Frequency

这个设置在Project Settings->Player->IOS->Other Settings中,这个功能定义Unity从设备读取加速度仪信息的频率,在不需要加速仪的游戏中,将它启动或设置了高于需求的频率,会影响性能表现。因为读取硬件设备信息,会增加CPU的处理时间。

 

11.移动物体

Unity中有许多移动游戏对象的方法,例如 transform.Translate,如果对象需要碰撞判定,我们则会添加刚体和碰撞体,如果还是使用 transform.Translate 方法,会造成PhysX物理引擎整体重新计算,对于复杂的场景,成本可能很高。因此若要移动带有刚体的对象,使用rigidBody.MovePosition,并且要在FixedUpdate方法中执行。

建议使用transform.Translate就在Update中执行,使用rigidBody.MovePosition或AddForce方法在FixedUpdate中执行。

 

12.添加组件

在运行时调用AddComponent其实很没效率,尤其在一帧中多次启用这类调用。

当我们添加一个组件的时候,Unity会做下列操作:

先看组件有没有DisallowMultipleComponent的设置,如果有,就要去检查是否有同类型的组件已加入然后检查RequireComponent设置是否存在,如果设置了,就代表这个组件需要别的组件同步加入(重复做添加组件的操作)最后调用所有被加入的MonoBehaviour的Awake方法

上述这些步骤都发生在堆积上,所以可能会影响性能和增加GC的处理时间。

 

13.缓存引用对象(与第8条类似)

例如我们常常会在游戏运行的时候去查找一些对象,GameObject.Find与其他所有关联的方法,需要遍历所有内存中的游戏对象以及组件,因此在复杂场景中,效率会很低。GameObject.GetComponent,会查询所有附加到GameObject上的组件,组件越多,GetComponent的成本就越高。若使用的是GetComponentInChildren,随着查询变复杂,成本会更高。

因此不要多次查询相同的对象或组件,而且查询一次后将其缓存起来,方便后续的使用。

 

资源导入的一些优化建议

例如下图中左右两边使用的都是相同的模型与贴图,但是最终所占的磁盘大小却差了很多,就是因为一些设置导致的。

 

有关纹理导入设置的建议:

1.根据平台不同,纹理的 Max Size 设成该平台最小值

2.纹理的大小为2的幂次方(POT),因为有些压缩格式可能不支持非2的幂次方的。

3.尽量将多张纹理合并成为大图

4.对于不透明纹理,关闭其 alpha 通道

5.除非你必须从代码来访问纹理的底层数据,否则关闭 Read/Write Enabled 选项,减少内存使用

6.选择合适的Format,可减少占用的空间

7.例如UI元素这类相对于相机Z轴的值不会有任何变化的纹理,关闭Generate Mip Map选项

 

Mesh的导入设置建议:

1.试着用高比率的Mesh压缩,来减少磁盘容量。注意:运行时的内存不受这项设置影响

2.尽量关闭 Read/Write Enabled 选项,若开启,Unity会存储两份Mesh,导致运行时的内存用量变成两倍。

3.如果没有使用动画,请关闭Rig,例如房子,石头这些

4.如果没有用到 Blendshapes,也关闭

      

5.如果Material没有用到法向量和切线信息,关闭可以减少额外信息。

 

图像(Graphics)的一些优化建议

基本上当Unity渲染游戏图像时,会调用 draw call 来对GPU下指令,让场景能成功渲染。对象,材质和纹理越多,处理起来需要的时间也越多。所以过多的drawcall就会影响游戏的优化,这对于瓶颈在GPU上的游戏影响特别大,也就是我们的游戏已经给GPU太大的压力了。

 

使用批处理:

我们可以使用批处理来尽量减少drawcall,使用批处理需要满足一些情况,例如,要批处理的对象必须引用一样的材质,并使用相同的纹理(纹理合并在这就很重要),但是使用的模型可以不一样。

动态批处理:可以减少对于移动对象的drawcall。只能用于少于900个顶点信息的情况,包含坐标、法线、uv0、uv1、切线。动态批处理每帧评估一次,由CPU负责。

静态批处理:即对开启 static 标记的对象做批处理,在构建期完成。适用于绝大部分的静态Mesh,因此任何不会动的对象都应标记为静态的。如果我们在运行时要添加静态对象,可以看一下 StaticBatchUtility.Combine() 的API

有关SRP Batcher可以看下:https://blog.csdn.net/wangjiangrong/article/details/105518220

 

Cast Shadows

默认情况下,MeshRenderder组件的Cast Shadows是开启的。

阴影的渲染可以让游戏的光线增加真实度和深度感,但是某些情况下可能并不需要。在复杂场景中,可能会造成多余的阴影计算,阴影效果最后也看不见。

因此若场景有的对象是否有阴影对整体效果没有影响的话,就关闭这个选项。不计算阴影可以省下CPU时间。(具体渲染步骤可以在 Frame Debugger的Shadows.Draw中查看)

 

Light Culling Mask

在复杂场景中,许多光线紧靠彼此,你可能觉得光线不能影响特定对象。根据渲染流程的设置,场景中越多的光照,性能可能就会越差。因此我们要确保光照只影响特定的对象层(例如专门给角色打光的光源,设置成只影响角色),尤其是多光源和多对象彼此紧靠的时候。

 

避免使用手机原生分辨率

现在的手机分辨率非常的高,在手机呈现高分辨率可能会影响性能和手机过热的问题。因为会有大量的计算需求,如后期处理。如果游戏本身很耗GPU,高分辨率会恶化这些问题。建议使用 Screen.SetResolution 来降低游戏预设的解析设置(根据不同的设备来找到一些合适的值),来提高性能。

 

UI的一些优化建议

显示与隐藏

UI的隐藏我们可以使用将其移到Canvas外的方法,而不是利用SetActive(false)的方法来隐藏。

视频中建议的似乎是SetActive(false)

 

UI的批处理

如果UI元素会改变数值或是位置,会影响批处理,导致向GPU发送更多的drawcall。因此建议:

1.将更新频率不同的UI放在不同的Canvas上。

2.相同Canvas中的UI元素的Z值要相同,这样才不会打断批处理。

3.相同Canvas中的UI元素要使用相同的材质和纹理,材质或着色器可以有动态变换(例如一些特效),这不会影响批处理。

4.相同Canvas中的UI元素要使用相同裁剪矩阵。

 

Graphic Raycaster

该组件是用来处理输入事件,默认挂载在每个Canvas上。有时不能互动的对象仍是canvas中的一部分,并附带了该组件,所以当每次鼠标或触控点击时,系统就要遍历所有可能接受输入事件的UI元素,就会造成多次的 “点落在矩形中” 的检查,来判断对象是否该作出反应。在UI很复杂的情况下,这个运算成本就会很高。因此建议确保只有可互动的Canvas才有该组件,节省CPU运行时间。

 

全屏UI的处理

游戏中可能会有些全屏UI(例如一些设置界面),会遮挡住场景物体或其他UI元素。然而它们即使被遮挡看不见,CPU和GPU还是会有消耗,因此建议:

1.3D场景完全被遮挡的话,关闭渲染3D场景的摄像机。

2.被遮蔽的UI,Disable这些Canvas,注意不是SetActive(false)。

3.尽可能的降低帧率,因为这些UI一般不需要刷新那么频繁。

 

音频(Audio)一些优化建议

音频文件常以不正确的方式导入的Unity中,原因可能是对硬件或格式不熟悉,或是导入过程中出现了问题。这将造成运行时内存使用过高。打包中占用大量的空间。以及没有善用底层硬件提供的解压缩方式。因此建议:

1.可以的话,将音频文件设置为Force To Mono,这样做可以省下一半的内存和磁盘空间。

2.如果需要额外的压缩,可以降低文件的比特率(bitrate),前提音频品质不会被破坏太严重。

3.IOS适合使用ADPCM和MP3格式,Android适合使用Vorbis格式。

 

载入方式

小型音频文件(< 200kb)Decompress On Load

中型音频文件(>= 200kb)

Compressed In Memory大型音频文件,例如背景音Streaming

注:文件必须小于200kb,因为内部内存管理的问题,大于200kb的文件也还是只会被分配到这么多。

 

静音处理

一般游戏中都会有静音的设置,我们往往我们只是把AudioSource或Mixer的音量设置为0,这样还是会造成不必要的内存和CPU占用,关音量并不会释放音频的内存。

因此建议在内存中卸载音频相关的来源或是内存中的音频文件,将AudioSource组件Disable,同时有个上层管理系统负责过滤和音频相关的API调用。当然卸载和重新载入音频的成本也很高,要是玩家频繁的开启和关闭静音的话,就不适用了(一般情况下不会)

 

 

郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。