当前位置: 首页 > news >正文

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( )

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


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中哈希索引。

// 预计算哈希值(推荐使用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( )进行字符串比较

相关文章:

  • 项目名称:基于计算机视觉的夜间目标检测系统
  • 本地内网搭建网址需要外部网络连接怎么办?无公网ip实现https/http站点外网访问
  • 公网 IP 地址SSL证书实现 HTTPS 访问完全指南
  • Ubuntu下使用PyTurboJPEG加速图像编解码
  • 新能源知识库(46)EMS与协控装置
  • Peiiieee的Linux笔记(1)
  • [OS_20] 设备和驱动程序 | GPIO | IPP | PCIe总线 | ioctl
  • Android S - 恢复部分应用安装
  • 使用Gitlab CI/CD结合docker容器实现自动化部署
  • javascript入门
  • RT-Thread Studio 配置使用详细教程
  • Spring Cloud Gateway 介绍
  • 金蝶K3 ERP 跨网段访问服务器卡顿问题排查和解决方法
  • 用户态与内核态是什么?有什么作用?两者在什么时候切换?为什么要切换?
  • word用endnote插入国标参考文献
  • 【C++】多重继承与虚继承
  • IDEA2025(2025.1.1)都更新了什么???
  • DevSecOps实践:用Terraform策略检查筑牢基础设施安全防线
  • 蓝桥杯20112 不同的总分值
  • 金属切削机床制造企业如何破局?探索项目管理数字化转型
  • 泉州it培训/itmc平台seo优化关键词个数
  • 企业网站源码哪个最好/找小网站的关键词
  • 10m网站并发量/电子商务网站建设论文
  • 网站限制国内ip访问/50个市场营销经典案例
  • 网站建设的技术指标/百度宣传推广费用
  • 公司做网站,要准备哪些素材/短视频入口seo