欢迎光临散文网 会员登陆 & 注册

[UE5]使用C++批量管理StreamLevel(上)

2023-09-24 22:53 作者:Epslin  | 我要投稿

说说前情

在智慧园区开发过程中,经常有要切换StreamLevel(以下和“流送关卡”叫法混用)的需求,比如聚焦到某个建筑时加载建筑内部结构,使用流关卡,能避免在UE打包文件刚运行时就加载99%的时间都用不上的内容,实现按需加载。同时,还有一些功能需要我们基于某些包含多个场景的“预设”额外再加载卸载,通过对流送关卡做一个管理系统将“预设”与当前已加载的关卡进行对比,可以避免重复的加载卸载,实现精准控制。

用蓝图管理会有什么问题?

那么该怎么搞这么一个管理系统呢?使用过蓝图的朋友都知道,蓝图内默认的StreamLevel管理函数比较孱弱,甚至不能原生加载多个关卡。如果在For循环中连续调用LoadStreamLevel,会发现只响应了其中的一个,所以就需要一些奇妙的写法来实现多个关卡的加载。

蓝图中“手动Loop”递归实现多个关卡加载

但这样会有什么问题呢,对于蓝图的异步逻辑,如果有邻近的两次调用,比如用户在两个都需要切换的模块间切换,就会发现事情再次坏了起来。

如果A还没有完成卸载,B就要加载,就会概率触发加载卸载不完全的问题。那么又需要写个检测函数,等A完成后再告诉B你可以进行加卸载了,这对业务模块的编写搞出了很多心理负担,又或许你想除了这样更加“奇妙”的写法。

问题更大的奇妙写法

另外的问题就是,在加载完成后需要告知加些在申请的模块,常见的写法就是用委托/事件分发(EventDispatcher),此时有一个更加危险的事情是蓝图中可以调用UnbindAllEventsFrom,万一同事一不小心调用了这个,那其他同事的功能很有可能被误伤。心理负担++。

所以该怎么解决这些问题?

  • 对于奇妙的Loop写法,也可以调用C++函数使用简单的For循环完成。

  • 对于通知问题,可以定义一个接口类(Interface),需要加卸载时先把要告知谁传进来,如果这个告知目标实现了这个接口,在处理完对应的请求时就通过接口通知它。

  • 对于两个十分接近的加卸载请求,可以使用队列,在A的请求没做完之前,把B的请求先压入队列,依次处理。UEC++中提供了TQueue<>数据类型但不能暴露给蓝图。

考虑到不想在蓝图中实现队列(懒)和蓝图和C++不同模块的兼容性,使用C++编写关卡管理内容和接口有更好的通用性。Subsystem作为可以储存变量又不需要Cast的全局对象,在使用起来更加简单,也可以帮其他同事减轻记忆负担。

在C++工程中创建WorldSubsystem,开整。

准备测试场景

这里我们准备了测试场景,每个Mesh处于一个单独的StreamLevel,其中奇数结尾的关卡默认为加载状态,偶数结尾的关卡默认为不加载状态。这样我们可以直观了解各个流关卡的加载状态。

测试场景-编辑器下

测试场景-运行状态

如何加载/卸载多个流送关卡

加载流关卡的核心函数是UGameplayStatics::LoadStreamLevel,这个函数需要依次传入WorldContextObject、关卡名称、加载后是否可见、是否阻塞加载、用于加载完成回调的 FLatentActionInfo。

前几个参数其实都很好解释,主要在于最后一个参数 FLatentActionInfo,也是后文中会提到的“回调地狱”的源头。但后边的事后边再说,先写一个小函数来测试,创建TryLoadSomeLevels函数如下:

在调用LoadStreamLevel的时候传入上边定义的FLatentActionInfo,在加载完成后会调用第三个参数中指定名称的函数,这个函数需要使用UFUNCTION()修饰以加入反射系统。在蓝图中尝试调用TryLoadSomeLevels

蓝图调用
调用前
调用后

发现还是只调用了一次,回调函数的Log也只打印了一次,和最早提到的简单for循环蓝图方法没有区别。

问题的源头在于FLatentActionInfo,如果传入的UUID相同,便只会执行一次,为了实现多次执行就需要把UUID在循环中每次变更,我们可以在循环里做如下更改:

这边还有一点需要额外提示,在构造FLatentActionInfo的时候,我选择了带参数的构造函数而不是默认构造,在尝试过程中,发现默认构造有时会没法修改UUID,比如下面的代码,在后续想修改其中参数时,就发现并没有修改成功。

默认构造值修改失败


再次尝试调用,发现已经能加载传入的全部关卡了,同时回调的函数也被执行了关卡数对应的此次数。

循环中更改UUID后调用结果
回调函数Log打印

那我们再试试卸载,同样的方法更改一下调用函数:

蓝图调用
执行成功
回调函数Log打印

从上图我们可以发现,C++中的逻辑已经能按我们的想法加载、卸载多个关卡了。我们的初步目标已经完成。

告知调用者加载完成

由于加载卸载都是异步过程,需要在加卸载完成后才能执行的行为就依赖加卸载管理类的通知,告知后续模块可以加卸载行为已经完成,可以进行后续操作。在尝试过程中使用接口和多播委托的方式都有可以起到目的。这边我们分别演示一下。

首先需要对完成回调计数,当全部完成后才告知目标。因此需要创建用于计数的变量,当全部完成后调用委托。

然后在蓝图里绑定即可使用:

蓝图中绑定委托实现通知
成功打印通知消息

但是委托的问题在于每次使用都需要绑定解绑,不但增加记忆负担而且容易被误通知、误解绑。使用接口就更方便一些,下面演示一下使用接口通知。

首先创建接口TrySwitchLevelAction

为了照顾通知其他对象的需求,在调用前需要传入需要通知谁,因此在上边委托的修改基础上,再次对Load、Unload、完成通知函数进行修改

完成之后到蓝图中实现接口:

在调用者中实现接口

因为传入的NotifyTarget并不限制为自己,可以通过传入其他对象来实现跨类告知,这里创建了另一个蓝图同样实现这个接口,在收到通知时通过修改PrimitiveData实现材质颜色的修改:

在其它类中实现接口

当收到通知后,会将自己的PrimitiveData设置为1来影响材质

使用PrimitiveData驱动颜色变化
在调用者中传入需要通知的对象

调用前

未加卸载关卡、没有收到通知
加卸载完成收到通知
两个对象都受到了接口调用

接口的缺点在于对于异步任务需要传入非const的对象指针,在C++编程使用中会增加被对象被修改的风险。

小结

在上面的尝试中,我们已经大致搞定了多个流送关卡的批量加载卸载和通知自己或其他对象,但是每次都需要在蓝图中创建两个节点还是太麻烦了,尤其对于多播委托,甚至还需要再搞个计数器,实在是不够便捷。

在下一篇中,会讲解一些进阶些的内容,主要包括:

  • 尝试把加载卸载放在一起统一处理,让调用过程更加简洁;

  • 把要加卸载的关卡和当前关卡比较,避免不必要的加卸载调用;

  • 使用队列引用高频的关卡切换请求。



[UE5]使用C++批量管理StreamLevel(上)的评论 (共 条)

分享到微博请遵守国家法律