目录
.12 禁用未使用的脚本和对象
场景有时会变得非常繁忙,特别是构建大型的、开放的世界时。在Update回调中,调用代码的对象越多,它的伸缩性就越差,游戏也越慢。
2.12.1 通过可见性禁用对象 (P70)
OnBecameVisible()
OnBecameInvisible()
这里得测试一下,这两个什么情况下触发。
请注意,Unity还计算Scene窗口对这两个回调隐藏的摄像头数。如果发现在播放模式测试期间,这些方法没有被正确调用,请确保将Scene窗口的摄像机背对所有对象,或完全禁用Scene窗口。
测试过程中确实碰到这个问题,这个只有在模型上的脚本有一些计算,而要优化这种计算的时候才好用。
2.12.2 通过距离禁用对象
定期检查与给定目标对象的总距离,如果它偏离目标太远,就禁用它自己。
2.13 使用距离平方而不是距离
CPU比较擅长将浮点数相乘,但不擅长计算他们的平方根。
Vector3类的sqrMagnitude属性
2.14 最小化反序列化行为 (P73)
Unity的序列化系统主要用于场景、预制件、ScriptableObjects和各种资产类型(往往派生自ScriptableObject)。当其中一种对象类型保存到磁盘时,就使用YAML(Yet Another Markup Language,另一种标记语言)格式将其转换为文本文件,稍后可以将其反序列化为原始对象类型。所有的GameObject及其属性都会在序列化预制件或者场景时序列化,包括所有的和受保护的字段,它们的所有组件,及其子GameObjects和组件等。
构建应用程序时,这些序列化的数据会捆绑在大型二进制数据文件中,这些文件在Unity内部被称为序列化文件。在运行时从磁盘读取和反序列化数据是一个非常慢的过程(相对而言),因此所有的反序列化活动都伴随着显著的性能成本。
一旦数据从磁盘加载到内存中,以后重新加载相同的引用会快得多,但是在第一次访问时总是需要磁盘活动。需要反序列化的数据集越大,此过程所需的时间就越长。由于预制组件的每个组件都是序列化的,因此层次结构越深,需要反序列化的数据就越多。
加载大型序列化数据集可能会在第一次加载时造成CPU的显著峰值。
2.14.1 减小序列化对象
我们的目标应该是是序列化的对象尽可能小,或者将他们分割成更小的数据块,然后一块一块地组合在一起,这样它们就可以一次加载一块。
2.14.2 异步加载序列化对象
可以通过Resources.LoadAsync()以异步方式加载预制块和其他序列化的内容,这将把从磁盘读取的任务转移到工作线程上,从而减轻主线程的负担。
2.14.3 在内存中保存之前加载的序列化对象
一旦序列化对象加载到内存中,它就会保留在内存中,如果以后需要,可以复制它。
2.14.4 将功能数据移入ScriptableObject
这减少存储在预制文件中的序列化数据量。
2.15 叠加、异步地加载场景 (P74)
可以加载场景来替换当前场景,也可以添加内容到当前场景中,而不卸载前一个场景。
SceneManager.LoadScene(),LoadSceneMode
另一种场景加载模式是同步和异步完成,两者各有千秋。
SceneManager.LoadSceneAsync(),LoadSceneMode.Additive。
Unity可以通过叠加式加载,支持多个场景同时加载,允许每个场景代表一个关卡的一小块。利用这一功能需要一个系统不断检查玩家在关卡中的位置,直到他们接近为止。需要确保触发场景的异步加载有足够的时间,以便玩家不会看到对象弹出到游戏中。
场景也可以卸载,从内存中清除出来。这将删除任何不再需要的使用Update()的组件,节省一些内存或提升一些运行时性能。
SceneManager.UnloadScene(),SceneManager.UnloadSceneAsync()
根据玩家在关卡中的位置只使用需要的内容。原来的场景必须分解成更小的场景,然后根据需要加载和卸载。需要考虑的是,场景卸载会导致许多对象被销毁,这可能会释放大量内存并触发垃圾回收。
这种方法需要大量的场景重新设计、脚本编写、测试和调试工作,这是不可低估的,但是改进用户体验的好处是非常多的。在游戏中拥有区域间的无缝过渡是一种经常受到忘记和评论家称赞的优点,因为它不会打断玩家的操作。如果适当地使用它,就可以显著提升运动时性能,进一步改善用户体验。
2.16 创建自定义的Update层
成千上万的MonoBehaviour在场景开始时一起初始化。
它们极可能在同一帧内触发导致CPU使用率在一段时间内出现一个巨大的峰值,接着会临时下降,然后处理下一轮再次出现峰值。理想情况下,我们希望随时间分散这些调用。
可能解决方案:
- 每次计时器过期或协程触发时,生成一个随机等待时间。
- 将协程的初始化分解到每个帧中,这样每个帧中只会启动少量的协程初始化。
- 将调用更新的职责传递给某个God类,该类对每个帧的调用数量进行了限制。
一个可能更好的方法是根本不使用Update(),或者准确的说,只使用一次。
当Unity调用Update()时,实际上是调用它的任何回调,都要经过前面提到的本机-托管的桥接,这可能是一个代价高昂的任务。让God类MonoBehaviour使用它自己的Update()回调来调用自定义组件使用的自定义更新样式的系统,可以最小化Unity需要跨越桥接的频率。
一开始就实现这个设计,更好的控制更新合适以及如何在整个系统中传播。
如果发现即将达到当前帧的CPU预算,就暂时降低优先级任务。
所有想要与这样一个系统集成的对象必须有一个功能的入口点。使用interface
public interface IUpdateable
{
void OnUpdate(float dt);
}
接口的优点在于它们改善了代码库的解耦能力,允许替换大型子系统,只要坚持使用接口楼,它就能继续按预期工作。
using UnityEngine;
public class UpdateableComponent : MonoBehaviour,IUpdateable
{
public virtual void OnUpdate(float dt)
{
}
}
我们定义了相同概念的自定义版本。
GameLogic类实现
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameLogicSingletonComponent : SingletonComponent<GameLogicSingletonComponent>
{
public static GameLogicSingletonComponent Instance{
get{
return ((GameLogicSingletonComponent)_Instance);
}
set{
_Instance=value;
}
}
List<IUpdateable> _updateableObjects=new List<IUpdateable>();
public void RegisterUpdateableObject(IUpdateable obj){
if(!_updateableObjects.Contains(obj))
{
_updateableObjects.Add(obj);
}
}
public void DeregisterUpdateableObject(IUpdateable obj){
if(_updateableObjects.Contains(obj)){
_updateableObjects.Remove(obj);
}
}
void Update()
{
float dt=Time.deltaTime;
for(int i=0;i<_updateableObjects.Count;++i)
{
_updateableObjects[i].OnUpdate(dt);
}
}
}
using UnityEngine;
public class UpdateableComponent : MonoBehaviour,IUpdateable
{
void Start()
{
GameLogicSingletonComponent.Instance.RegisterUpdateableObject(this);
}
void OnDestroy()
{
if(GameLogicSingletonComponent.IsAlive){
GameLogicSingletonComponent.Instance.RegisterUpdateableObject(this);
}
}
public virtual void OnUpdate(float dt)
{
}
}
如果确保所有自定义组件都继承自UpdatableComponent类,那么实际上用一个Update()回调和N各虚函数调用替换了Update回调的N次调用。这可以节省大量的性能开销。
2.17 本章小结 (P80)
在已经证明是它们导致性能问题的原因的情况下,提高性能。
------------------------------------------------
实际上用一个Update()回调和N虚函数调用替换了Update()回调的N次调用。可以节省大量的性能开销。
实际测试一下两种Update
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UpdateTest : MonoBehaviour
{
public bool IsUpdate=false;
public int scriptCount=1000;
public int objCount=100000;
public static int ObjectCount=0;
void Start()
{
ObjectCount=objCount;
if(IsUpdate)
{
for(int i=0;i<scriptCount;i++)
{
this.gameObject.AddComponent<UpdateComponent>();
}
}
else{
for(int i=0;i<scriptCount;i++)
{
this.gameObject.AddComponent<OnUpdateableComponent>();
}
}
}
}
using UnityEngine;
public class OnUpdateableComponent : MonoBehaviour,IUpdateable
{
void Start()
{
GameLogicSingletonComponent.Instance.RegisterUpdateableObject(this);
}
void OnDestroy()
{
if(GameLogicSingletonComponent.IsAlive){
GameLogicSingletonComponent.Instance.RegisterUpdateableObject(this);
}
}
public virtual void OnUpdate(float dt)
{
ProfilerTest.DoSomethingCompletelyStupid(UpdateTest.ObjectCount);
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UpdateComponent : MonoBehaviour
{
// Update is called once per frame
void Update()
{
ProfilerTest.DoSomethingCompletelyStupid(UpdateTest.ObjectCount);
}
}
测试的结果,没明显差别,
的情况下似乎用OnUpdate快一点点
其实说不清楚,这种Update下的,帧率不断波动的。
说不动是以前2017时Native-Managed桥性能开销比较大,现在(2020.1.3)好了呢。一种可能的优化而已了。
看到现在2章了,确实学到了一点点东西,理解加深了,但是,具体的一目了然的优化手段还没看到。
继续吧。