2020-09-16

Unity 10个游戏开发代码优化小技巧

此文章是作者从知乎借鉴而来,只是希望能学习一下,分享给大家,勿喷
1、通过合理的计算顺序来减少矢量操作次数
来看下面这几行代码:

Vector3 vec = new Vector3(1, 2, 3);
Vector3 nvec = vec * 3f * 2f;
Vector3 与 float 相乘,实际上是 Vector3 中的三个分量分别与 float 相乘,也就是进行了 三次乘法运算,然后返回乘后的 Vector3

那么对于上面的代码,vec 与 3f 相乘,进行了 3次 乘法运算,然后得到一个临时的 Vector3 我们称其为 temp。 然后 temp 与 2f 相乘,又进行了 3次 乘法运算。那么总共就进行了 6次 乘法运算。 但我们可以通过调整运算顺序来减少乘法次数:

Vector3 vec = new Vector3(1, 2, 3);
Vector3 nvec = vec * (3f * 2f);
这里我们只是加了一个括号,让 3f * 2f 先进行运算。那么这行代码的运算过程就是这样的:首先计算 3f * 2f 只需要一次乘法运算 得出 6f,然后计算 vec * 6f 需要三次乘法运算,得出最终结果。那么总共进行了 4次 乘法运算,与之前的 6次 相比减少了 2次 乘法运算。

这里我做了简单的测试:
在这里插入图片描述
在这里插入图片描述
结果显示为,在 100000 次运算下,两者运算时间差了 6ms

2、缓存 transform
故名思意,就是我们在 MonoBehavior 中可以先把物体的 transform 缓存一下,之后使用这个缓存的 transform 指针。这里直接给出测试代码和结果:

不缓存时:

在这里插入图片描述
在这里插入图片描述

缓存后:
在这里插入图片描述
在这里插入图片描述

可以看出,在每帧 100000 次计算下,缓存后每帧运算时间优化了近 20ms。

原文并没有解释缓存后性能得到优化的原理。个人猜测是因为过多的继承关系,导致直接获取 transform 指针较慢。

3、尽可能使用localPosition 而不是 position
原因是因为在 transform 中只存储和计算了 localPosition,而在获取 position 时,会根据当前物体的层级结构来一层一层地向上进行坐标转换计算,直到最终转换为世界坐标。

那么,如果该物体所在层级越深,获取 position 时 计算量就越大。下面给出测试代码和结果:

层级:
在这里插入图片描述
直接使用 position:

在这里插入图片描述
在这里插入图片描述

使用 localPosition:

在这里插入图片描述

在这里插入图片描述
可以看到,在拥有 11 个父物体的层级下,每帧计算 100000 次,使用 localPosition 比 使用 position 优化了 70ms 左右。

同样的道理,使用 localRotation 也能起到优化效果。

4、减少引擎调用
这里给出的例子是,我们对 transform.localPosition 进行修改时,不直接修改,而是缓存一份 transform.localPosition,然后对缓存进行修改,再把缓存值给 transform.localPosition

下面给出测试代码 和 结果:

缓存前:

在这里插入图片描述
在这里插入图片描述
缓存后:

在这里插入图片描述
在这里插入图片描述

在每帧 100000 次计算下, 缓存后 比 缓存前 优化了 10ms 左右

同样,缓存 localRotation 也能起到优化效果

原文并没有给出这一优化的原理,本人水平有限,也没有想出合适的解释。

5、不使用 getter 和 setter
getter 和 setter 也就属性。下面给出测试代码 和 结果:

使用属性:

在这里插入图片描述
在这里插入图片描述

不使用属性:
在这里插入图片描述
在这里插入图片描述

在 每帧计算 100000 次的情况下,不使用属性 比 使用属性 优化了 4ms 左右。

原文这里仍没有给出解释。这里本人猜测为 属性对于字段又封装了一层,导致获取字段值是会慢一点。

即使用属性,会隐式调用方法,进行栈操作。

同时还要注意,原文指出,在 mono 下 这样做是有优化效果的,但是在 il2cpp 下反而运算更慢了。

6、不要使用矢量运算符
这里说的是 vec1 += vec2 要比 vec1.x += vec2.x; vec1.y += vec2.y; vec1.z += vec2.z; 的效率要慢。
即 两个矢量直接计算,比我们把矢量拆开分别对分量进行计算要慢。下面给出测试代码 和 结果:

矢量直接相加:

在这里插入图片描述
在这里插入图片描述

矢量的分量对应相加:

在这里插入图片描述

在这里插入图片描述

可以看出,在每帧 100000 次运算下,把矢量拆分对分量分别进行计算会优化近 5ms。

在原文中,对于这一原理的解释为 Vector3 之间直接进行运算时,会在堆上面分配出一个临时的Vector3 变量。 而对分量直接进行计算,就是直接在分量上进行修改,不会产生临时变量。

个人觉得这一说法不太准确,矢量直接运算确实会封装出一个临时变量,但是矢量作为一个结构(struct) 这个临时变量应该是在栈上分配的。

如果觉得每次向量间进行计算,都要分成三行来写非常麻烦,可以封装出一个方法:

在这里插入图片描述
这是大神写的一个扩展方法,因为用了 ref 关键字进行修饰,所以不需要额外封装出一个矢量进行返回。

但因为使用时调用了拓展方法,仍有一定性能损失。每帧进行 100000次 运算情况下,其会比直接写三行代码要慢 2~3 ms

至于为什么封装之后仍慢 2~3ms,因为向量间直接运算会结果封装出一个新的向量,这是在栈上生成的。 而把向量分量进行计算避免了栈操作。

而封装出方法在调用时要进行出栈操作,所以相比下仍要慢一点点。

7、缓存 Time.deltaTime
这里先直接给出测试代码 和 结果:

缓存 Time.deltaTime 之前:

在这里插入图片描述
在这里插入图片描述

缓存 Time.deltaTime 之后:

在这里插入图片描述
在这里插入图片描述

可以看出,在每帧 100000 次运算下,缓存 Time.deltaTime 后,优化了 20ms 左右

原文没有给出该优化原理的解释。个人猜测为,每次获取 Time.deltaTime 时 Unity 都会重新计算一次 deltaTime,但这是没有必要的,因为我们只要确保 deltaTime 一帧能更新一次即可。

8、不要使用 foreach 循环
因为 foreach 会产生垃圾回收(GC)。

然而在本人实际测试中,foreach 并没有产生 GC…

我们创建 10 个物体并添加一个脚本,然后在另一个脚本中,每帧遍历 10000 次这 10个物体,并调用它们身上脚本的方法。

下面给出测试代码 和 结果:

使用 foreach 循环:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
使用 for 循环:
在这里插入图片描述
在这里插入图片描述

结果发现并没有产生明显优化。 后来本人又实验了几次,发现 for 循环还是稳定地要比 foreach 快的。

并且发现 foreach 并没有产生 GC。之前我一直觉得 foreach 会产生 GC 是基础常识,结果发现竟没有 GC 也是吃了一惊。于是又做了一些测试,下面是结果:
遍历 数组、列表、字典、哈希表,都没有 GC

不知道是我地测试代码有问题,还是时代变了…

补充: foreach 产生 gc 的问题,在之前版本已经解决。

9、使用 Array(数组),而不是 List
原文说 List 和 数组 相比,使用下标访问时,List 是从头遍历到 对应下标位置,时间复杂度为 O(n),而数组为 O(1)

这个说法是有误的。但是从下标访问的速度而言,数组 确实比 List 要快。

如果 List 的下标访问,其 时间复杂度为 O(n),那么 访问 头元素,和 访问 尾元素的时间应该是不一样的。但经过测试,访问 头、尾 和 中间的元素,所用时间都是一样的。但是也都比 数组访问要慢。

下面给出 测试代码 和 结果:

在这里插入图片描述

在这里插入图片描述

在计算 100000次 的情况下,List 的下标访问会比 数组慢 2~3 ms

这给了我启发:

在一些集合初始化时,我们无法确定集合的大小,因此会选用 List,但是在初始化完毕之后,我们一般不会再向里面添加新的元素。

例如一些配置文件的初始化。那么我们就可以把这个 List 再转换为数组,这样在其它地方进行下标查找时会更快。

10、尽量避免使用 Update 和 FixedUpdate
原文提到 Unity 的 Update 和 FixedUpdate 存在性能问题,应当避免使用。

那避免之后,一些需要每帧调用,或者一些计时任务该怎么办呢?

那就只能自己实现一个计时回调器了。关于计时回调器的实现,足以另起几篇文章来讲,因此这里不作赘述。

最后总结:
通过合理的计算顺序来减少矢量操作次数,一维向量先运算完毕之后再与 多维向量进行运算。
缓存 transform
尽可能使用本地坐标下的变量,例如 localPosition 和 loaclRotation,而不是直接使用 position 和 rotation
减少引擎API的调用,例如 缓存 transform.localPosition 和 transform.loaclRotation
减少使用 getter 和 setter 即属性
不使用矢量运算符,而是对矢量的分量进行一一运算。
缓存一个全局的 deltaTime,保障其每帧会更新一次即可,然后其它脚本共用这一个 deltaTime。
不要使用 foreach,而是使用 for
如果 List 初始化完毕之后,以后不会再向里面添加元素,那么最后把它转换为 数组。
尽量避免过多的 Update 和 FixedUpdate 存在。可以自行实现计时回调器。
最后的最后
Unity 最近的几个实验功能,其里面的代码都使用到了 Job System 和 Brust编译器。
所以 Unity 之后的内部优化路线应该就是通过底层多线程调度来提高运行效率。

在最后,原文也指出了,在面向对象这一概念产生之后,所形成的代码规范和习惯,对于游戏开发来说,在性能上都是比较低效的。但是面向对象对于开发的效率而言是无需言喻的。

所以,要做出性能最佳的游戏程序,就要跳回到面向对象产生之前的编码方式。

这一思路便诞生了 ECS 框架,即面向数据而非面向对象。

现在 Unity 也提高了现成的 ECS 框架。 但我觉得使用 ECS,就意味着代码更加 难以书写、臃肿 和 难以阅读。且需要做大量的底层重构。

因此在开发前要明确好具体的框架使用。如果使用面向对象的框架来开发,那么为了开发效率,我觉得可以按照面向对象的传统习惯来编码,功能实现完毕之后再对照优化技巧来修改代码。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值