Unity性能优化-C#编码模块

本期我将整理个人所学关于Unity中C#编码方面的一些优化思路,我们的目标是提升代码性能。我将从GC,算法效率,UnityAPI,字符串方面,结合一些实际场景给出性能优化方案。

目录

一.GC优化

1.GC对性能的影响

2.GC优化方向

3.优化方案

1.场景A

2.场景B

3.场景C

二.算法优化

一.算法效率对性能的影响

二.算法优化方案

1.减少循环调用

2.仅在改变时更新显示

3.增加代码更新的延时

4.在初始化时获取并缓存组件

三.UnityAPI优化

[1.减少使用 Sendmessage( ) / BroadcastMessage( )](#1.减少使用 Sendmessage( ) / BroadcastMessage( ))

[2.减少使用 Find( )](#2.减少使用 Find( ))

3.减少使用Transform

4.尽量使用localPosition,localRotation

5.删除空的生命周期函数

6.优化向量开方运算

7.避免高频Camera.main

8.Animator字符串哈希优化

9.预缓存协程yield指令

四.字符串优化

一.字符串的不变性

二.优化方案

1.字符串拼接优化

2.字符串比较优化


一.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( )中代码更新的延时。

cs 复制代码
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);)会触发以下开销:

  1. 字符串哈希计算 :每次调用时实时计算"Run"的哈希值
  2. 字典查询:Animator内部通过哈希值查找参数索引
  3. 参数校验:验证参数类型是否匹配

**2.建议方案:**预缓存Animator中哈希索引。

cs 复制代码
// 预计算哈希值(推荐使用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指令。

cs 复制代码
// 预缓存常用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拼接字符串。

cs 复制代码
// 错误示例:高频拼接
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( )进行字符串比较

相关推荐
Sator11 小时前
Unity的FishNet相关知识
网络·unity·游戏引擎
AllBlue2 小时前
安卓调用unity中的方法
android·unity·游戏引擎
小股虫3 小时前
RabbitMQ异步Confirm性能优化实践:发送、消费、重试与故障应对
分布式·性能优化·rabbitmq
大江东去浪淘尽千古风流人物3 小时前
【MSCKF】StateHelper 学习备注
vscode·学习·性能优化·编辑器·dsp开发
李岱诚3 小时前
epic商城下载,ue4报错处理
游戏引擎·ue4
jtymyxmz17 小时前
《Unity Shader》10.1.4 折射
unity·游戏引擎
在路上看风景18 小时前
12. Burst
unity
平行云PVT20 小时前
实时云渲染解决UE5 像素流插件迁移及传输数据受限问题
unity·ue5·xr·实时云渲染·云桌面·像素流·云推流
梵得儿SHI20 小时前
AI Agent 性能优化与成本控制:从技术突破到行业落地实战指南
人工智能·性能优化·智能路由·aiagent落地实践·成本控制和稳定性保障·提示词压缩·模型运行慢
勤劳打代码1 天前
追本溯源 —— SetState 刷新做了什么
flutter·面试·性能优化