【Unity游戏开发】马三的游戏性能优化自留地

摘要:
自游戏开发以来,我们没有进行任何系统的性能优化。最近,出于各种原因,我们需要优化游戏。其他同事都有开发任务,因此性能优化的任务落在马三身上。老实说,马三在性能优化方面没有太多经验,他总是咨询前任,并利用在线信息了解如何渡过难关。但现实给了马三一个残酷的打击。XCode报告了“无法启动”的警告消息。

一、简介

  很久没有更新博客了,最近马三比较忙,一直在处理游戏中优化相关的事务。我们的游戏自从开发以来一直没有做过比较系统的性能优化,最近因为各种原因需要对游戏进行优化,其他同事都有开发任务,因此性能优化的任务就落在了马三身上,说实话马三在性能优化方面也没有太多的经验,都是不断地咨询前辈并且结合网上的资料摸着石头过河。本篇博客中马三就和大家分享一些优化过程中的心得体会,顺便记录一下方便自己日后查阅。

二、优化

  1.闪退问题排查

  这次优化是因为手机频繁地闪退引起的,我们测试机用的是 iPhone11 Pro 和 iPhone11 Pro Max (博主写下这篇博客的时间是2020年),此时这两款手机可以说是市面上性能最强劲的两款手机了,但是我们的游戏最近跑在上面却频繁地闪退。虽然马三比较有先见之明地接入了Bugly SDK,并且在Bugly控制台上也捕获到了闪退信息,而且进行了符号表解析,但是Bugly上仅仅有下面这张图这样一个简单的堆栈信息,并不能看出具体是因为什么引起的闪退。

【Unity游戏开发】马三的游戏性能优化自留地第1张

   此时就需要进行iOS真机调试了,当马三准备真机调试的时候才发现我们打包机的XCode版本是10.x,而我们的测试机的版本是iOS13.4.1,XCode版本太低并不能直接调试。马三按照网上的教程去下载了真机调试包,然后放在指定目录下。但是并没有什么作用,又试了网上各路大神提供的各种奇巧淫技,最后都没能奏效。后来只能老老实实地对XCode进行了升级操作,在升级XCode之前还需要把Mac系统升级到最新,真是蛋疼。不过最后全部升级完以后,去尝试连接真机,果然一瞬间就连上了。此时我突然想起了前辈说过的一句话“没事千万别跟软件较劲”,的确软件该升级就升级,别跟软件较劲,吃力不讨好。

  在升级完MacOS系统和XCode软件版本,并且能够连接上真机以后,马三满心期待地点下了Build for running的按钮,并且自信地以为马上就可以看到分析器信息了。但是现实又给了马三残酷的一击,XCode报了一个can not launch的警告信息。马三又是去网上一通查找,把那些方法都试了一遍,还是没有解决问题。后来我怀疑是苹果证书的问题,我们是企业证书,我一度怀疑企业证书打的包不能进行真机调试。后来请教了快手的iOS开发前同事以后,得知了企业证书也可以真机调试,我们这个企业证书不能真机调试的原因很可能是这个企业证书是发布证书,不是Development证书,因此打出的包无法进行调试。时值周六,IT部门并没有上班,因此想去弄一个开发版的企业证书也不现实。后来马三忽然想起苹果现在可以免费申请iOS开发者账户了,于是乎赶紧去申请了一个,然后顺利的打出了包并且进行了真机调试,打开instrument一分析,最后定位到闪退是limit memory of 2GB上面,也就是应用程序占得内存太多了,导致被系统杀死了。

【Unity游戏开发】马三的游戏性能优化自留地第2张

   2.ShaderLab内存占用量优化

  知道闪退是什么原因导致的,就知道了去向着什么方向进行优化了,内存占用高就去降低内存峰值就好了,连接上了Profiler后,马三一看,好家伙,ShaderLab占用了630MB的内存,按理来说Unity游戏中ShaderLab的内存占用量在40MB上下才是比较合理的,我们这个直接顶到了630MB,不崩溃才怪了。ShaderLab的占用量一般和Shader变体数量有关系,变体数量多的话,编译Shader就需要更长的时间并且占用更多的内存。但是咨询过TA以后,说我们游戏还是DEMO期,并没有使用到很多的Shader,但是为什么分析器中还显示占用了这么多内存呢?马三决定写个Shader变体数量收集统计小工具,批量查询一下游戏中的Shader的变体数量,康康到底的是怎么回事。工具的原理很简单,就是收集项目中所有的Shader文件,然后依次对他们执行通过反射拿到的UnityEditor.ShaderUtil.GetVariantCount方法,获取到变体数量,然后输出到csv文件就好了,csv文件可以用Excel工具打开,可以利用Excel按照变体数量进行排序,然后从高到低逐个优化。

[MenuItem("Tools/AAAAAAAAAAAA")]
    public static void GetAllShaderVariantCount()
    {
        Assembly asm = Assembly.LoadFile(@"D:UnityUnity2018.4.7f1EditorDataManagedUnityEditor.dll");
        System.Type t2 = asm.GetType("UnityEditor.ShaderUtil");
        MethodInfo method = t2.GetMethod("GetVariantCount", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
        var shaderList = AssetDatabase.FindAssets("t:Shader");
 
        var output = System.Environment.GetFolderPath(System.Environment.SpecialFolder.DesktopDirectory);
        string pathF = string.Format("{0}/ShaderVariantCount.csv", output);
        FileStream fs = new FileStream(pathF, FileMode.Create, FileAccess.Write);
        StreamWriter sw = new StreamWriter(fs, Encoding.UTF8);
 
        EditorUtility.DisplayProgressBar("Shader统计文件", "正在写入统计文件中...", 0f);
        int ix = 0;
        sw.WriteLine("ShaderFile, VariantCount");
        foreach(var i in shaderList)
        {
            EditorUtility.DisplayProgressBar("Shader统计文件", "正在写入统计文件中...", ix/shaderList.Length);
            var path = AssetDatabase.GUIDToAssetPath(i);
            Shader s = AssetDatabase.LoadAssetAtPath(path, typeof(Shader)) as Shader;
            var variantCount = method.Invoke(null, new System.Object[] { s, true});
            sw.WriteLine(path + "," + variantCount.ToString());
            ++ix;
        }
        EditorUtility.ClearProgressBar();
        sw.Close();
        fs.Close();
    }

  通过上面的工具一查,好家伙,有一个Shader竟然有18.4K个变体,这个数量真是惊人。再次与TA进行沟通了以后,发现这个Shader里面有相当一部分的关键字并没有用到,都是以前遗留下来的,能用到的KeyWord就5个左右。众所周知,Shader的变体数量和关键字数目有关,一般来说一个Shader中的关键字每增加一个,该Shader的变体数量就会x2,是成几何裂变的方式去增加的,着实恐怖啊!赶紧让TA把没用的关键字去掉了,然后再次打包进行观察,发现ShaderLab的占用量一下降低到峰值为260多MB了,是小了不少,但是这个占用量依然不合理,还是太多了。但是此时再次通过上面的工具去排查变体数量,却发现并没有变体数量特别多的Shader了,这时该如何下手呢?幸好 夜莺 大佬给了我一个指点,ShaderControl这个插件可以查看Shader的变体数量,冗余关键字、查看哪些材质引用了这个Shader的哪些关键字。

  用ShaderControl插件一看,发现实际打进包中的Shader变体数量和在Editor下预览的还是不一样的,实际打进包中的变体数量要多于Editor下预览的,并且发现了有很多隐藏的关键字,这些关键字其实根本没有显式的引用,但是却在打包的时候出现了,并且增加了Shader变体数量。后来经过请教之前的快手TA前辈,才发现原来Unity关键字有个坑:

  材质球里会记录之前使用的关键字,打个比方 :“A Shader 使用的关键字 _WOYAOKAIFEIJI ,B Shader使用率关键字 _WOYEYAOKAIFEIJI ,材质球C 开始使用了Ashader ,那么会把_WOYAOKAIFEIJI这个关键字记录在材质球,由于某个原因这个材质球不想使用Ashader了,这是切换到B Shader ,那么这个材质球就会包含_WOYAOKAIFEIJI、_WOYEYAOKAIFEIJI这两个关键字”。

  比如下图中的这个关键字,完全没有材质在显式地使用它,但是就可以搜出来有用了这个关键字的材质。然后打开对应材质球的debug面板,可以发现shader keywords这一栏记录了以前的残留

【Unity游戏开发】马三的游戏性能优化自留地第3张

【Unity游戏开发】马三的游戏性能优化自留地第4张

  后来又跟TA优化了一些没用的关键字,并且通过ShaderControl的Clear All Material功能,批量清除了材质球中残留的关键字。之后再一打包测试,发现ShaderLab的内存占用量降到了62MB左右,这次应用再也不会因为内存暴涨的问题闪退了。折腾了近一周的时间,终于把ShaderLab的内存占用量从630MB降到了62MB左右。

  经过这次粗犷地优化以后,我们的游戏竟然可以运行在iPhone6s上了,在战斗场景还能流畅地跑到30帧左右,主程连续说了三次不错不错,牛逼啊之类的词汇来表达心里的激动之情。

  3.UI界面的ShaderLab占用竟然比战斗场景内的高

  出于提升界面的效果的目的,我们的UI界面上也开了后效处理。与战斗内的后效处理相比,UI界面上的后效处理较为简单,并且没有用到像战斗内那么多的Shader。所以期望的结果是UI场景中的ShaderLab内存是小于战斗内的ShaderLab内存的,但是我在通过Profiler进行分析的时候,却发现恰恰相反,UI界面占用的ShaderLab内存竟然比战斗内的占用还要高。

  后来经过进一步的排查发现,UI界面上有个隐藏的Chessboard节点,他上面挂载了一个战斗内的人物角色,这个人物的Avatar使用了很多战斗内的Shader,用到了这些Shader就需要编译,就需要占用内存。并且这个界面上用了非常多的Standard Shader,这个也是非常耗的。因此UI场景占用的ShaderLab实际上就为 UIShader+战斗内Shader > 战斗内Shader。所以就出现了UI界面占用的ShaderLab内存竟然比战斗内的占用还要高的奇怪现象。经过与制作这个UI的程序沟通,确认这个节点是很久之前测试用的,忘记删除了,把它移除掉以后,UI场景的Shader占用就正常了,大概才3MB左右。因此很有必要在实际的项目中制定标准的UIPrefab制作规范和相应的检查工具,从而避免类似的事情再次发生。

  4.Odin插件的优化

  我们游戏中的很多配置都是通过Odin的SerializedScriptableObject实现的,借助于Odin的可视化界面和强大的序列化、反序列化库,配置游戏的数据非常方便。而且这些数据可以所见即所得,可以在运行的状态下直接修改并且马上就看到效果。但是随着配置规模和数据量变大以后,策划发现每次在改动一些很小的参数的时候都要卡顿半天,非常影响工作效率。后来我开了Profiler的Deep模式对Editor进行了分析,得到了下面的这张图:

【Unity游戏开发】马三的游戏性能优化自留地第5张

  可以看到主要的耗时都发生在了序列化写入这一步,产生了很高的GC,并且这一帧的self time也非常高。再结合Odin的源码进行分析,发现最终的原因就是:“每次在Odin Inspector中修改序列化数据的时候,都会触发ProertyTree重绘,这个重绘里面有个Record UnDo的机制需要保存现场。然后就会触发序列化的行为 会引起很大的GC和耗时。”

   又通过不断地翻看Odin论坛的Issue,我发现有人跟我遇到了一样的问题,并且作者亲自下场给出了答复,截图在下面:

【Unity游戏开发】马三的游戏性能优化自留地第6张

   最后的解决办法很简单,在Odin的Perferences配置面板中将Data formatting options的序列化格式都改成bin就不卡了,如下图所示。当Odin的序列化文件以Node的方式保存的时候,这个GC和耗时就很明显,所以就会卡顿。将Odin的序列化文件改为bin就快了,因为二进制的序列化和反序列化是非常快的。Node这种方式,我盲猜会用递归的方式去处理嵌套的每一个节点,所以GC和耗时比较高。

【Unity游戏开发】马三的游戏性能优化自留地第7张

  5.Json解析的优化

  除了上面所说的Odin的SerializedScriptableObject配置外,我们游戏中还有许多Json格式的配置文件。经过Profiler分析,在批量解析这些Json文件的时候也会产生内存和CPU的峰值,撑大Mono的堆内存。经过进一步的分析,发现是SimpleJson这个插件造成的,我们用的是老版的SimpleJson插件,里面在解析的时候用了很多字符串拼接的操作,众所周知string是不可变的,每次拼接实际上都是产生了一个新字符串出来。解析Json的时候会涉及到大量的字符串拼接,因此GC和CPU峰值也就来了。这个解决办法很简单,我在Github上查看了最新版的SimpleJson插件,其中解析Json部分已经替换成了使用StringBuilder去实现,测试了一下果然效果比之前好了很多,只要把新版和旧版的API接口做一下调整和兼容就可以了,对外接口不变,内部给它翻新一下即可。

三、总结

未完待续...

免责声明:文章转载自《【Unity游戏开发】马三的游戏性能优化自留地》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇天气雷达原理layui与jQuery一起使用下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Unity编辑器扩展

Unity引擎除了提供大部分通用的功能以外,还为开发者提供了编辑器的扩展开发接口,开发者可以编写编辑器脚本,打造适合自己的游戏辅助工具和定制的编辑器。 以前脚本开发中使用的一些API和组件类,都属于运行时类,Unity还提供了编辑器类用于编辑器的扩展开发,包括编辑器环境下使用的GUI类,编辑器工具类,编辑器操作类(例如拖放、撤销操作)等。 注意:编辑器扩展...

unity优化一些总结 (长期更新)

unity优化一些总结 (长期更新) UI: 1:尽量不要使用动态文本 2: 使用更多画布 拆分画布 ​ 我开始使用3幅画布。一个用于我的背景图像,一个用于我的主要UI元素,另一个用于需要放置在其他所有元素顶部的元素。 我了解到,每当画布中的某些内容发生变化时,整个画布都会被重新评估并重新绘制。因此,除了最简单的UI之外,将UI分成多个画布的好处可能非常重...

前端性能优化常用方法

网页内容 1.1 减少http请求次数 1.1.1捆绑文件 通过一些现成的库将多个脚本文件捆绑成一个文件,将多个样式表文件捆绑成一个文件,以此来减少文件的下载次数。 1.1.2CSS Sprites 把多个图片拼成一副图片,然后通过CSS来控制需要显示图片的位置( CSS Sprites Generator) 1.1.3Inline images 通过Ba...

unity替换mesh测试

直接替换SkinnedMeshRender的Mesh,实现所谓断肢效果(不过最近发现,绑定多mesh似乎更好实现这样的效果。有时间准备写一篇): 只要不改变两个Mesh原始文件的层级,就不会出现权重的错乱问题。 权重映射的测试:http://www.cnblogs.com/hont/p/5252535.html...

使用点云数据在Unity中渲染场景

  最近接触了一个用点云数据渲染的方案, 非常给力, 几乎就是毫秒级的加载速度, 特别是在显示一些城市大尺度场景的时候, 简直快的没法形容, 之前的城市场景用了很多重复模型, 并且大量优化之后加载一个城市不仅时间很久, 10分钟级的, 而且内存消耗巨大, 10G级别的, 运行时CPU裁剪都能耗掉40ms, 几乎没有任何意义了...   这个方案好的地方在于...

WPF程序性能优化总结

原文链接:https://blog.csdn.net/u010265681/article/details/77571947 WPF程序性能由很多因素造成,以下是简单地总结: 元素: 1、 减少需要显示的元素数量:去除不需要或者冗余的XAML元素代码. 通过移出不必要的元素,合并layout panels,简化templates来减少可视化树的层次。这可以...