11月27日,由Epic Games举办的2020年虚幻引擎技术开发日大会正式拉开帷幕。会上,祖龙娱乐的引擎技术研究总监王远明带来了“厚积薄发,祖龙娱乐使用UE研发开放大世界的实践”的主题演讲。
以下为演讲内容(有节选):
一个好玩的游戏是具有一个超大的世界,这样的超大的世界它会给玩家带来无与伦比的真实体验,开发一这样的大世界的游戏需要哪些挑战呢?
首先我们先讲一些优化内容,主要是性能和内存。然后再给大家讲解一下开发迭代的过程,这其中包括一些工具。
超大世界的MMORPG它有很多的优势,首先它会有更加真实的游戏体验,因为我们平时玩的游戏如果是场景比较小的话,它会有一个频繁的加载和卸载的过程,并且它也不符合我们在真实世界中的生活体验。
我们讲的大世界,它不管是视距也好,减少loading的这种频率也好,它会给玩家都是一种非常真实的体验。并且在减少loading的情况下,它会让策划有很大的空间来设计更加丰富的游戏玩法。
如果要做一款开放大世界的游戏,会面临哪些问题?
大世界需要解决一些很实际的问题。
首先是场景的创建编辑。因为大世界不同于小的世界。小的世界你可以用美术手工去拉地形、刷纹理、铺植被,这些方式都可以解决。但是大世界它的工作量非常会非常大,并且它的这种对真实感的要求的也非常高。如果还采用原始的那种美术去拉地形、刷纹理的方式是非常地低效,并且不会带来很真实的体验。
并且还带来一个问题是大的世界我们一般采用分块的方式来做,这就会有一个问题是多人协同。像以前每一个场景是一个独立的场景,由美术的不同的人员来做的话,他们彼此之间的工作是一个独立的,不会存在有交集。但是大世界的不同的分块它是有这个相连的关系。这样就要求我们不管是工具上来讲经常来讲都要处理好接缝的问题。
还有就是光照问题。小的场景,光照数据比较小,它的光照烘焙的这种方式也比较简单。单独的一个场景加载下来,几天之后就可以做一个LightingMap的烘焙。
那大型的场景,由于分成不同的区域,它这样的方式就会产生一些问题。比如说同样的一棵树,它可能会投影投到相邻的两个或者是三个或者更多的场景块。这就需要在烘焙单一的地形地块的时候,做一些额外的特殊的处理。
还带来了一个问题就是我们怎么样去进行一个迭代开发,怎么样进行二次编辑。还是一样,如果一个小的场景,迭代是一个非常简单的事情,因为它不影响其他的场景。
对于大场景的分块来讲,如果编辑之后需要二次修改的话,怎么样去解决一个工作流,怎么样去方便地更新,怎么样能够使最终游戏的配置包尽可能地小,这也是一个需要解决的问题。
还有大的场景会带来大植被的这样一个功能需求。因为你场景比较大,如果你的植被范围比较小的话,它也是不是很真实的。这也是需要我们解决的就是如何创建和渲染出高范围大范围的这样一个植被。
大的场景的游戏还需要解决一个问题,就是我们的场景的实时的加载和卸载。它不像小游戏,小游戏当你进入一个关卡的时候,它的整个场景是全部加载完成的。因为会走一个进度条。当加载完成之后,你在游戏里不管打怪、做任务、还是聊天已经不涉及到场景的加载了。
相反换成大场景以后它会遇到一个问题,是玩家在运动过程中,会涉及到场景的加载和卸载,加载量是非常地巨大,这样会造成一些卡顿。并且当在加载特效或者是别的模型的时候,可能会出现一个加载不及时的问题。
贴图的问题也是,因为内存变大了,你的模型、贴图、地块,这些都非常占内存。
还有运行效率问题。场景大了之后,为了营造出一个逼真的感观,你的视野会必须要放得无限,不能说把这个远处的这种轮廓,通过一个视距的Culling把它裁剪掉。这样的话会有问题,当人物向前跑动的时候,你的远处的地形会变形,逐渐的进入到远平面以内,这样的话你会产生一个错觉是那种远处的山好像在慢慢生长出来,这样对于游戏的体验会非常的糟糕。
另外极大解决的问题就是我们的运行效率。因为大场景意味着大的模型数量、大的面数,以及带来的很多高数量的DrawCall和CPU的消耗,场景管理的逻辑会变得更加的复杂。并且模型多了以后,它的Tick,每帧需要处理的逻辑也会变多,这也是一个CPU需要解决的问题。
5种方式解决制作难点
接下来我为大家逐一地介绍我们的这个大世界面对这些问题的解决方案!
首先我们怎样去创建一个真实场景。之前小场景是通过美术去通过画刷去拉出地形,然后通过一些随机数的扰动来模拟出来一个凹凸不平的地形。这种真实感是非常地不足的。我们现在采用的是Houdini的这样一个软件工具来创建真实的地貌。
Houdini这个工具其实是一个非常强大的工具,它是基于一个节点模式工作的。也就是说它的所有修改都是一个节点。这个节点可以串行化并行化来操作,经过层层的节点的不断地计算,呈现出一个最终的形态。
第二个它是所有的操作也好,模型地形也好,都是通过扩展名为Hda的文件来保存的。我们在这个的解决方案当中,我们所有的操作都是基于创建一个一个的Hda的的文件。
比如说我们去创建地形的某一个地表的操作都是创建成为一个一个的Hda文件,我们在用Houdini创建出大世界之后,最终的目的是要把它在整个游戏场景里分拆成很多的小块,这样的话我们才能做到逐一地加载,然后逐一地卸载。这样的话就有一个大世界的拆分创建和拆分,这也是我们需要重点去解决的一个问题。
有了分块之后,我们需要的解决方案中就包含了分块地加载和每一块的单独的编辑。当我们单独编辑每一块的时候,并且多人协同的时候,我们会发现不同的相连的块有可能会被不同的美术去编辑,这就遇到一个边界融合的问题。
我们也提出了一个相应的解决方案,还有很多对象是跨很多分块的。比如说道路、河流这些它是比较蜿蜒曲折很长的,这就要求我们有一种方法能够对这种跨块的这种对象进行一个编辑,这也是我接下来将要提到的我们是怎么去解决这些问题。
真实的场景不光是需要地形地貌来体现出真实,它还必须要有一套比较完善的真实地貌系统。我们改进了UE的植被系统,可以和Houdini来结合来生成最终地表上的植被系统。这也帮助了我们节省内存,节省DrawCall,以及提高渲染效率。
编辑好场景发布之后,接下来我们会介绍我们场景迭代的解决方案,其中也会介绍我们的工作流。
大世界分块了之后,它并不是静态的,它必须是由动态的加载机制加载到游戏里面才能够被玩家所看到,才能够被玩家所接纳。对于大世界的一个很重要的因素是视距要无限的远。
在这种情况下,我们怎么去设计加载机制呢?一般我们需要加载不同的LOD,因为我们要涉及到内存的节省。我们在这个远处的地方是加载这个最差一级的LOD,拿这个和在较近的地方,精度比较稍微低一点的地形块进行比较。在最近的地方我们是加载全精度的,然后我们实现的是一套全Lua,实现的一个加载逻辑。我们并不会使用World Composition这样的一个引擎自带的加载逻辑。
相对于小的游戏场景来讲,这个问题倒不是特别突出。当场景大了之后加载的多了我们的DrawCall数量就会多。因为你加载了很多这样的分块,这些分块都有一些东西是要为丰富这个场景服务的,它的DrawCall会很多,我们主要采用了Hierarchical Instance的方式。
每一个分块里面,最多有一种类型的Hierarchical Instance,这样的话我们就保证了以这种方式渲染的物体,一个分块只贡献一个DrawCa,对于较远处的小物体部分的低精度的物体,我们是不投射阴影的。另外我们会分类别设置不同的Culling Distance。
我们光照的解决方案,是采用了动静结合的方式。
有一些游戏可能也是大场景,但是它是采用了全动态,但这方面我们是考虑到了运行的消耗,因为我们想为玩家尽量地创建一个比较真实的游戏体验。
比如说树这一物体,在远处的假山、石块都会有很多,采用全动态的光照系统可能造成DrawCall扛不住,所以我们采用的是动静结合的光照方式。在远处我们可以采用完全的ShadowMap,在近处采用动态阴影。因为我们没有采用LightingMap的方式,所以我们的场景在户外其实是没有的。
具体实操过程
刚才粗略地列举了一下我们的一些解决方案,下面我们会重点介绍一些解决方案具体的实施的过程。
分块加载和编辑,我们刚刚说了。最终的Tile.hda是我们的最小编辑单元和最小加载单元,Tile.hda拆分以后,将会对应上UE的level,在这个level里面有一个叫做UHoulandProxy这个Actor,它是作为一个UE对象和这个Hda共同交互的一个桥梁。
在它的Proxy身上,我们会利用Houdini的各种插件提供的API来编辑。当前Tile.hda这个文件编辑好了之后,我们通过Houdini的API产生的程序化数据来刷新UE的地形,然后通过UE引擎将地形地貌渲染出来。
需要注意的是,每次的编辑依然是以节点的方式保存修改,每一个节点是一个以Hda文件的形式保存的。一个Houdini的文件在这个Level里按常规的方式编辑Level里的其他对象。
比如说我们需要在这个场景里放一个建筑,放一个光源,然后再放一些策划们需要的一些东西,我们是采用常规的UE的编辑器来编辑。而地形地貌则采用Houdini的方式编辑。
值得注意的是,边界融合的编辑。
图中绿色的边界,右边的图是做了一个高度的调整,也做了一个纹理的渐变,所以它看起来就比较真实的。
在发布之后,我们肯定还是需要对场景做一个修改的,就涉及到刚才说的场景迭代的过程。
在迭代的时候,我们尽量不要修改,整个世界的地貌。因为如果你整个世界的地貌修改了就是说修改了大世界,就要涉及到Tile.hda的重新批分、批拆,然后被拆成不同的Area最后再拆成不同的Tile,整个的工作流就会很长,它的影响面也会比较大。
不过如果确实有修改需要的话,我们的这种供应链是可以支持的。因为修改完了之后,在最终的Tile.hda编辑时,所生成的节点都是基于这个操作的,我们最终在场景迭代的时候,对大世界的地貌、地形的修改,其实也是可以最终反映到最终的Tile.hda上去的。所以这也是我们使用节点的方式操作,而不是保存最终修改状态值的,达到省LOD的作用。
光照的解决方案,刚才介绍过了。具体实施过程,我们对光照主要是采用静态的ShadowMap、以及动态光照的方式,因为完全的动态,光照有比较多的DrawCall,我们为了追求真实的这种大场景,可能会承受不了。所以只能是在远处部分采用静态的ShadowMap。
而角色是动态的阴影,这里我们只使用ShadowMap,不使用LightingMap,所以我们是没有建立光照信息数据。
我们还要尽量减少动态点光源的数量。由于我们在游戏的过程中,我们会实时地加载分块,这样的话会造成一定的卡顿。不像小场景,我们是预先加载了所有的整个场景的,所以小场景的这种实时加载的工作量是比较小的。
而大世界不一样。首先我们没有办法做预先加载过多的内容,因为我们本身场景的内容已经非常多了。同时我们也没有办法做太多的Preload的内容,这就意味着需要尽量去让更多内容是实时加载的。
但是如果我们全部场景内容都采用实时同步加载的话,这个卡顿的情况会非常的严重。所以我们采用了异步的加载方式,即需要哪个就加载哪个,节省内存,解决卡顿的问题。
大世界还带来一个问题是内存压力变大,因为有很多的贴图,很多的静态模型,加载的块太多了。并且我们为了达到无限的视距,至少所有的块都要加载进来,还得是加载一个LOD这样的精度,造成我们的内存压力很大。
所以我们要做一个Mipmap的剔除,这是我们自己加入的功能,以此减少我们模型的种类。
贴图的内存占用量是非常大的,当我们做了一个Mipmap的剔除之后,我们的内存会得到很大的改善。
还有将资源的分级。涉及到不同的StreamingLevel对内存影响大的,我们可能走StreamingLevel的量要多一些,而对于内存影响少的我们就可能少些。
精度比较低的StreamingLevel我们就不加载了,能够达到省内存的作用。还有就是每一个分块做不同的LOD级别。然后通过改变我们的加载距离,当内存小的时候,我们应该加载LOD的就加载LOD,这样的话最终也能起到节省内存的作用。
今天的分享就到这里。希望以后,更多地跟大家交流。谢谢大家!
元宇宙数字产业服务平台
下载「陀螺科技」APP,获取前沿深度元宇宙讯息
110777025(手游交流群)
108587679(求职招聘群)
228523944(手游运营群)
128609517(手游发行群)