赞
踩
关于 unity 项目相关优化经验的墨迹
总结起来,主要的性能瓶颈在于:
优化使打出来的 assetbundle 更小,有利于减少包体大小 与 热更是的大小
测试过 3d模型 的 tga 与 png, 打出来的ab大小有点不同 , 在没有透明度的情况下 (模型一般都没有透明度需求), 尽量使用 tga 24b
贴图格式 | ab大小 |
---|---|
tga_24b_noalpha | |
tga_32b_noaplha | |
png_32b_hasalpha |
这个实在 3dmax 导出模型时, 如果勾选了 **嵌入媒体 **,会把贴图打进fbx中, fbx大小会变大. 导入unity 中是会自动生成一个 .fbm 文件夹, 里面就包含贴图文件.
经实测, 打出的ab资源, 不够是否嵌入媒体, 大小都是一样的
打包实测
使用默认 fbx 里面的材质球, 打出来的ab最大
不导入 fbx 的材质球, 还是很大
使用自定义的材质球 及 shader, 最小
此时不导入材质球也是最小的, 不需要remap了
总结: 其实就是使用了引擎内置的shader引起的. 所以必须保证使用的是 自己的shader (把引擎内置shader提取出来). 再者使用工具自动检测, 以防止使用引擎内置的shader.
最优的设置是 fbx 中引用自定的材质球, 同时不导入材质
假设有 10 个相同的物体, 使用不同的 材质球 (即使材质球的贴图是同一张贴图), 会有 10 个批次
如果10个物体都使用同一个材质球, 则会被unity合到一个批次渲染. 即使中间有别的物体插入, 也不会打断这个合批, 因为有 zbuff.
需要注意的地方: 在游戏运行时, 如果去get模型的材质球 Renderer.material
, 这个api会造成生成新的一个 材质球 返回, 因此会生成新的一个批次. 正确的做法是, 如果有多个物体都有需求用到 别的材质球, 可以新建一个 新的材质球, 别的物体都使用这个材质球, 这样只要使用这个材质球的模型就会在同一个批次绘制完成
别人的采坑参考: http://www.u3dnotes.com/archives/2267
这是一个最直观且有效的方法,比如在多人游戏中,我们可以减少可见玩家的数量,如果不影响游戏性和玩家体验,那这是就是一个即方便又快捷的方法。
遮挡剔除的原理就是当一个物体被其他物体遮挡住,不在摄像机的可视范围内时不对其进行渲染。具体方法如下所示:
把所有物体选中,Inspector面板中的static下拉菜单中勾选Occlusion Static 和 Occludee Static。
参考总结: unity-遮挡剔除OcclusionCulling.md
我们可以通过摄像机的Clipping Planes 的Far裁剪远端,从而降低摄像机的绘制范围,如同所示:
为了降低性能损耗同时保证游戏质量,Far的值应该合理控制,不要造成不好的游戏体验,或者我们可以用雾来掩盖不被渲染的远端。
可见情况下, 距离远的可以用模型精读低的替代, 减少顶点从 cpu 到 gpu 的传输量. 代价是 内存,包体 会增大
参考总结: unity-LOD优化.md
未开启阴影
开启阴影
会额外多次很多批次
对于静态物体, 烘焙光照图
动态物体, 比如角色, 只用 投影到地面的假阴影, 和 平面阴影
参考总结: unity-合批优化.md
尽量同一个ui上的使用到的图片, 打到一个图集中, 特别是 listview 这种, 如果item 与 item 之间断掉合批的话, 那么 dc 将会比较高.
fbx模型 选项设置
工具: 资源检查 -> 模型优化 -> 所有模型优化
使用到unity内置的shader,提取 (从官网下,路径加上 ITS/ 方便识别 是否是内置shader) 出来到项目中,打成一个ab
工具: 资源检测 -> 检查Shader All
最高压缩率方式 LZMA
:
BuildAssetBundleOptions options = BuildAssetBundleOptions.DeterministicAssetBundle | BuildAssetBundleOptions.ChunkBasedCompression;
常用压缩方式 LZ4
:
BuildAssetBundleOptions options = BuildAssetBundleOptions.DeterministicAssetBundle | BuildAssetBundleOptions.None;
使用 LZMA
包体最小,但解压速度也会变慢一点。
主要就是在 unity 中编写插件工具, 自动生成所需的 prefab 资源
根据美术的需求编写相关工具.
比如我们的关卡场景编辑, 我会先编好一个模板, 然后写个插件工具, 让不同关卡需要不同模型时, 直接往插件上拖动, 然后直接生成一个新关卡, 然后再稍微细调一下.
场景需要的必要元素, 也编写了工具去检查, 防止编辑人员的疏漏.
**1.顶点处理。**顶点处理是指GPU需要渲染网格中每一个顶点的工作。
顶点处理的消耗受两方面影响:必须渲染的顶点数量,以及在每个顶点上要进行的操作数量。
**2.填充率。**填充率是指GPU在屏幕上每秒可以渲染的像素数。如果我们的游戏受到填充率的限制,意味着我们的游戏每帧尝试绘制的像素数量超过了GPU的处理能力。
**3.显存带宽。**显存带宽是指GPU读写其专用内存的速度。如果我们的游戏速度受限于显存带宽,通常可能是我们使用的纹理太大,以至于GPU无法快速处理。
可以通过减少不必要的物体绘制, 也就是 减少要渲染的对象数量
通过 unity 内置的 帧调试器, 可以看出每一帧的绘制情况, 要运行时去调试. 因为运行时才会 static batch.
比如 cube 和 cylinder 期望是需要 动态合批 的, 但是实际上没有合批, 调试提示也很明显了, dynamic batch 开关没有打开.
如果我们的场景包含距离摄像机很远的物体,我们可以通过使用miapmap来缓解显存带宽的问题,mipmap的主要作用便是模型的贴图会根据摄像机距离模型的远近而调整不同质量的贴图显示,以达到优化目的。
这里可以发现图片大小发生了变化,这是因为我们使用MipMap技术之后,会对此贴图生成八张精度质量不同的贴图,所以内存占用变大。
这种方式是以 内存 换 显存
MinMaps 正确使用姿势
有远近区分的物体才需要开启,如 场景中的3d物体,特效等
没有远近区分的不需要开启,如 ui
SkinnedMeshRenderer 组件用于处理骨骼动画,常用在角色动画上。与蒙皮相关的任务可以在主线程或者独立的工作线程中执行,具体取决于游戏设置和目标硬件平台。
蒙皮渲染的开销比较高,下面一些 对蒙皮渲染进行优化的手段:
减少SkinnedMeshRenderer组件的数量。导入模型时,模型可能带有SkinnedMeshRenderer组件,如果游戏中该模型并不会使用骨骼动画,就应该将SkinnedMeshRenderer组件替换为MeshRenderer。在导入模型时,可以选择不导入动画,请参考模型导入设置。
减少使用SkinnedMeshRenderer的对象的Mesh顶点数,参考蒙皮渲染器手册。
使用GPU Skinning。在硬件平台支持并且GPU资源足够的条件下,可以在Player Setting中启用GPU Skinning,将蒙皮任务从CPU转移到GPU。
更多优化内容请参考 角色模型优化手册
编写工具检测 ab资源 是否有冗余
管理好 ab及其加载出来的资源和场景对象上的引用, 在合适的时候适当触发gc回收掉. 比如 要加载一个内存占用大的资源时, 可以尝试释放已经不存在于场景上的资源.
ab模式下查看内存ab占用, 查找出哪些没有在用却又一直占据着内存的资源.
重点优化对象,播放时长较长的音乐文件需要进行压缩成.mp3或.ogg格式,时长较短的音效文件可以使用.wav 或.aiff格式。
减少码率.
查看某个 tag 包裹下的 cpu 消耗.
参考总结: unity-打tag技巧.md
参考总结: q6调试工具总结.md
对应的文档: q6调试工具文档.md
一个相对中大型的游戏,系统非常的多。这时候合理的适时的释放内存有助于游戏的正常体验,甚至可以防止内存快速到达峰值,导致设备Crash。
目前主流平台机型可用内存:
Android平台:在客户端最低配置以上,均需满足以下内存消耗指标(PSS):
1)内存1G以下机型:最高PSS<=150MB
2)内存2G的机型:最高PSS<=200MB
iOS平台:在iPhone4S下运行,消耗内存(real mem)不大于150MB
中端机子
顶点小于10w个, dc 小于150
1.PC平台的话保持场景中显示的顶点数少于200K~3M,移动设备的话少于10W,一切取决于你的目标GPU与CPU。
2.如果你用U3D自带的SHADER,在表现不差的情况下选择Mobile或Unlit目录下的。它们更高效。
3.尽可能共用材质。
4.将不需要移动的物体设为Static,让引擎可以进行其批处理。
5.尽可能不用灯光。
6.动态灯光更加不要了。
7.尝试用压缩贴图格式,或用16位代替32位。
8.如果不需要别用雾效(fog)
9.尝试用OcclusionCulling,在房间过道多遮挡物体多的场景非常有用。若不当反而会增加负担。
10.用天空盒去“褪去”远处的物体。
11.shader中用贴图混合的方式去代替多重通道计算。
12.shader中注意float/half/fixed的使用。
13.shader中不要用复杂的计算pow,sin,cos,tan,log等。
14.shader中越少Fragment越好。
15.注意是否有多余的动画脚本,模型自动导入到U3D会有动画脚本,大量的话会严重影响消耗CPU计算。
16.注意碰撞体的碰撞层,不必要的碰撞检测请舍去。
Unity都要去遍历所有的组件来找到目标组件。每次都去查找是不必要的耗费,我们可以通过缓存的方式来避免这些不必要的开销
所以当游戏内对象很多时,这个函数将很耗时
或者采用GameObject.FindWithTag
来寻找特定标签的对象。如果能在一开始就确定好对象,可以通过Inspector注入的方式,将对象直接拖到Inspector中,从而避免了运行时的查找
Camera.main
用来返回场景中的主相机,Unity内部是通过GameObject.FindWithTag
来查找tag为MainCamera
的相机。
当需要频繁访问主相机时,我们可以将其缓存以获得性能提升
GameObject.tag
常用来比较对象的tag,但是直接采用.tag ==
来进行对比的话,每一帧会产生 180B GC Alloc。通过GameObject.CompareTag
来进行比较则可以避免掉这些GC
大量的MonoBehaviour
的Update
需要执行时,在profiler中可以看到它们的耗时很高。因为在MonoBehaviour
内部调用Update
时需要做一系列检查,如下图所示
MonoBehaviour管理器,里面维护一个List,然后将这些需要调用Update
的MonoBehaviour扔进List中,并将它们的Update
函数改成其他名字,比如MonoUpdate
。然后在这个管理器的Update
函数中循环遍历所有的MonoBehaviour调用它们的MonoUpdate
。结果可以获得数量级上的提升
每次调用Transform.SetPosition
或Transform.SetRotation
时,Unity都会通知一遍所有的子节点。
当位置和角度信息都可以预先知道时,我们可以通过Transform.SetPositionAndRotation
一次调用来同时设置位置和角度,从而避免两次调用导致的性能开销
Animator
提供了一系列类似于SetTrigger
、SetFloat
等方法来控制动画状态机。例如:m_animator.SetTrigger(“Attack”)
是用来触发攻击动画。然而在这个函数内部,“Attack”
字符串会被hash成一个整数。如果我们需要频繁触发攻击动画,我们可以通过Animator.StringToHash
来提前进行hash,来避免每次的hash运算
private static readonly int s_Attack = Animator.StringToHash(“Attack”);
m_animator.SetTrigger(s_Attack);
与Animator
类似,Material
也提供了一系列的设置方法用于改变Shader。例如:m_mat.SetFloat(“Hue”, 0.5f)
是用来设置材质的名为Hue的浮点数。同样的我们可以通过Shader.PropertyToID
来提前进行hash。
private static readonly int s_Hue = Shader.PropertyToID("Hue");
m_mat.SetFloat(s_Hue, 0.5f);
如果需要比较距离,而非计算距离,用SqrMagnitude
来替代Magnitude
可以避免一次耗时的开方运算。
在进行向量乘法时,有一点需要注意的是乘法的顺序,因为向量乘比较耗时,所以我们应该尽可能的减少向量乘法运算。
// 耗时:73ms
for (int i = 0; i < 1000000; i++)
Vector3 c = 3 * Vector3.one * 2;
// 耗时:45ms
for (int i = 0; i < 1000000; i++)
Vector3 c = 3 * 2 * Vector3.one;
可以看出上述的向量乘法的结果完全一致,但是却有显著的耗时差异,因为后者比前者少了一次向量乘法。所以,应该尽可能合并数字乘法,最后再进行向量乘。
当需要实现一些定时操作时,有些同学可能会在Update
中每帧进行一次判断,假设帧率是60帧,需要定时1秒调用一次,则会导致59次无效的Update调用。
用Coroutine则可以避免掉这些无效的调用,只需要yield return new WaitForSeconds(1f);
即可。当然这里的最佳实践还是用一个变量缓存一下new WaitForSeconds(1f)
,这样省去了每次都new的开销
SendMessage
用来调用MonoBehaviour的方法,然而其内部采用了反射的实现机制,时间开销异常大,需要尽量避免使用。
可以用事件机制来取代它。
众所周知,输出Log是一件异常耗时,而且玩家感知不到的事情。所以应该在正式发布版本时,将其关闭。
Unity的Log输出并不会在Release模式下被自动禁用掉,所以需要我们手动来禁用。我们可以在运行时用一行代码来禁用Log的输出:Debug.logger.logEnabled = false;
。
不过最好采用条件编译标签Conditional
封装一层自己的Log输出,来直接避免掉Log输出的编译,还可以省去Log函数参数传递和调用的开销。具体可以参见:Unity3D研究院之在发布版本屏蔽Debug.log输出的Log。
少用foreach,因为每次foreach为产生一个enumerator(约16B的内存分配),尽量改为for.
字符串连接会导致GC Alloc,例如string gcalloc = "GC" + "Alloc"
会导致"GC"
变成垃圾,从而产生GC Alloc。又比如:string c = string.Format("one is {0}", 1)
,也会因为一次装箱操作(数字1
被装箱成字符串"1"
)而产生额外的GC Alloc。
所以如果字符串连接是高频操作,应该尽量避免使用+
来进行字符串连接。C#提供了StringBuilder
类来专门进行字符串的连接。
I2LCPP是Unity提供的将C#的IL码转换为C++代码的服务,由于转成了C++,所以其最后会转换成汇编语言,直接以机器语言的方式执行,而不需要跑在.NET虚拟机上,所以提高了性能。同时由于IL的反编译较为简单,转换成C++后,也会增加一定的反汇编难度。
IL2CPP的C++代码虽然是自动生成的,但是其中间的某些过程也可以被人为操纵,从而达到提升性能的目的。
Lua的默认变量都是全局变量,必须要加上local
修饰才能变成局部变量。
局部变量相对于全部变量有以下几点好处: 1. 读写更快 2. 可以避免不经意的全局变量名污染 3. 在作用域结束时,会被自动标记为垃圾,避免了内存泄漏
所以,虽然Lua的默认变量声明都是全局变量,我们还是应该将其用local
修饰为局部变量。
如lua中有 update 等频繁的调用函数, 里面有 频繁调用的 全部变量, 内置库, 可以考虑在当前 lua 文件(也就是一个环境中), 用一个 local 变量 作为函数的 upvalue, 去引用 全局变量, 内置库. 减少去全局搜索的消耗.
Lua中的表内部分为两部分:hash部分和array部分。当创建一个空表时,这两个部分都会默认初始化空间为0。随着内容的不断填充,会不断触发rehash。rehash是一次非常耗时的操作,所以应尽量避免之。
如果同时需要创建较多的小表,我们可以通过预先填充表以避免rehash。
与C#类似,在Lua中的字符串连接的代价也很高昂,但是与C#提供了StringBuilder
不同,Lua没有提供类似的原生解决方案。
不过我们可以用table来作为一个buffer,然后使用table.concat(buffer, '')
来返回最终连接的字符串
硬件实现, 软件可以控制打开或关闭, 这个unity自动会处理
参考总结: graphic-前向渲染管线浅析.md 中的 深度测试提前:Early-Z技术
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。