Unity性能优化

非原创 ,参考文章: 

2024 腾讯游戏客户端面经 - 知乎 (zhihu.com) 

C#篇

1.Unity API

GameObject.GetComponent

Unity是基于组件的开发方式,所以GetComponent是一个高频使用的函数 每次调用GetComponent
时,Unity都要去遍历所有的组件来找到目标组件 每次都去查找是不必要的耗费,可以通过缓存的方式来避免这些不必要的开销 其中Transform是用到最多的组件,GameObject内部提供了一个.transform来获取此组件 然而经过测试发现缓存的效率依然是最高的 所以若要经常访问一个特定组件,将其缓存

GameObject.Find

GameObject.Find会遍历当前所有的GameObject来返回名字相符的对象 所以当游戏内对象很多时,这个函数将很耗时可以通过缓存的方法,在Start或Awake时缓存一次找到的对象,在后续使用中使用缓存的对象而非继续调
用GameObject.Find或者采用GameObject.FindWithTag来寻找特定标签的对象 如果能在一开始就确定好对象,可以通过Inspector注入的方式,将对象直接拖到Inspector中,从而避免了运行时的查找

Camera.main

Camera.main用来返回场景中的主相机,Unity内部是通过GameObject.FindWithTag来查找tag为
MainCamera的相机 当需要频繁访问主相机时,可以将其缓存以获得性能提升

GameObject.tag

GameObject.tag常用来比较对象的tag,但是直接采用.tag ==来进行对比的话,每一帧会产生GC Alloc 通过GameObject.CompareTag来进行比较则可以避免掉这些GC,但是前提是比较的tag需在Tag Manager中定义

关于GC alloc: UNITY 的GC ALLOC到底是什么 - 时空观察者9号 - 博客园 (cnblogs.com)

Transform.SetPositionAndRotation

每次调用Transform.SetPosition或Transform.SetRotation时,Unity都会通知一遍所有的子节点 当位置和角度信息都可以预先知道时,可以通过Transform.SetPositionAndRotation一次调用来同时设置位置和角度,从而避免两次调用导致的性能开销

Animator.Set…

Animator提供了一系列类似于SetTrigger,SetFloat等方法来控制动画状态机 例
如:m_animator.SetTrigger("Attack")是用来触发攻击动画 然而在这个函数内部,"Attack"字符串会被hash成一个整数 如果需要频繁触发攻击动画,可以通过Animator.StringToHash来提前进行hash,来避免每次的hash运算

Material.Set…

与Animator类似,Material也提供了一系列的设置方法用于改变Shader 例如:m_mat.SetFloat("Hue", 0.5f)
是用来设置材质的名为Hue的浮点数 同样可以通过Shader.PropertyToID来提前进行hash

Vector Math

如果需要比较距离,而非计算距离,用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;

Coroutine

Coroutine是Unity用来实现异步调用的机制,如果对其不够了解可以参考对Unity中Coroutines的理解 当需要实现一些定时操作时,若在Update中每帧进行一次判断,假设帧率是60帧,需要定时1秒调用一次,则会导致59次无效的Update调用 用Coroutine则可以避免掉这些无效的调用,只需要yield return new
WaitForSeconds(1f);即可 当然这里的最佳实践还是用一个变量缓存一下new WaitForSeconds(1f),这样省去了每次都new的开销

SendMessage

SendMessage用来调用MonoBehaviour的方法,然而其内部采用了反射的实现机制,时间开销异常大,需要尽量避免使用 可以用事件机制来取代它

Debug.Log

输出Log是一件异常耗时,而且玩家感知不到的事情 所以应该在正式发布版本时,将其关闭 Unity的Log输出并不会在Release模式下被自动禁用掉,所以需要手动来禁用 可以在运行时用一行代码来禁用Log的输出:Debug.logger.logEnabled = false; 不过最好采用条件编译标签Conditional封装一层自己的Log输出,来直接避免掉Log输出的编译,还可以省去Log函数参数传递和调用的开销 具体参见:Unity3D研究院之在发布版本屏蔽Debug.log输出的Log

2.Csharp

反射

反射是一项异常耗时的操作,因为其需要大量的有效性验证而且无法被编译器优化 而且反射在iOS下还可能存在不能通过AOT的情况,所以应该尽量避免使用反射 可以自己建立一个字符串-类型的字典来代替反射,或者采用delegate的方式来避免反射

内存分配(栈和堆)

在C#中,内存分配有两种策略,一种是分配在栈Stack上,另一种是分配在堆Heap上 在栈上分配的对象都是拥有固定大小的类型,在栈上分配内存十分高效 在堆上分配的对象都是不能确定其大小的类型,由于其内存大小不固定,所以经常容易产生内存碎片,导致其内存分配相对于栈来说更为低效

值类型和引用类型

在C#中,数据可以分为两种类型:值类型ValueType和引用类型ReferenceType值类型包括所有数字类型,Bool,Char,Date,所有Struct类型和枚举类型 其类型的大小都是固定,它们都在栈上进行内存分配 引用类型包括字符串,所有类型的数组,所有Class以及Delegate,它们都在堆上进行内存分配

装箱

装箱Boxing指的是将值类型转换为引用类型,而拆箱UnBoxing的是将引用类型转换为值类型装箱和拆箱存在着从栈到堆的互指以及堆内存的开辟,所以它们本质是一项非常耗时的操作,应该尽量避免之

垃圾回收

在堆上分配的内存,其实是由垃圾回收器(Garbage Collector)来负责回收的 垃圾回收算法异常耗时,因为它需要遍历所有的对象,然后找到没有引用的孤岛,将它们标记为「垃圾」,然后将其内存回收掉 频繁的垃圾回收不仅很耗时,还会导致内存碎片的产生,使得下一次的内存分配变得更加困难或者干脆无法分配有效内存,此时堆内存上限会往上翻一倍,而且无法回落,造成内存吃紧 所以应该极力避免GC Alloc,即需要控制堆内存的分配

字符串

字符串连接会导致GC Alloc,例如string gcalloc = "GC" + "Alloc"会导致"GC"变成垃圾,从而产生GC Alloc又比如:string c = string.Format("one is {0}", 1),也会因为一次装箱操作(数字1被装箱成字符串"1")而产生额外的GC Alloc 所以如果字符串连接是高频操作,应该尽量避免使用+来进行字符串连接 C#提供了StringBuilder类来专门进行字符串的连接

IL2CPP

IL2CPP是Unity提供的将C#的IL码转换为C++代码的服务,由于转成了C++,所以其最后会转换成汇编语言,直接以机器语言的方式执行,而不需要跑在.NET虚拟机上,所以提高了性能 同时由于IL的反编译较为简单,转换成C++后,也会增加一定的反汇编难度 IL2CPP的C++代码虽然是自动生成的,但是其中间的某些过程也可以被人为操纵,从而达到提升性能的目的

Sealed修饰

在C#中,虚函数的调用会比直接调用开销更大,可以用sealed修饰符来修饰掉那些确保不会被继承的类或函数

避免自动判空

在自动转换的C++代码中,IL2CPP默认会对所有Nullable的变量做判空 在某些非常确定参数不为空的场合,这种检测是不必要的 具体步骤是复制Il2CppSetOptionAttribute.cs文件到Assets目录下,然后在类或者函数定义上加一个修饰语句[Il2CppSetOption(Option.NullChecks, false)]即可以禁用整个类或者函数的判空检测

避免数组越界检测

同理,IL2CPP也会默认对所有数组的读写做越界检测,可以通过修饰语句
[Il2CppSetOption(Option.ArrayBoundsChecks, false)]来将其禁用

3.数据结构

容器类型

容器应该针对不同的使用场合进行选择,主要看使用场合哪种操作的频率较高 例如:

  • 经常需要进行随机下标访问的场合,优先选择数组Array)或列表(List)
  • 经常需要进行查找的场合,优先选择字典(Dictionary)
  • 经常需要插入或删除的场合,优先选择链表(LinkedList)
  • 还有一些特殊的数据结构,适用于特殊的使用场合 例如:
  • 不能存在相同元素的,可以选择HashSet
  • 需要后进先出的,用来优化递归函数调用的,可以选择Stack
  • 需要先进先出的,可以选择Queue

对象池

对象池(Object Pool)可以避免频繁的对象生成和销毁 游戏对象的生成,首先需要开辟内存,其次还可能会引起GC Alloc,最后还可能会引发磁盘I/O 频繁的销毁对象会引发严重的内存碎片,使得堆内存的分配更加困难
所以在有大量对象需要重复生成和销毁时,一定要采用对象池来缓存好创建的对象,等到它们无需使用时,不需要将其销毁,而是将其放入对象池中,可以免去下次的生成

空间划分

在计算空间碰撞或者寻找最近邻居时,如果空间很庞大,需要参与计算的对象太多的情况下,用两层循环逐个遍历去计算的复杂度为平方级 可以借助于空间划分的数据结构来使复杂度降低到N*Log(N) 四叉树一般用来划分2D空间,八叉树一般用来划分3D空间,而KD树则是不限空间维度 具体参考:KD树的应用与优化

4.算法

循环

循环的使用非常常见,也非常容易成为性能热点 应该尽量避免在循环内进行耗时或无效操作,尤其是这个循环在每帧的Update调用中时

数学运算

开方运算,三角函数这些都是耗时的数学运算,应尽量避免之 如果只是单纯比较距离而不是计算距离的话,就可以用距离的平方来表示,可以节约掉一次耗时的开方运算


三角运算可以通过简单的向量运算来规避之,具体参考:向量运算在游戏开发中的应用和思考


又比如如果经常需要除一个常数,比如用万分位整数来表示小数需要经常除10000,可以改成乘0.0001f,可以规避掉较乘法更为耗时的除法运算 实际验算证明,现代的编译器会对此进行优化,所以没有必要为此牺牲可读性 很多时候还是要先测算再去写代码会比较好

缓存

缓存的本质就是用空间换时间 例如之前在Unity API中提到的很多耗时的函数,都可以用缓存来提升性能包括对象池,也是缓存技术的一种 针对于需要依赖复杂运算而且后续要经常用到值,可将其缓存起来,以避免后续的计算,从而获取性能提升

Lua 篇

Unity性能优化 Lua篇 (kdocs.cn)

Unity与 四叉树

参考:Unity实现基于四叉树的范围检测算法 - 知乎 (zhihu.com)

概念

四叉树或四元树也被称为Q树(Q-Tree),同理,八叉树或者八元树也被称作Oc树(Octree),二者原理相仿。四叉树广泛应用于图像处理、空间数据索引、2D中的快速碰撞检测、存储稀疏数据等,而八叉树主要应用于3D图形处理。在游戏引擎中,四叉树多被用于2D的碰撞算法,而3D的则有更先进的算法。

四叉树,顾名思义,每个结点都有四个子结点,为什么四叉树可以做到范围检测优化,正常的范围检测都是至少两层遍历来互相检测距离的;但是四叉树将物体存在范围不断进行1/4分割,并且将每个物体分割到不同的区域内每个区域就相当于一个结点,当进行检测时,只需要让每个区域内的物体互相进行检测就行了(因为超过这个区域的一定大于了检测范围);同时如果一个物体在两个结点范围内,将他视为同时存在于两个结点范围

思路

为什么需要四个子结点,如果我们将2D空间视为一个坐标系,原点在正中心,那么很明显,我们可以将这个坐标系分为四个象限。随后我们对每个象限再次进行同样的划分,如此递归下去,就会得到一颗四叉树。

那么这样分割如何实现范围检测呢?四叉树的主要思想就是把很多很多物体分割到不同的小象限内,并且每个象限都有最大的容纳物体上限,在进行范围简称时,实际上只要让最小的象限内的物体互相比较是否碰撞就行了。

代码实现

实际上,四叉树的理解是比较简单的,但是如何将这种思路让计算机去理解,才是最困难的地方。

为了实现四叉树,和其他树一样,我们需要树节点的结构。与纯算法的结点结构不同的是,由于我们要在游戏引擎中实现效果,所以实际上我们需要记录物体。为了方便,我用来模拟碰撞的物体不是普通的GameObject,而是UI上的Image,因为他的 RectTransform 的标准单位更大。同时,我们的结点需要知道自己在树的哪一层级、最多可以容纳多少物体。为了防止无限分割,我们需要限制最大层级,一旦结点到了最大层级,不论内容物是否超量都需要终止递归,一般来说,最后一层的象限大小,实在是分的太小了,可以认为其内容物都是互相碰撞的。最后,我们还需要像其他树节点一样,记录子结点以及自己的值,这里我们存储的值结构是 Rect ,它可以很好地表示一个象限的形状与位置。

Rect是一个四元结构体,记录的是x、y、w、h,代表的是物体的位置坐标与宽高,这里不做赘述。

结合上文,我们可以写出这样一个结构体(类):

public class QuadTree
{//下文两个大写字母的变量可变成静态/常量,按需更改//节点内允许的最大对象private readonly int MAX_OBJECTS = 1;//最大层级private readonly int MAX_LEVELS = 3;//当前层级private readonly int level;//当前层级内的对象public List<RectTransform> rectTrans;//rect范围private Rect bounds;//子节点private readonly List<QuadTree> childs;/// <summary>/// 构造函数/// </summary>public QuadTree(Rect pBounds, int pLevel = 0, int maxObjs = 1, int maxLevel = 3){rectTrans = new List<RectTransform>();childs = new List<QuadTree>();bounds = pBounds;level = pLevel;MAX_OBJECTS = maxObjs;MAX_LEVELS = maxLevel;}
}

由于这棵树实在是太复杂了,我们不可能一次写完他的全部方法,所以只能循序渐进,先实现最简单的清理与静态取值方法。注意,静态取值方法是获取 RectTransform 对象的Rect值,由于Unity的RectTransform 类的 rect 属性实际上返回的是Pivot和Size的组合值(而不是Position与Size),我们需要写一个静态取值方法来正确获得某个RectTransform 物体的Rect。

清理的代码很简单,递归一下就好了,获取值也没什么好说的,拿坐标与宽高重新算一下就好了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/466015.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Qt【一】:Qt3个窗口类的区别、VS与QT项目转换

一、Qt3个窗口类的区别 QMainWindow&#xff1a;包含菜单栏、工具栏、状态栏 QWidget&#xff1a;普通的一个窗口&#xff0c;什么也不包括 QDialog&#xff1a;对话框&#xff0c;常用来做登录窗口、弹出窗口&#xff08;例如设置页面&#xff09; QDialog实现简易登录界面…

【原创 附源码】Flutter安卓及iOS海外登录--Tiktok登录最详细流程

最近接触了几个海外登录的平台&#xff0c;踩了很多坑&#xff0c;也总结了很多东西&#xff0c;决定记录下来给路过的兄弟坐个参考&#xff0c;也留着以后留着回顾。更新时间为2024年2月7日&#xff0c;后续集成方式可能会有变动&#xff0c;所以目前的集成流程仅供参考&#…

【Linux】构建模块

&#x1f525;博客主页&#xff1a;PannLZ &#x1f38b;系列专栏&#xff1a;《Linux系统之路》 &#x1f94a;不要让自己再留有遗憾&#xff0c;加油吧&#xff01; 文章目录 构建第一个模块1模块的makefile2内核树内构建3内核树外构建 构建第一个模块 可以在两个地方构建模…

Python:函数和lambda表达式

函数实质性特定任务的一段代码&#xff0c;程序通过将一段代码定义成函数&#xff0c;并为该函数指定一个函数名&#xff0c;这样即可在需要的时候多次调用这段代码。因此&#xff0c;函数是代码复用的重要手段。 与函数紧密相关的一个知识点就是lambda表达式。lambda表达式可…

防御保护防火墙综合实验

一&#xff1a;办公区设备可以通过电信链路和移动链路上网&#xff08;多对多的NAT&#xff0c;并且需要保留一个公网IP不能用来转换&#xff09; 以上两条链路&#xff0c;任意一条故障则可以通过另一条链路继续上网 二&#xff1a;分公司设备可以通过总公司的移动链路和 电信…

C||1.水仙花数是指一个n位数,每一位数字的n次幂的和正好等于这个数本身。2.有n个整数,使其前面各数顺序向后移m个位置,最后m个数变成最前面的m个数。

1.水仙花数是指一个n位数&#xff0c;每一位数字的n次幂的和正好等于这个数本身。 比如&#xff1a;153 13 53 33。 要求打印出所有三位数的水仙花数。 #include <stdio.h> #include <math.h> int main() {int i,x,y,z;for(i100;i<1000;i){xi/100%10;yi/10%…

多旋翼无人机飞行控制详解,四旋翼无人机飞控原理深入解析

在四旋翼无人机中&#xff0c;相邻的两个螺旋桨旋转方向是相反的。如图所示&#xff0c;三角形红箭头表示飞机的机头朝向&#xff0c;螺旋桨M1、M3的旋转方向为逆时针&#xff0c;螺旋桨M2、M4的旋转方向为顺时针。当飞行时&#xff0c;M2、M4所产生的逆时针反作用力&#xff0…

blender怎么保存窗口布局,怎么设置默认输出文件夹

进行窗口布局大家都会&#xff0c;按照自己喜好来就行了&#xff0c;设置输出文件夹如图 这些其实都简单。关键问题在于&#xff0c;自己调好了窗口布局&#xff0c;或者设置好了输出文件夹之后&#xff0c;怎么能让blender下次启动的时候呈现出自己设置好的窗口布局&#xff…

【JAVA-Day74】探讨Java字节输入流(InputStream)

标题 《深入探讨Java字节输入流(InputStream)的奥秘》摘要引言一、什么是字节输入流 &#x1f60a;扩展理解和代码示例工作原理示例代码注意事项 二、字节输入流操作 TXT &#x1f4c4;1. 打开文件2. 读取文件内容3. 关闭文件示例代码注意事项 三、字节输入流的应用场景 &#…

DAY9.

1.选择芯片型号 2. 3. 4. 5. 6. 7.

《Linux 简易速速上手小册》第9章: 备份与恢复策略(2024 最新版)

文章目录 9.1 理解备份的重要性9.1.1 重点基础知识9.1.2 重点案例&#xff1a;数据中心遭受火灾9.1.3 拓展案例&#xff1a;个人电脑硬盘故障9.1.4 企业级数据库被恶意软件加密 9.2 实施备份策略9.2.1 重点基础知识9.2.2 重点案例&#xff1a;为中小企业实施备份策略9.2.3 拓展…

如何配置Pycharm服务器并结合内网穿透工具实现远程开发

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f3a5;系列专栏&#xff1a;《C语言》 《数据结构》 《Linux》《Cpolar》 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;…

机器学习:过拟合和欠拟合的介绍与解决方法

过拟合和欠拟合的表现和解决方法。 其实除了欠拟合和过拟合&#xff0c;还有一种是适度拟合&#xff0c;适度拟合就是我们模型训练想要达到的状态&#xff0c;不过适度拟合这个词平时真的好少见。 过拟合 过拟合的表现 模型在训练集上的表现非常好&#xff0c;但是在测试集…

unity 点击事件

目录 点击按钮&#xff0c;显示图片功能教程 第1步添加ui button&#xff0c;添加ui RawImage 第2步 添加脚本&#xff1a; 第3步&#xff0c;把脚本拖拽到button&#xff0c;点击button&#xff0c;设置脚本的变量&#xff0c; GameObject添加 Component组件 点击按钮&am…

线性时间非比较类排序之基数排序

基数排序 基数排序是桶排序的扩展&#xff0c;因此又称“桶子法”&#xff0c;它是通过键值的部分信息&#xff0c;将要排序的元素分配至某些“桶”中&#xff0c;以达到排序的作用。 1. 算法思想 将各元素按位数切割成不同的数字&#xff0c;然后分别根据每个位数的比较结果…

SCI论文作图规范

SCI论文作图规范包括以下几个方面&#xff1a; 一、图片格式 SCI论文通常接受的图片格式包括TIFF、EPS和PDF等。其中&#xff0c;TIFF格式是一种高质量的图像格式&#xff0c;适用于需要高分辨率和颜色准确性的图片&#xff1b;EPS格式是一种矢量图形格式&#xff0c;适用于需…

app逆向-android-studio安装使用教程

Android Studio 是谷歌推出的一个Android集成开发工具&#xff0c;基于IntelliJ IDEA. 类似 Eclipse ADT&#xff0c;Android Studio 提供了集成的 Android 开发工具用于开发和调试。 android-studio下载地址&#xff1a;https://developer.android.com/studio/archive androi…

Acwing 5469. 有效点对【正难则反+巧妙选择根节点】

原题链接&#xff1a;https://www.acwing.com/problem/content/5472/ 题目描述&#xff1a; 给定一个 n 个节点的无向树&#xff0c;节点编号 1∼n。 树上有两个不同的特殊点 x,y&#xff0c;对于树中的每一个点对 (u,v)(u≠v)&#xff0c;如果从 u 到 v 的最短路径需要经过…

算法沉淀——模拟(leetcode真题剖析)

算法沉淀——模拟 01.替换所有的问号02.提莫攻击03.Z字形变换04.外观数列05.数青蛙 模拟算法是一种通过模拟问题的描述或场景来解决问题的算法。这种算法的核心思想是按照问题描述的规则&#xff0c;逐步模拟问题的发展过程&#xff0c;从而得到问题的解决方案。通常&#xff0…

第7讲 全局异常统一处理实现

新建GlobalExceptionHandler类。 package com.java1234.exception;import com.java1234.entity.R; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdv…