网络同步中, Package异步加载
背景
UE中加载机制是, 加载会有一个队列, 一个同步加载会flush掉前面所有的异步加载, 进而造成卡顿. 例如: 当前存在异步加载A和B, 此时我调用同步加载C, 会立即将A和B flush掉, 然后执行C.
UE NetAsyncLoad大致流程:
在TickDispatch(收包)阶段,
针对所有ExportNetGUID(这里就指其所对应的UObject, 下同)进行异步加载,
并将其填入PendingGuidResolves
中. 在接收Bunch中,
如果需要等待GUID加载或者QueuedBunch还没处理完,
就将该Bunch添加到QueueBunch中.
在TickFlush(发包)阶段, 解析GUID,
如果PendingGuidResolves
为空, 并且其他条件也满足了,
就会处理QueueBunches.
可以看到上述流程中, 如果PendingGUID都已经被解析了, 那么走的流程和正规流程没什么区别. 如果存在没有被解析的PendingGUID, 则会将Bunch放入QueuedBunches中.
关键变量
IsExportingNetGUIDBunch
1 | // 在对应的逻辑处理期间, 是否需要将被处理的Bunch视作ExportGuidBunch. |
当DS导出ExportGuid时候, 需要将该Bunch视作ExportBunch.
当客户端收到NetGUIDBunch时候, 也需要将其视作ExportGUIDBunch.
bHasMustBeMappedGUIDs
1 | // 该bunch是否存在在该Bunch处理之前必须被映射的GUID. 即处理该bunch之前,其内部使用的某些GUID是否必须被映射. 后面会紧跟着需要被映射的GUID列表 |
DS端处理MustBeMappedGUIDs
如果开启NetAsyncLoading, DS会将该Bunch(非ExportGuidBunch)中用到的GUID放入Bunch头部, 记作MustBeMappedGuids.
填充UPackageMapClient.MustBeMappedGuidsInLastBunch:
将MustBeGUIDs序列化到Bunch中:
客户端处理MustBeMappedGUID
当客户端收到的Bunch中含有MusMappedGUID时, 会将本地确实没有的但是还需要映射的GUID添加到UActorChannel.PendingGuidResolves中, 等待解析完成后处理该Bunch.
开启NetAsyncLoading
当开启NetAsyncLoading并且客户端收到新Bunch时, 会检测MustBeMappedGUID是否已经全部已经映射完成, 如果完成则处理Bunch, 否则放入QueueBunch中等待处理.
命令:
1 | static TAutoConsoleVariable<int32> CVarAllowAsyncLoading( |
在ini中配置:
在程序中调用:
DS处理
在序列化Bunch时, 如果存在还没被客户端ACK的GUID, 则将其放入ExportGUIDBunch中. 当开启NetAsyncLoading后, 在序列化Bunch时, 还会将其放入MustBeMappedGUIDs中, 最后再将MustBeMappedGUIDs放在Bunch的头部.
当开启NetAsyncLoading后, 在DS端, 如果该Bunch并不是ExportGUIDBunch, 并且GUID需要客户端加载, 则将之放入MustBeMappedGuidsInLastBunch中.
哪些GUID又需要客户端加载呢?
- 静态GUID
- 非地图GUID
- 非NoLoad的GUID(不加载只find的GUID)
将MustMapGUID序列化到Bunch中:
最后, 将Bunch发送给客户端.
客户端处理
UE NetAsyncLoad大致流程:
在TickDispatch(收包)阶段,
针对所有ExportNetGUID(这里就指其所对应的UObject, 下同)进行异步加载,
并将其填入PendingGuidResolves
中. 在接收Bunch中,
如果需要等待GUID加载或者QueuedBunch还没处理完,
就将该Bunch添加到QueueBunch中.
在TickFlush(发包)阶段, 解析GUID,
如果PendingGuidResolves
为空, 并且其他条件也满足了,
就会处理QueueBunches.
异步加载资源
在处理ExportGUIDBunch
时候,
会调用UPackageMapClient::InternalLoadObject
,
在里面会触发FNetGUIDCache.GetObjectFromNetGUID
,
进而调用FNetGUIDCache.StartAsyncLoadingPackage
,
即触发异步加载.
反序列化ExportGUIDBunch里面所有的GUID:
如果开启NetAsyncLoading
, 会触发异步加载.
注意:UPackage加载完成后并不设置FNetGuidCacheObject.Object
,
而是在UNetConnection.Tick
中每帧重新查找(StaticFindObject
)绑定.
将GUID添加到PendingGuidResolves中
DS收到的Bunch不允许为bHasMustBeMappedGUIDs类型Bunch
DS不允许有bHasMustBeMappedGUIDs, 否则直接报错, 返回. 因为DS端是Guid的配置者, 它不可能收到新的GUID, 所有GUID由它分配.
客户端将GUID添加到PendingGuidResolves
当客户端收到MusMappedGUID时, 会将本地确实没有加载的但是还需要映射的GUID添加到UActorChannel.PendingGuidResolves中.
将Bunch存入QueueBunch中
满足以下条件, Bunch会放入QueueBunch中,等待处理.
- PendingGuidResolves已经有了待解析的GUID.
- QueuedBunches中还有待解析Bunch.
- 想要ShutDown的Actor, 但是在处理一些事情之前还想处理bunch.
- Driver范围内需要针对该Actor进行QueueBunch
处理QueueBunch
清理PendingGuidResolves中元素
客户端在UNetConnection.Tick
时,
每次查询GUID对应的Obj是否存在, 如果存在进行绑定,
并删除PendingGuidResolves中的元素.
通过StaticFindObject
查找GUID对应的Obj, 然后进行绑定:
处理QueueBunch
当条件都满足时, 就可以处理Bunch了.
注意
通过上文可以发现, UE并不是针对每个Bunch单独记录其MustBeMappedGUID, 而是统一记录, 直至清空. 这样做虽然简单, 但是会产生一个问题, 就是如果一直有需要MustBeMappedGUID的Bunch, 那么客户端就会阻塞Bunch的处理, 这个可能引发连锁反应, 就是虽然不卡, 但是游戏仿佛停止了. 目前, UE是针对ActorChannel做Bunch的阻塞, 即不同AcotrChannel是相互独立的.
UActorChannel.QueuedBunches
归属于ActorChannel.
UActorChannel.ProcessQueuedBunches
也是在ChannelTick中执行的.
即Bunch已经存储在对应的Channel中了.
进一步优化方法: 针对Bunch, 单独记录其MustBeMappedGUID
,
load完一个, 处理一个, 这样可以保证游戏正常运行.
构建复现环境
首先, 编辑器下无法复现. 原因: DS和客户端同处一份环境中, 资源加载是共享的, 所以无法复现DS加载但是客户端还没加载的情况.
了解了原因, 可以专门启用命令行DS, 然后客户端一定要保证没加载过该资源, 例如第一次启动编辑器, 并且默认地图的相关引用没有引用到该资源.
在一个空场景使用Standalone方式启动客户端连接DS.
DS端专门创建了一个Actor, 并开启Replication.
1 | if (LocalFrameIndex==10) |
客户端下断点, 并连接DS.
总结
- NetAsyncLoading仅仅在客户端网络层触发异步加载. 在DS端是不需要的,
因为DS不会存在网络层加载UObject的情况,
一般都是主动Load后
Spawn/NewObject
. - 需要异步加载的Guid对应的资源全部都是静态的, 并且是客户端可以加载的(非Level, 非NoLoad).
- 开启NetAsyncLoading, DS会将MustedMappedGUID放入Bunch(非ExportGUIDBunch)头部, 用bHasMustBeMappedGUIDs表示.
- 开启NetAsyncLoading, 客户端会异步加载GUID对应的Package(当前还没加载), 在此期间收到的Bunch都放入BunchQueue中, 等待GUID对应的Package加载完成后处理.
- 必须等所有MustBeMappedGUID都处理完成后才会处理BunchQueue.(ActorChannel级别的阻塞, 不同ActorChannel是相互独立的.)
- UPackage加载完成后并不设定
FNetGuidCacheObject.Object
, 而是在UNetConnection.Tick
中每帧重新查找(StaticFindObject
)绑定.