《Unity游戏优化》笔记(4)[21/02/05_周五][P29_46]

目录

第2章 脚本策略 (P29)

2.1 使用最快的方法获取组件

2.2 移除空的回调定义 (P31)

2.3 缓存组件索引 (P34)

2.4 共享计算输出

2.5 Update、Coroutines和InvokeRepeating (P36)

2.6 更快的GameObject空引用检查 (P40)

2.7 避免从GameObject去除字符串属性。

2.8 使用合适的数据结构

2.9 避免运行时修改Transform的父节点 (P44)

2.10 注意缓存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属性的次数,方法是将他们缓存在一个成员变量中,只在帧的末尾提交他们。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值