Quest Unity开发技巧:流畅稳定的无缝场景加载

arinchina 2021年12月20日10:27:51
评论

要登陆Quest Store,你的应用需要保持每秒72帧的稳定速度。这并不容易实现,但通过遵循相关文档和最佳实践来优化性能,你应该依然可以提交一款符合标准的作品。然而,现在的游戏越来越大,同时将所有一切都保存在内存中变得越来越困难。所以,你需要一种方法来加载游戏中需要的部分,并卸载不需要的部分,而且你必须要在玩家不注意的情况下完成。

解决方案是流式传输,利用Unity的异步场景加载来实现。不过,你在实现过程中依然有可能会遇到一定的问题。下面这篇来自Meta的博文将向你介绍相关的使用技巧。注意,以下所有建议均基于Unity 2020.3.8f1中的行为。在Unity的其他版本中,性能结果可能不同。

为什么我的‘异步’加载仍会导致丢帧?

从磁盘加载场景可能需要很长时间,尤其是在有大量网格和纹理的情况下。要读取的数据非常多。这就是为什么Unity提供了一种异步加载场景的方法SceneManager.LoadSceneAsync()。这非常棒,因为你不必在等待加载数据时显示加载屏幕,但你可能已经注意到,当你将allowSceneActivation flag设置为true时,你会得到一个很长的帧。你可能会问,“什么原因,这不是异步吗?”。下面是原因:

-Unity中的所有对象都遵守Unity生命周期,例如Awake, OnEnable, Start, Update, OnDisable, OnDestroy。

-所有生命周期函数都在主线程执行。

所以当你激活场景时,Unity必须为场景中的每个对象运行Unity生命周期的第一步。

你可能会说,“但我没有任何脚本附加到对象”。你需要知道的是,GameObjects和Transforms都存在Unity版本的“Awake”。无论是活动的还是非活动的,场景中的每个GameObject都会增加场景激活所需的时间。所以,这引出了我们的第一个技巧:

技巧1:减少场景中GameObject的数量

我运行测试了异步加载不同数量GameObject的场景,并测量了将allowSceneActivation设置为true的帧长度。以下是在Quest 2运行的结果:

Quest Unity开发技巧:流畅稳定的无缝场景加载

如你所见,场景激活所需的时间与场景中GameObject的数量呈线性关系。在这种情况下,活动和非活动的GameObject之间没有区别。

你可能会问,如何减少场景中GameObject的数量?减少GameObject数量的一个好办法,并且是提高性能的一个好办法是:将尽可能多的静态几何体批处理成尽可能少的对象。第二,因为即使是未使用的对象都需要时间加载,所以在Release之前请从场景中删除所有不必要的/占位符对象。

技巧2:禁用场景中的GameObject

“但你刚才说,在我的场景中禁用或启用GameObject并不重要?”这对于empty GameObject正确,但它不是适合所有一切。接到GameObject的每个组件都会花费一定的时间。但是,时间的分布并不均匀。例如,ParticleSystem组件的激活时间比MeshFilter和MeshRenderer长,而TextMeshPro组件的激活时间比两者加起来都长,因为它必须在激活时构建网格:

Quest Unity开发技巧:流畅稳定的无缝场景加载

禁用GameObjects后,你将推迟所述不同组件在使用前需要执行的昂贵设置。下面是所有GameObject禁用时发生的情况:

Quest Unity开发技巧:流畅稳定的无缝场景加载

当然,为了获得组件的好处,你需要激活它们,但在加载时禁用它们可允许你在schedule中启用它们,这意味着你可以在多个帧滚动激活时间,避免糟糕的帧问题。

技巧3:预编译所有着色器

加载场景时可能会导致很大的问题,而非常容易避免的是着色器编译。当活动对象第一次引用着色器变量时,Unity将尝试编译它,这可能需要相当长的时间,特别是当需要同时编译多个着色器时。最好确保所有着色器都已预编译,并在初始加载期间准备就绪(可以被静态加载屏幕屏蔽)。一种方法是用游戏中引用的每种材质创建一个场景,然后首先将其加载到初始屏幕后面。确保涵盖游戏中可能会更改Unity着色器关键字的所有不同变量,如reflection probes、light probes和direction light等。避免使用Shader.WarmupAll,因为它将编译你的着色器的所有可能变量,而你不太可能在游戏中遇到它们中的大多数。

技巧4:小心意外的网格陷阱

尽管大多数网格都非常乐意与场景异步加载,但有一些条件会使网格加载在激活场景时出现故障。如果网格需要CPU读取,则其激活时间将很长。有一些因素会导致网格的CPU读取,其中最明显的是保持ReadWrite启用状态。另外,如果网格可蒙皮,则其需要CPU读取。很难避免同时加载蒙皮网格,但你可以通过一定的技巧进行避免。例如,一些模块化角色系统将所有可能的部分包含在一个大型FBX文件中,这在运行时将导致巨大的负载。相反,你最好将所有片段分解为单独的网格,并在运行时构建模块化角色。在最坏的情况下,最好将蒙皮网格从异步场景中完全移除,并在初始加载屏幕后面加载它们。

技巧5:在尽可能少的“根”游戏对象下嵌套游戏对象(但仅在加载时)

如果所有游戏对象都嵌套在“根”游戏对象下,你将看到激活时间的改进:

Quest Unity开发技巧:流畅稳定的无缝场景加载

Unity必须对位于场景根的每个对象进行额外的处理。为了避免这种情况,只需将所有内容放在单个或一小组“根”对象下,即可加快场景激活速度。

但是,嵌套对象在运行时同样有缺点。Unity将在多个线程上更新转换,所以使用单个根对象实际上会降低性能,因为它将多线程操作限制为单个线程。因此,最好在运行时将嵌套对象“解包”到平面层次结构中,或者使用少量根对象来保持多线程运行时性能。

技巧6:回收利用你的GameObject

一种优化方法是开始共享和重用对象。大多数Unity开发者已经熟悉这项技术。本质上,这个想法是创建一个通用重用对象池,并在玩家移动时在游戏世界中移动它们。例如,如果玩家在不同的地方看到木桶,你就会有一个木桶池,当玩家走过一个木桶并移动到另一个区域时,在幕后你将同一个木桶移动到新的位置,玩家一点都不知道。尽管听起来很简单,但它存在复杂的部分。

首先,设计师需要将桶放置在该位置,因此你可能需要在场景中放置“BarrelProxy”脚本,以便池知道将其放置在何处。其次,如果你使用的是烘焙照明,你必须考虑光照如何影响你的桶。如果你的桶是可破坏的,它们不需要在你的光照贴图中投射阴影,但它们可能仍然会接收阴影。在支持光照贴图之前,需要实例化所有桶,记录光照贴图ID和Rect,并将其包含在代理组件中。然后在运行时,当你从池中拉出桶时,你应该只需要将该光照贴图数据应用到渲染器。

这需要构建自定义步骤来处理代理对象实例化/移除,我们可以使用这些步骤来执行另一个步骤:从场景本身移除代理,并构建代理位置的清单。然后,你可以将此清单作为每个场景中组件的一部分,或者作为游戏中某个地方包含的简单文本文件。

技巧7:发挥创意

如果前面的6个技巧不足以实现场景的无缝过渡,那么你是时候发挥创意了。你可以决定什么对你的作品最有意义,并创建最适合你的解决方案。场景包含的越少,加载的速度就越快,因此逻辑结论可能是进一步减少场景的使用。也许你有一个构建步骤可以将每个场景分割成若干子场景,每个子场景可以在不挂接的情况下加载。也许解决方案只是将场景用于静态几何体,其他所有内容都将在运行时作为预设加载。如果你能想出一个有创意的解决方案,请务必向社区分享。

 

文章来源:映维网

weinxin
我的微信
这是我的微信扫一扫
广告也精彩
arinchina
  • 本文由 发表于 2021年12月20日10:27:51
  • 转载请注明:https://www.arinchina.com/11197.html
广告也精彩
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: