最近一段时间在项目上新增了大地图的分块加载的机制,踩了很多坑,记录一下经历,分享之余,也希望能和大家共同探讨优化的方向。
目的
项目的框架用的是上一个项目的小地图实时竞技的框架,面对新项目的中等偏大的地图存在明显得不适应,包括loading过慢,运行时帧数过低等各种问题,需要引入新的地图加载策略来改善加载与运行时效率的问题。
方案
定方案时参考了SECTR COMPLETE插件,也参考了网上的一些资料,SECTR COMPLETE插件主要还是针对地形,包括提供了地形分割工具以及依据分割后的分块加载案例,但是我们的场景是由模型搭建起来的而不是地形,加上我们的场景不仅是横向扩展,也是纵向扩展,有多层楼,所以不太适用。
所以比较后还是选用了一个比较傻瓜化且容易实现的方案,即程序实现分块加载和配置机制 + 美术手动进行场景分割 + 策划手动配置加载卸载时机的方式,这种方案虽然看起来比较low,但胜在实现难度较低,可操作性强。
美术
美术主要是将场景由单个场景分拆为多个场景,注意场景分布相对均匀且不穿帮,程序需要提供一个编辑器用于将分块场景进行加载和卸载,方便美术编辑。
策划
策划需要配置每块场景的加载和卸载时机,这里有两个需要注意的点
- 提前制定可用于加载或卸载的时机
加载时对CPU的占用会变高,需要程序提前和策划协商好加载时机,比如CG和视频播放,或者走过某些只需要玩家纯移动的过场等CPU相对空闲的时候
- 添加保底
我们在加载过程中在底层和应用层都用了分帧加载,存在走到某一块时资源还未准备好的情况,需要提供可配置的保底机制,最粗暴的是打开loading之类的,当然一般不会触发。注意开启loading时关闭分帧加载,将加载速度拉满。
程序
程序的工作重点在于,如何保证实现丝滑的分块加载和卸载。
围绕这个重点,我们重点做了以下件事情
- 资源层的分帧加载
这是最为核心的一部分。因为如果不能实现丝滑的分帧加载,那对于策划来说不如把他们丢回loading去做,虽然慢,但是至少不影响过程中的体验。
资源层的对象池这些就不赘述了,几乎是每个项目必备。在对象池的基础上,资源层的分帧加载集中在两块,一是加载,二是实例化。分帧加载的思路是,所有的加载请求用加载队列进行维护,每帧分配固定的时间进行加载和实例化,比如10ms,超过了则放到下一帧进行处理,核心代码也非常简单
1 | Queue<T> _reqQueue = new Queue<T>(); |
上面的算法虽然可以实现基本的分帧需求,但问题也很明显:
1)单个资源的加载和实例化的耗时可能仍较大时会产生尖峰
2)单个资源的加载耗时已经较大时会产生尖峰
第一个问题比较好解决,可以进一步优化,为单个资源的请求维护一个状态,未加载/加载/实例化/已加载,在加载与实例化之前加多一步满载检测,满载则跳出循环,下一帧再继续处理
第二个问题其实是第一个问题的恶化结果,常见于场景加载,我们的处理方式是将场景资源分块导出成预制体,运行时再分块加载。这里有一个需要注意的点,需要确保场景在分拆为预制体后烘焙结果不受影响。我们用的是Bakery来做场景烘焙,烘焙后的结果可能存在对GameObject或Component的实例依赖(如下图),这种情况下做分拆会导致烘焙结果失效,这种情况的一个解决方法是将烘焙后的结果和依赖的实例打成一个预设。
- 业务层场景配置的彻底分块
战斗场景中除了有基本的场景资源外,经常伴随着刷怪,刷关卡物件等场景配置类需求,需要将对应的配置资源进行彻底分块,比如怪物A和物件A隶属于分块A等,加载分块A时将怪物A和物件A进行预加载,将配置资源的彻底分块有利于减少内存和CPU的浪费。
- 业务层的分帧加载
业务层除了资源的加载外,各个项目可能还有其他许多需要处理的逻辑,比如我们项目中使用了帧同步,需要创建相应的逻辑层和显示层,我们还将部分较为耗时的Component在导出资源时进行隐藏,加载后进行分帧激活,等等的逻辑,也需要做分帧处理。
其他优化
- 组件分帧加载
部分Component的启动生命周期耗时较高但激活顺序对结果没有影响时,可以将Component或GameObject在场景加载后,进行延迟分帧激活