本期我将整理个人所学关于Unity中C#编码方面的一些优化思路,我们的目标是提升代码性能。我将从GC,算法效率,UnityAPI,字符串方面,结合一些实际场景给出性能优化方案。
目录
1.减少使用 Sendmessage( ) / BroadcastMessage( )
4.尽量使用localPosition,localRotation
一.GC优化
1.GC对性能的影响
让我们先来提一下性能杀手之 GC(Garbage Collection)
大家可以去看这篇博客,温习一下GC相关的知识。
来源: Unity优化之GC——合理优化Unity的GC_unity gc alloc-CSDN博客
在学习时看到了其他大佬总结的图示,觉得很有帮助,这里贴出来。
在Unity中,GC触发时的卡顿本质上是主线程被强制暂停以执行内存回收。
GC暂停主线程的底层机制:Stop-The-World机制
当GC启动时,CLR(公共语言运行时)会冻结所有托管线程
主线程(游戏逻辑线程)在此期间无法执行任何游戏代码,严重时会有明显卡顿。
2.GC优化方向
正如博文中所说,垃圾回收主要是指堆上的内存分配和回收,Unity中会定时对堆内存进行GC操作。
(*)注意 堆上的内存大致包括:C#引用类型,协程中的迭代器,委托/事件等。如果是在函数中声明了局部的引用类型对象,当函数执行完毕后,该对象的引用会脱离作用域,但实际对象仍存在于托管堆中。等待GC时清理。【关键点:引用失效 ≠ 内存释放,对象需等待GC回收】
(*)降低GC的影响的方法
大体上来说,我们可以通过三种方法来降低GC的影响:
减少GC的运行次数;
减少单次GC的运行时间;
将GC的运行时间延迟,避免在关键时候触发,比如可以在场景加载的时候调用GC
(*)两种优化策略
1.对游戏进行重构,减少堆内存的分配和引用的分配。更少的变量和引用会减少GC操作中的检测个数从而提高GC的运行效率。
2.降低堆内存分配和回收的频率,尤其是在关键时刻。也就是说更少的事件触发GC操作,同时也降低堆内存碎片。
了解降低GC的思路后,我们大致可以得到一种优化方向:避免高频堆内存分配
3.优化方案
1.场景A
需要(类内)高频或多处调用的引用类型对象;
建议方案:使用实例变量进行缓存避免重复查找。
2.场景B
需要在(局部)函数内部实例化新对象(如子弹预制件等);
建议方案:使用对象池将对象进行缓存循环利用。
3.场景C
高频创建的小型数据(如坐标等) , 需要连续内存访问的结构;
建议方案:优先使用值类型(无GC)。
建议:根据实际需求权衡实例和局部变量以达到优良性能。
二.算法优化
一.算法效率对性能的影响
低效算法直接导致CPU时间占用过长,引发帧率下降和卡顿。
二.算法优化方案
1.减少循环调用
将循环内的条件判断外置,避免无用的循环遍历
2.仅在改变时更新显示
常对于Text等组件,当文本信息不发生变化时无需在Update中更新。
3.增加代码更新的延时
当需要每帧更新的情况
可以增加 Update( )中代码更新的延时。
int interval=3;//帧间隔
Update(){
Time.frameCount%interval== 0 //每间隔3帧更新一次
}
【进一步优化】
在第0帧 执行 耗时操作1...
在第2帧 执行 耗时操作2...
4.在初始化时获取并缓存组件
对于稳定的组件,减少循环调用GetComponent函数。
三.UnityAPI优化
我们在平常使用时,常常会忽略Unity中的一些API其实是具有昂贵的性能损耗的。
1.减少使用 Sendmessage( ) / BroadcastMessage( )
1.原因:基于运行时反射,获取每个对象上的每个脚本组件,效率很低(建议仅用原型开发)。
2.建议方案:缓存需要访问的脚本组件对象(但这种会出现写死的情况,代码结构可能不灵活),若不知道事件接收者,可改用 事件/ 代理 / MVC框架。
2.减少使用 Find( )
1.原因:(1)Unity的Find() 会从场景根节点开始深度优先搜索,便利所有Gameobject及其子对象,递归检查每个节点的Name属性。
(2)仅搜索已激活对象,对预制件实例和动态生成对象一视同仁,大小写敏感。
2.建议方案:序列化(性能消耗为0)或启动时缓存。
3.减少使用Transform
【本质上是减少计算开销和内存访问成本】
1.原因:每一次设置transform组件的position、rotation属性都将引发OnTransformChanged( )事件
并且会对其所有子节点也都这么做。
2.建议方案:减少对transform.position直接赋值,将位置值使用Vector3缓存,经过计算后再一次拷贝给Transform。
4.尽量使用localPosition,localRotation
【本质上是减少计算开销和内存访问成本】
注意:当对象没有父级时,position和localPosition等价。
1.原因:localPosition是直接读取对象本地坐标系数据,存储在连续内存块。而position需要动态计算,使用transform访问对象的position时返回的都是世界空间下的位置,对于子物体,需要经过层级运算才能得到其世界坐标。
2.建议方案:子级对象尽量使用localPosition,localRotation。
5.删除空的生命周期函数
【存在隐藏开销】
1.原因:(1)在引擎层和脚本层的每帧交互。
(2)每帧调用前的安全检查:检查gameobject有效性,多个对象开销会叠加
2.建议方案:移除空的Update,避免隐藏开销。
6.优化向量开方运算
1.原因:开方开销很大:
Vector3.magnitude
Vector3. Distance()
2.建议方案:使用Vector3.sqrMagnitude。
7.避免高频Camera.main
1.原因:每次访问Camera.main,Unity内部会执行GameObject.FindGameObjectWithTag("MainCamera");属于O(n)线性全场景搜索;
2.建议方案:启动时缓存一次。切忌在Update中使用Camera.main!!
8.Animator字符串哈希优化
1.原因:直接使用字符串参数(如animator.setBool("Run",true);)会触发以下开销:
- 字符串哈希计算:每次调用时实时计算
"Run"
的哈希值 - 字典查询:Animator内部通过哈希值查找参数索引
- 参数校验:验证参数类型是否匹配
2.建议方案:预缓存Animator中哈希索引。
// 预计算哈希值(推荐使用readonly)
private static readonly int RunHash = Animator.StringToHash("Run");
private static readonly int SpeedHash = Animator.StringToHash("Speed");
void Update() {
animator.SetBool(RunHash, isRunning); // 比字符串快3倍
animator.SetFloat(SpeedHash, speed);
}
9.预缓存协程yield指令
1.原因:每减少一个new操作可节省约40B内存,高频协程调用下效果显著
2.建议方案:预缓存常用Yield指令。
// 预缓存常用Yield指令
private static readonly WaitForSeconds wait1Sec = new WaitForSeconds(1);
private static readonly WaitForFixedUpdate waitFixed = new WaitForFixedUpdate();
IEnumerator CountdownCo() {
yield return wait1Sec; // 复用对象
yield return waitFixed;
}
四.字符串优化
一.字符串的不变性
——摘自Unity/C#基础复习(3) 之 String与StringBuilder的关系 - sword_magic - 博客园
String是继承自object的引用类型,在C#中string类型的底层由char[],即字符数组进行实现,但我们并不能像修改字符数组的方式来对字符串进行修改。事实上,我们以为的修改(字符串的连接,字符串的赋值)对于字符串来说都不是真正的修改,每当我们对字符串进行赋值时,底层首先会去查找字符串池,如果字符串池有这个字符串,那么直接将当前变量指向字符串池内的字符串。如果字符串池内没有这个字符串,那么在堆上创建一块内存用于放置这个字符串,并将当前变量指向这个新建的字符串。字符串的这种特性,使得它的赋值和连接操作很容易造成内存浪费,因为每一次都将在堆上创建一个新的字符串对象。
字符串为什么会被设计成不可变的形式呢?很显然,不可变的形式对于字符串可变的形式是利大于弊的。主要有两个原因1.线程安全。在多线程环境下,只有对资源的修改是有风险的,而不可变对象只能对其进行读取而非修改,所以是线程安全。如果字符串是可修改的,那么在多线程环境下,需要对字符串进行频繁加锁,这是比较影响性能的。
2.防止程序员误操作意外修改了字符串。想象下面这样一种情况,一个静态方法用于给字符串(或StringBuilder)后面增加一个字符串。
二.优化方案
1.字符串拼接优化
1.原因:字符串是无法改变的数组。如果要把两个字符串连接起来,会创建新数组,而旧数组会成为垃圾。
2.建议方案:使用StringBuilder拼接字符串。
// 错误示例:高频拼接
void Update() {
text.text = "HP:" + currentHP + "/" + maxHP; // 每帧生成新字符串
}
// 正确做法:StringBuilder复用
private StringBuilder sb = new StringBuilder(32);
void Update() {
if(hpChanged) {
sb.Clear();
sb.Append("HP:").Append(currentHP).Append("/").Append(maxHP);
text.text = sb.ToString(); // 仅在变化时生成
}
}
2.字符串比较优化
1.原因:直接比较字符串效率低,尤其是长字符串。
2.建议方案:使用String.Compare( )进行字符串比较