Unity优化策略(常用优化手段)

不知名大佬曾说过:程序的尽头就是优化

这句话在游戏开发过程中,更是体现的淋漓尽致

初入行业内,我明白了两件事情

        1、程序猿写代码的速度永远跟不上需求下发的速度

        2、游戏能保持的帧数取决于你拒绝策划脑洞的次数

当然,这么说只是开个玩笑

身为一名程序员,我们可以动用太多的手段去优化一个体量臃肿的游戏

从项目资源、框架结构到脚本编写,每一步都能尝试去优化一下

本人学识尚浅,以下内容权当抛砖引玉,梳理杂乱的记忆……

Unity内的优化手段

一、优化三剑客:drawcall,overdraw,profile

        1、drawcall:

        什么是drawcall:为了将物体绘制到屏幕上,引擎必须向图像API发送一个drawcall指令,每一次发送drawcall指令的过程为一个渲染批次(Batch),而这个过程分为两大部分:设置渲染状态(setPass)和调用drawcall(Batches),此时CPU的主要工作就是设置这些物体的渲染状态后调用drawcall,从而造成CPU性能的开销,简单来说也就是CPU向GPU发送指令并由GPU进行绘制

        因此drawcall次数越多,CPU进行计算的量越大,而CPU的处理速度比GPU慢多了(特指复杂度低但计算次数多的运算),所以可以将绘制的压力移交给GPU。Unity可以将一些物体进行合并,从而用一次DrawCall来渲染他们。这一操作,称为批处理。

        而每次提到drawcall,其实就是在说:该怎么对物体进行批处理?

        ①静态批处理:将gameobject的static设置为true,就算处理完了。

        静态批处理默认是开启的,Project Setting—Player—Static Batching游戏运行时会自动为它们合批,将所有静态物体的Mesh Filter自动合并成一个新的Mesh。

注意:
        需要有相同材质,如果不同只会合并mesh,batches还是会增加
        静态合批的最大顶点数是65535,如果顶点数超过了它,Unity就会自动合并出多个Mesh
        运行游戏后合并过的Mesh对象是不可以发生位移的,但是可以移动父物体节点
        打包时会把合并的mesh存储起来,所以需要额外的内存去存储,并且运行时需要加载这个合并后的大Mesh,所以使用时需要注意,例如一个很大场景,使用静态批处理后包体中就会多一份合并的Mesh,运行时也会加载整个大场景的Mesh,但是游戏运行后只有一小部分出现在摄像机内,那么整个大的Mesh都需要参与渲染

        ②动态批处理:默认是关闭的,开启方式:Project Setting—Player—Dynamic Batching

        unity会自行合批,如果材质相同,会将这一批的物体统一打包发给GPU进行drawcall。可以降低cpu发送指令的次数,减少消耗

注意:
        必须使用相同材质(相同材质的物体之间的差别只在于顶点数据不同,可以将顶点数据合并在一起,再一起发送给GPU)
        Mesh不能超过900个顶点(带uv的模型不能超过300个顶点)
        transform的scale属性不能有负值

        ③手动合批 & GPU Instancing:不会()
        

        2、overdraw:

        什么是overdraw?

                字面意思即为:过量绘制,你多次绘制同一像素(在一帧中)时,会发生OverDraw

        例如:当前游戏的屏幕为 2560 * 1440,此时UI界面有Image A和 ImageB,它们都为非透明图片,且覆盖了整个屏幕,那么在玩家的视角里,屏幕上应当只能看到一张图片,因为总有一张图片会覆盖在另一张图片上。

        但实际unity在绘制时,会先绘制底下的那张图片(假设是Image A),当绘制到Image B时,发现当前这个像素需要重新上色绘制,那么前面绘制A时的开销就相当于浪费了。因为真正显示到屏幕上的图片只有Image B。由于这不合理的图片堆叠操作,系统需要整整多绘制:

2560 * 1440 = 3,686,400  个像素点!

        为了应对这种情况,unity提供了专门的应对手段,可以简单方便的查看到当前的overdraw

        开启方式:scene窗口 -> 点击左上角Shaded -> 点击Overdraw

因此在游戏制作过程中,可以尝试多开一个scene窗口,专门用于查看当前的overdraw情况

        利用可视化的overdraw来优化我们的游戏:

        例如底下这张图,左边是game窗口看到的,右边是该UI的overdraw情况。不难发现,右边这个UI都快叠成麻花了……因此发现这种情况的时候,可以尝试降低UI堆叠,简化该UI的复杂度。

每多一次overdraw,对于整个游戏就多一层负担。发热、卡顿和掉帧等情况的出现往往就是一次又一次的增添累赘,最终拖垮整个系统。

        3、profile:

        profile是Unity内置的工具,方便开发者以可视化界面查看当前游戏的开销情况

        开启方式:Window -> Analysis - > Profiler

        打开后如下图:

        开启窗口后,选择启动游戏,Profile窗口便开始绘制当前游戏进程的性能开销情况

        利用profile来优化我们的游戏:

        每当一个功能写好后,我们可以开启profile来查看新功能是否会造成 “波峰”,需要重点关注这些波峰,profile会详细的绘制开销情况,例如Rendering,Scripts,UI,Animation等。一旦某个部分在当前帧占比过大,则需要考虑是否可以优化优化,例如降低GameObject创建和销毁的次数(引入对象池,或针对特定功能做成单例),减少UI开关的次数等。

        

二、优化的小手段

        虽然很想扯一些牛逼的优化技术,但是对于我这种小菜鸟来说,简单、快捷且易于上手的优化手段如果能多掌握几项,并且时常用在游戏开发中,杀伤力一样不低。

        1、ui优化

        ①一个ui图片,即使身上挂的是透明的sprite,unity依旧会将其纳入渲染范畴(增加drawcall),如果真的需要透明的image,应将其sprite的alpha调为0,同时设置Cull Transparent Mesh设为true。

接下来用drawcall来观察一下:

        当前UI Image 的 sprite 透明度已经设为了0,但是没有勾选Cull Transparent Mesh

        此时勾选Cull Transparent Mesh

        ②不需要射线检测的ui组件,关掉raycast target(射线检测)
        ③ui制作尽量保持“层级少,锚点准”。不要套娃式叠ui,也不要忘记设置锚点,合理的UI布局可以有效的防止在不同分辨率的屏幕下出现UI堆叠的情况
        ④不要养成手动调整UI Scale的习惯,直接改width和height即可,防止代码中出现setScale()而造成的各种问题
        ⑤使用 uiGameObject.SetActive(true or false) 或 Destory(uiGameObject)控制UI组件的开启或关闭  颇耗性能,如果条件允许,你可以尝试 将该UI组件的 Scale X(或者是 Scale Y)设置为0。

        节约就是胜利……

        2、图片资源压缩

        ①设置图片压缩格式

        第一步、设置项目的游戏平台

        设置方式:File -> Build Setting -> 在Platform中选择平台 -> 点击Switch Platform

        (这个步骤可能会花点时间)

        第二步、设置图片的压缩格式

        将图片导入到Unity后,单击该图片,在Inspector窗口中可以看到图片的详细设置

博主常用的设置方式:

        Texture Type:给UI用的图片,选择 Sprite(2D and UI)

        Sprite Mode:选择 Multiple

        sRGB: 勾选即可

        Alpha Source:如果图片需要保留透明通道,选择Input Texture Alpha

        

        平台压缩格式设置:最底下的功能栏可以选择平台(例举Android平台)

        Override For Anfroid:勾选

        Max Size:看情况勾选,小图一般512*512,大图1024*1024,特大图2048

        Format:使用RGB(A) Compressed ASTC

                RGBA类型的图片最大只能设置压缩为 5 * 5 (数字越大,压缩的越狠)

                RGB类型(不含alpha通道)的图片最大只能设置压缩为 6 * 6

        不同平台下的图片压缩格式自行设置,但是记得要开启

具体设置方式还是需要根据项目来定

        ② 打图集:将零散的图片打包成一张图集,相当常见的优化手段

图集的好处很多:

        加载快,压缩率高,并且方便资源管理

打入到图集中的图片,宽高需要额外注意:

        打包图集相当于堆叠俄罗斯方块,每个“方块”都需要规范,才能在有限的图集空间内容纳更多图片。因此图片尽量做成正方形,如果不能做成正方形,那么图片的 width 和 height应该尽量满足 2 的幂次方。

        3、模型资源压缩

建模师制作一个模型(以.fbx举例)时,应当先约法三章:

        ①控制骨骼节点数的上限

        ②明确模型的种类:

                例如人体模型可以将其分为:成男、成女、少男、少女、幼男、幼女

                分类好各种模型,开发过程中可以减少额外的沟通成本,方便控制工作流程。

        ③统一模型的结构:(商业游戏项目一般都带有一套流水线工具)

                将美术做好的资源导入进Unity也是流水线的一部分,比较常用的就是AssetPostprocessor类( AssetPostprocessor - Unity 脚本 API )。

                资源导入工具负责判断资源种类,针对不同种类的资源挂载预设好的组件或脚本。例如模型导入进Unity后,需要根据FBX文件,自动生成相应的Prefab,同时挂上Animator组件、模型控制脚本,这就需要确保模型的整体结构是统一的。

                这类工具是为简化工作流程,避免将人力资源浪费在重复的体力劳动上,

                例如下边这个模型:

                        renderer 负责挂载蒙皮骨骼渲染器(skinned mesh renderer)

                        root 负责挂载模型的骨骼节点

                        mgs 为保留的空节点,可以根据实际项目需求挂载其他东西

        ①通过设置模型的网格大小,实现模型压缩

        ②点击模型的fbx文件,在Inspector窗口找到Rig模块,勾选Optimize Game Objects

        勾选了Optimize Game Objects后,FBX中的骨骼节点,如果只有Transform组件,会被剔除不导入。如果需要某些骨骼节点不被剔除,例如需要挂点,则需要在Extra Transform Paths中勾选对应的骨骼名称。
        注意:不剔除的节点会被移到根节点之下,因此代码中不能通过原有路径查找(transform.Find(“Bip001/Bip001 Spine”))。
        原理:Unity会将骨骼信息映射到avatar中,这样,unity在更新骨骼矩阵时,不再考虑场景中的Transform节点,也不用更新它的坐标,而是直接通过获取avatar骨骼信息来更新蒙皮,表现动画,从而节省了cpu计算。

        4、场景压缩与优化

        ①场景压缩:(没有实践过,只记录下思路)

        如果场景是由若干小物件拼成的,并且数量比较大,物件种类统一(比如都是小方块),可以选择对场景内的gameobject进行压缩,保留gameobject在场景中需要绘制的图像信息,再清除掉这些gameobject。
            优点:场景体积可以下降很多,因为很多小物件的gameobject都销毁了,场景只存储的这些物件的图像信息
            缺点:由于没有gameobject,自然也没法光照反射等光线效果,如果需要有灯光,得先挂上灯光,再进行场景压缩,保留光照的效果。(只能支持点光源)

        ②场景优化:

        对于稍大的场景可以使用BVH树(层次包围体),在一定距离内再开始绘制场景资源,动态加载场景。

        场景内一些小物体可以选择动态加载,比如树木、草和一些小模型,在场景load finish后再创建和实例化(create and instantiate)。没必要全部放在场景中。

        给场景物体打上tag或划分layout,例如“height render”和“low render”,需要提高性能而低画质时,场景内的camera可以选择忽略“height render”标签的物体,减轻性能压力。

        5、光照相关

在Unity中,常常会碰到场景里错综复杂,光照信息众多,而导致性能明显降低
Unity2021版本,URP管线的灯光有三种模式:realtime(实时),mixed(混合),baked(烘培)。
        其中realtime实时光非常消耗性能,但是可以产生实时光照效果;烘培光最节省性能,能提供静态烘培;而mixed混合光照,鉴于两者之间。

        针对灯光进行优化。也就是我们将会把灯光信息,烘培到灯光贴图里。这个操作可以直接在unity里完成。
        如果要用实时光照,尽可能的用一种光源。
        如果要用实时光照,又不会只有一种光源,尽可能的使用延时渲染或其它自定义的渲染管线(轻量级渲染)来代替向前渲染。因为向前渲染每个光源都要绘制一次,性能影响很大。

        可以考虑使用贴图来做物体的阴影,代替实时阴影

        6、打包优化

        ①AssetBundles打包时勾选DisableWriteTypeTree    

AB打包参数 BuildAssetBundleOptions.DisableWriteTypeTree。

会极大缩减AB包大小,同时也带来运行时SerializaFile 相同数量级下内存占用缩减一半左右,并提高加载速度。

官方说明比较简单:如果开启DisableWriteTypeTree选项,则可能造成AssetBundle对Unity版本的兼容问题。

        ②打包的时候,尽量将资源打进AB包内,Resources文件夹下只放必要的资源文件 

        ③记得打图集

        ④抗锯齿数记得设置

        ⑤最大阴影距离记得设置

        ⑥后处理记得开

        ⑦渲染精度记得设置

        7、额外补充

        ①Texture、Model或者Partical,如果身上有用到mesh时,其实是可以对这个mesh的isReadable(设置可读写,对应inspector面板的Read/Write Enabled参数)进行设置的
        如果游戏在运行时,需要针对这个mesh进行替换什么的,就将isReadable = true
        如果在游戏运行时,这个mesh不会涉及到变动,那么就将isReadable = false
        启用 Read/Write会导致纹理占用两份内存,一份在GPU端,一份在系统内存中。因为启用 Read/Write说明CPU端逻辑程序可能在运行时读取或修改纹理像素信息(GetPixel32,SetPixel32),
        像素信息必须在系统内存中保留一份。而在项目中,绝大多数的图片都是只供GPU渲染引用的,所以如果没有特殊需要就禁用Read/Write。

 以下是设置某模型所引用mesh的isReadable,直接提供mesh的资源地址也一样

// 模型路径的资源路径为path
// 假设模型身上挂的是ParticleSystemRenderer(粒子系统渲染器)
// ParticleSystemRenderer.mesh != null
// 设置该mesh的isReadable

public void SetModelRW(string path, bool isReadable){
    GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path) as GameObject;
    foreach (ParticleSystemRenderer particle in prefab.GetComponentsInChildren<ParticleSystemRenderer>(true)){
    {
        EnableMeshRW(particle, isReadable);
    }
}

private void EnableMeshRW(ParticleSystemRenderer particle, bool isReadable){
    Mesh mesh = particle.mesh;
    if (mesh == null)
    {
        return;
    }
    string meshPath = AssetDatabase.GetAssetPath(mesh);
    ModelImporter modelImport = AssetImporter.GetAtPath(path) as ModelImporter;
    if (modelImport == null)
        return;
    if (modelImport.isReadable == isReadable)
        return;
    modelImport.isReadable = isReadable;
    modelImport.SaveAndReimport();
}



        ②MipMaps:mipmap主要是用在3D场景中的模型贴图,渲染底层会根据物体在屏幕中呈现的实际渲染大小选择适当的一级mipmap。
            如果相机的远近距离对某些物体渲染到屏幕的大小没有任何影响(如正交相机),那么这些物体的贴图就应该禁用mipmaps,
            最典型的就是UI界面的贴图。这几乎可以降低接近1半的贴图内存占用。

三、代码相关优化

        代码方面的优化,例举几个在日常开发过程中能时刻注意到的简单方法

        1、减少频繁的数据调用

        2、string类型的变量尽量减少 += 的次数

        3、事件,回调等逻辑记得用完就销毁

        4、instance(单例)虽好,不要多用,容易造成数据持久化管理出错,逻辑混乱,销毁不及时,多模块同时操作一个全局变量等问题,最好遵循“谁创建谁释放”

        5、善用池化思想

优化讲究的就是水磨功夫

多关注开发过程中的小细节,优化自然而然就做起来了

写累了,下次再更……

  • 44
    点赞
  • 58
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不伤欣

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值