目录
2.5 Update、Coroutines和InvokeRepeating (P36)
2.9 避免运行时修改Transform的父节点 (P44)
第2章 脚本策略 (P29)
本章探索将性能优化应用于下述领域的方式:
访问组件
组件回调(Update()、Awake()等)
协程
GameObject和Transform的使用
对象间通信
数学计算
场景和预制加载等的反序列化
2.1 使用最快的方法获取组件
GetComponent(string),GetComponent<T>(),GetComponent(typeof(T))
最好使用GetComponent<T>()
测试验证,100万次,
6413ms,89ms,95ms,
后面两者其实都能用,差别不大。
GetComponent(string)只用与调试和诊断。
一个非常罕见的例外,解析用户输入的字符串,来获取一个组件。这种情况也有办法,把常用的组件,用枚举类型列出来。
2.2 移除空的回调定义 (P31)
MonoBehaviour在场景中第一次实例化时,Unity会将任何定义好的回调添加到一个函数指针列表中,它会在关键时刻调用这个列表。即使函数体是空的,Unity也会挂接到这些回调中。这将浪费少量的CPU。
测试,30000个EmptyCallbackComponent会占用8ms。
这里说“很难想象一个场景有超过30000个对象”,我的项目要解决的是50w-100w个对象的渲染问题啊。而且我确实碰到这个问题了,把脚本绑定到游戏对象上,记录一些信息,Start和Update根本没用,但是我没有删除,浪费了大量的资源。
搜索空Update定义的正则表达式:
void \s*Update\s*?\s∗?\s∗?\s*?\n*?{\n*?\s*?\}
性能问题的常见来源:
- 反复计算很小或从不改变的的值
- 太多的组件计算一个可以共享的结果
- 执行工作的频率远超必要值
要达到60FPS,每帧应在16.667毫秒内完成所有Update()回调中的所有工作。
下面是解决这些问题的一些提示。
2.3 缓存组件索引 (P34)
除非内存非常有限,否则更好的的方法是在初始化过程中获取引用,并保存它们,直到需要使用它们为止。
2.4 共享计算输出
让多个对象共享某些计算的结果,可节省性能开销。
通常很容易养成在基类中隐藏大型复杂函数的习惯,然后定义使用该函数的派生类,完全忘记了该函数的开销,因为我们很少再次查看该代码。
2.5 Update、Coroutines和InvokeRepeating (P36)
另一个很容易延迟的习惯是在Update()回调中以超出需要的评率重复调用某段代码。
首先,与标准函数调用相比,启动协程会带来额外的开销成本(大约是标准函数调用的三倍),还会分配一些内存,将当前状态存储在内存中,直到下一次调用它。这种额外的开销也不是一次性的成本,因为协程经常不断地调用yield,这会一次又一次地造成相同的开销成本,所以需要确保降低频率的好处大于此成本。
其次,一旦初始化,协程的运行独立于MonoBehaviour组件中Update回调的触发,不管组件是否禁用,都将继续调用协程。
再次,协程会在包含它的GameObject变成不活动的那一刻自动停止。
最后,将方法转换为协程,可减少大部分帧中的性能损失,但如果方法体的单次调用突破了帧率预算,则无论该方法的调用次数怎么少,都将超过预算。
几种yield类型:WaitForSeconds,WaitForSecondsRealTime,WaitForEndOfFrame,WaitForFixedUpdate,WaitUntil,WaitWhile.
某些Update()回调的编写方式可以简化为简单的协程。
协程很难调试,因为他们不遵循正常的执行流程;在调用堆栈上没有调用者。如果希望使用协程,最好使他们尽可能简单,且独立于其他复杂的子系统。
有些协程通常可以替换成InvokeRepeating(),它的建立更简单,开销成本略小。
感觉InvokeRepeating实际上也是Coroutines
2.6 更快的GameObject空引用检查 (P40)
对GameObject执行空引用检查会导致一些不必要的性能开销。与典型的c#对象相比,GameObject和Monohaviour是特殊对象,因为他们在内存中有两个表示:一个表示存在于管理C#代码的相同系统管理的内存中,C#代码是用户编写的(托管代码),而另一个表示存在于另一个单独村里的内存中间中(本机代码)。数据可以在这两个内存空间之间移动,但是每次这种移动都会导致额外的CPU开销和可能的额外内存分配。
这种效果通常称为跨越本机-托管的桥接,会生成额外的内存分配。
RefrenceEquals(gameObject,null)
除非执行大量的空引用检查,否则最多只能获得很少的好处。
2.7 避免从GameObject去取字符串属性。
从GameObject是另一种意外跨越本机-托管桥接的微妙方式。
GameObject中受此行为影响的两个属性是tag和name。在游戏过程中使用这两种属性是不明智的。
CompareTag()方法避免了本机-托管的桥接。
2.8 使用合适的数据结构
列表,迭代快速
字典,查找快速
通常最好在列表和字典中(同时)存储数据。
2.9 避免运行时修改Transform的父节点 (P44)
从Unity5.4以后,Transform组件的父子关系操作起来更新动态数组。
应该将父Transform参数提供为GameObject.Instantiate()调用,它跳过了这个缓冲区分配步骤。
Transform组件的hierarchyCapacity属性修改缓冲区大小。
2.10 注意缓存Transform的变化 (P44)
访问和修改Transform组件的position、rotation、scale属性会导致大量未预料到的矩阵乘法计算。
对象在Hierarchy窗口中的位置越深,确定最终结果需要进行的计算越多。
使用localPosition、localRotation和localScale的相关成本相对较小,应该尽可能使用这些本地属性值。
但是将数学计算从世界空间更改为本地空间,会使原本很简单的问题变得过于复杂。为了更容易解决复杂的3d数学问题,牺牲一点性能是值得的。
不断更改Transform组件属性的另一个问题是,也会向组件(如collider、Rigidbody、Light和Camera)发送内部通知,这些组件也必须进行处理。
应尽量减少修改Transform属性的次数,方法是将他们缓存在一个成员变量中,只在帧的末尾提交他们。