UE4 大世界场景中的动态物体管理方式
距离上次写东西大概有差不多半年多了,一直没有时间写(这篇文章已经在草稿箱半个月了,主要还是自己懒 。。。),今天刚好有点空闲时间,加上最近一段时间踩了 LevelStreaming 的坑,所以打算总结一下自己之前搞的一套大世界场景下动态物体的管理方式,顺带过一下动态物体的同步方式。
游戏中的动态物体
游戏中一般会有各种各样可见的Object,比如墙、石头、载具、路牌等等。
- 从游戏玩法角度来看,这些物体可以分为可交互的和不可交互的,比如在绝地求生游戏中,可以与玩家驾驶的载具进行碰撞的木栅栏、玩家死亡后留下的盒子等等。
- 从游戏表现的角度来看,可以分为需要同步的和不需要同步的,例如绝地求生中的木栅栏,它本身和游戏玩法无关,不同客户端上可以允许存在不同的表现,你的小伙伴开车撞烂了一个木栅栏,你的机器上这个木栅栏是否被撞坏完全不重要。
动态物体的同步方式
1.同步输入
UE4中刚体的碰撞一般走PhysX流程,可以通过NotifyHit事件捕捉到碰撞信息(例如法线、碰撞点、冲量等),若想在PhysX底层进行碰撞的处理,比如修改弹性系数、物理材质、甚至是过滤碰撞等,需要使用PhysX的contactModifier接口,这里就不详细讨论了。为了减小模拟物理带来的开销,也可以通过施加Impulse的方式来模拟碰撞。
同步输入指的是每次物体受到玩家或者其他物体的输入,比如位移,碰撞等。DS(Dedicated Server) 需要下发该消息到每个客户端,其他客户端再进行表现,碰撞判定可以在DS上进行也可以下放到Local Client。同步输入有一个优点就是流量比较小,只有当物体状态被外部改变了才需要同步,缺点是不同客户端模拟同一个输入由于浮点误差很容易出现不一致的情况(即便是走PhysX流程,因为PhysX的计算依赖于DeltaTime,不同客户端模拟时DeltaTime往往不同)。
2.同步状态
同步状态比较简单,本地客户端每一帧将物体状态(Transform、Linear velocity、Angular velocity等)同步到DS,接着DS再将物体状态Replicate到所有客户端,该同步方式优点是不会出现不同客户端不一致的情况,因为所有客户端的位置都是DS下发的。但是缺点是流量大,因为每一帧都要同步,而且如果没有优秀的插值平滑算法(单独为了这东西搞一套同步机制成本太高,如果可以的话一般会考虑复用主角同步的机制),拉扯是不忍直视的。另外,如果引起碰撞的客户端掉线,则会导致其他客户端出现表现中断的情况,比如被撞飞在空中突然卡住不飞了。
3. 同步方式的选则
主要还是看项目的具体需求以及对性能的要求,手机游戏一般不太可能同步大量物体的状态,所以采取同步输入,客户端各自表现的做法比较好,当然如果该物体需要和玩法相关那可能就要考虑同步状态了。
大世界场景的LevelStreaming
在大型3A游戏中(比如GTA系列, Forza horizontal 系列,Far Cry 系列等),玩家可探索的地图非常大,大到甚至需要花费几十个小时才能完整探索。在这种游戏中,即使性能强劲的PC也不可能一次性将整个地图加载进内存,当然这也是没必要的,LevelStreaming的出现就是为了解决内存不够用的情况,它的思想类似于操作系统中的虚拟内存机制,后者通过换出暂时不需要的页面,换入当前需要的页面来将资源动态加载进内存,LevelStreaming通过将大地图分块,当玩家要进入需要加载的地图块的时候才会进行地图的加载,当玩家离开地图块的时候再进行卸载。
在UWorld类的声明中我们可以看到:
一般一个World中必须要有一个PersistenLevel,若干个StreamingLevel。当玩家到达StreamingLevel的加载分界点的时候,UE4会自动加载相应的Level,并且卸载掉离开的Level,在被卸载的Level中的所有Actor都会被卸载,并调用其EndPlay来通知Actor。
为了在逻辑层区分一个 Actor或者Component是否在LevelStreaming中被卸载,在EndPlay的参数EEndPlayReason中可以判断:
LevelStreaming 引起的问题
在吃鸡游戏中,玩家死亡掉落的盒子是游戏中一个非常重要的可交互物体,因为盒子与玩法相关,其承载了玩家所收集的资源,所以无论是否有LevelStreaming,都必须保证掉落在地上的盒子不能因为关卡的加载和卸载而消失。在吃鸡中,这个问题比较好解决,因为DS不会发生Streaming,所以由DS来同步盒子的位置和包含的物资即可。
但是,如果这个盒子是客户端本地模拟的怎么办呢?也就是说每次盒子由于LevelStreaming从内存中卸载,下次再加载的时候,盒子已经被重置为初始状态了,这个时候就需要自己搞一个Manager来维护所有盒子的状态了,为了能够查找到盒子对应的状态,还必须维护一个GUID来索引每一个盒子,当盒子被Streaming loaded的时候,恢复当前状态。
关于LevelStreaming的一点思考
笔者本人对于需要自己维护GUID来索引场景物体的做法一直有疑惑,LevelStreaming讲道理对于大型网络游戏或者单机游戏是一个比较常用的东西,难道UE4引擎层面没有提供一定的序列化机制来保存当前被streaming的UObject的状态么?
如果可以利用UE4引擎原生的序列化机制,开发者只需要override serialize()接口来序列化需要保存的数据,该UObject在被重新加载的时候反序列化恢复出来即可,但是笔者在阅读引擎源码的时候发现,UE4引擎好像并没有这么做,我们可以看到USceneComponent中的Transform信息是没有被标注UPROPERTY()的,这也意味着它不可能被序列化保存起来,也就是说Actor的位置信息在Actor被Streaming out的时候并没有被序列化。为什么引擎没有相应的机制来让开发者不用手动维护和管理被streaming的对象呢,这一块笔者后续会抽时间好好研究一下。