针对这个近年最流行的设计要素,祖龙是这样解决技术实现难题的

来自 游戏葡萄 2020-12-10
深度

[ 游戏葡萄原创专稿,未经允许请勿转载 ]

针对这个近年最流行的设计要素,祖龙是这样解决技术实现难题的

整理/安德鲁

游戏里大世界要素的应用,正在成为很多厂商不得不学会的设计内容。

大世界/开放世界要素在近几年成为很多品类重要的支撑机制,传统的MMORPG、新兴的战术竞技,都离不开大世界对游戏玩法、体验的扩充。

但大世界在游戏中的实践应用并非易事。

利用虚幻引擎构建游戏大世界场景时,美术团队工作量会成倍的增加,快速创建出更真实的地貌,是其中一大难点。同时,程序上也会承受极大的运行压力,动态实时加载场景还要面临额外的优化挑战。

此外,游戏中构建大世界,也不可避免地会给中低端机带来运行考验。场景扩大后,游戏面数、内存及显存的增加,都可能造成严重发热。

1.png

对此,最先将虚幻引擎应用到各个品类的祖龙娱乐有一套自己的实践心得。前段时间结束的Unreal Open Day上,祖龙引擎技术总监王远明做了题为《祖龙娱乐使用UE研发开放大世界的实践》的分享。

2.png

针对大世界元素应用的难题,他谈到了祖龙的一些解决方案,比如采用第三方程序化生成场景工具,帮助美术快速生成场景以及后续的迭代及开发。

再如机型的分档,对低端机的场景使用LOD进行降级,缩短高精度场景的加载距离,使较远处的精度、面数比较粗糙,缩短Culling距离来减少面数的消耗。玩法上限制大世界的人数,将战斗场景放置在副本里等。

以下是王远明分享节选:

今天给大家带来的主题,是祖龙娱乐使用UE4研发开放大世界的实践,一个好玩的游戏应该具有超大的世界,它会给玩家带来无与伦比的真实体验。那么开发这样的大世界游戏有哪些挑战呢,今天我会给大家笼统地先讲一下,这个主题所涉及到的内容,还有解决方案。

3.png

解决方案提出之后,我们会做一下优化,主要是性能和内存。然后再讲解一下开发迭代,这其中包括一些工具,还有这个迭代开发的过程。

超大世界的MMORPG有很多的优势。首先它会有更真实的游戏体验。

4.png

我们平时玩的游戏如果场景比较小的话,那会有频繁加载卸载的过程,并且也不符合真实世界中的生活体验。大世界不管是视距也好、减少loading的频率也好,都会给玩家一种非常真实的体验。能减少loading的情况下,会让策划有很大的空间,来设计更丰富的游戏玩法。

大世界需要解决一些很实际的问题,首先是场景的创建编辑和开发迭代。不同于小的世界,美术可以手工去拉地形、刷纹理、铺植被,这些方式都可以解决。

5.png

大世界工作量非常大,对真实感的要求也非常高,如果还采用原始的那种拉地形、刷纹理的方式,会非常低效。那如何去开发大世界比较高效且比较真实?这是一个需要解决的问题。

6.png

这同时还带来一个问题,大世界一般采用分块的方式来做,这就会有多人协同的问题,以前每个场景是独立的。不同的美术人员来做的话,彼此之间的工作是独立的,不会有交集。但大世界不同的分块有相连的关系,这就要求工具上要处理好接缝的问题、协同的问题。

还有就是光照。原来小的场景光照数据比较小,光照烘焙的这种方式也比较简单。那么单独一个场景加载上来,下了几天之后就可以做一个LightingMap的烘焙,直接光、间接光的这种烘焙。

大型的场景分成了不同的区域,这样的方式就会产生一些问题,比如说同样一棵树,可能会投影到相邻的两三个或者更多场景块里面。这样在烘焙单一地形地块的时候,就需要做一些额外的特殊处理。

这还带来了一个问题,就是怎么进行迭代开发、二次编辑,还是同样的问题——小场景迭代非常简单,它不影响其他的场景。对于大场景的分块来讲,如果是编辑之后需要二次修改,怎么解决工作流、方便地更新、怎么使最终游戏的配置包尽可能小?这也是一个需要解决的问题

还有,大场景会带来大的植被功能需求——场景比较大的话,如果植被范围比较小,它也不是很真实,那么需要解决的就是如何创建和渲染出大范围的植被。

此外,还需要解决一个问题是场景的实时加载和卸载,不像小游戏,当你进入一个关卡的时候,整个场景是全部加载完成的——因为会走一个进度条——加载完成后你在游戏里玩,不管是打怪、做任务、还是聊天,已经不涉及到场景的加载了。那么遇到的加载无非就是特效、模型、换装这些,加载量是比较小的。

但是换成大场景以后,玩家运动过程中也会涉及到场景的加载和卸载,加载量非常巨大,会造成一些卡顿。当玩家在需要加载特效或别的模型的时候,可能会出现加载不及时以及贴图的问题,因为内存变大了,模型、贴图、地块都非常占内存,怎样解决这些内存也是一个问题。

同时还有远景,场景大了之后,为了营造出一个真实的感观,视野必须要放得无限大。不能说把远处的轮廓、山,通过一个视距的Culling裁剪掉。这样会有问题——当人物向前跑动的时候,远处的山、地形会逐渐进入到远平面以内——这会产生一个错觉,远处的山会慢慢生长出来,游戏体验会非常糟糕。

另一个需要解决的是运行效率,大场景意味着大模型数量,很高的面数以及带来的很高数量的DrawCall,还有CPU的消耗,场景管理的逻辑也会变得更加复杂。模型多了以后它的Tick、每帧需要处理的逻辑也会变多,这是CPU需要解决的问题。

7.png

接下来我为大家逐一地介绍一下,我们大世界的解决方案,针对刚才提到的每一个问题的解决方案。

首先,怎样创建真实地貌?以前小场景是美术通过画刷去拉出地形,然后通过随机数扰动模拟出来一个凹凸不平的地形。

这种真实感不足,我们现在用的是Houdini这样一个第三方工具来创建真实的地貌。Houdini非常强大,也许大家并不陌生,它是基于节点模式工作的,所有的修改都是一个节点,它的节点可以串行化并行化操作,经过层层节点不断地计算,会呈现出最终的形态。

那么当你修改了节点的时候,地形计算结果会实时变化。保存之后,它并不是最终的一个形态,而是把所有的节点都保存起来。这样当源头上改变最初输入——比如这个山体形状变得平坦了一些——那么这种变化,也会最终反映到所有节点计算完成后的最终形态。

第二,所有的操作也好、模型地形也好,都是通过扩展名为Hda的文件来保存的。我们解决方案中所有的操作,比如说我们去创建河流、创建某一个地表这样的操作,都是基于创建一个个的Hda文件。

用Houdini创建出大世界之后,我们最终目的是要在整个游戏场景里,把它分拆成很多的小块。做到逐一地加载卸载,这样就有一个大世界的拆分创建,也是我们需要重点解决的一个问题。

有了分块之后,我们的解决方案中就包含了分块加载和每一块的单独编辑。单独编辑每一块并且多人协同的时候,我们会发现不同相连的块,有可能是不同的美术编辑,那么这就遇到一个边界融合的问题,

我们也提出了相应的解决方案,有很多对象是跨分块的,并不在一个分块里。比如说路、河流,这些是比较蜿蜒曲折的,要求我们有一种方法能够对这种跨块的对象进行编辑,这也是接下来要提到的解决方案。

8.png

真实的场景,不光是需要地形地貌来体现出真实,而且还必须要有一套比较完善的、真实的地貌系统。我们改进了UE4的植被系统,可以和工具、和Houdini结合,来生成最终地表上的植被系统。场景是需要有LOD的,那么这在节省内存、节省DrawCall、提高渲染效率的方面非常重要。

当我们编辑好场景之后,也要有相应的解决方案来发布场景,之后还遇到一个问题是怎么样迭代。因为场景是不可能一成不变的,我们会介绍场景迭代的解决方案,所有的这一切做完之后,它其实就是一个完整的工作流。

大世界分块了之后,并不是一个静态的块,必须是由动态加载的机制加载到游戏里面去,才能够被玩家所看到、接纳。那么对于大世界的一个很重要的因素,是视距要无限的远,这样的话我们才可以能够有一望无际的体验。

9.png

这种情况下我们怎么去设计加载机制?要加载不同的LOD,涉及到内存的节省,那远处的地方是加载最差一级的LOD,较近的地方,要这样精度稍微低一点的地形块,最近的地方是加载全精度的。

然后我们实现的是一套全Lua的加载逻辑,并没有使用World Composition这样引擎自带的,运行效率其实是一个非常重要的问题。

10.png

相对于小的游戏场景来讲,这个问题倒不是特别突出,当场景大了之后,加载的多了,DrawCall数量就会多。因为加载了很多很多这样的分块,这些分块上面都有一些东西,是要为丰富这个场景服务的。它的DrawCall就会很多。

我们主要采用了Hierarchical Instance的方式,每一个分块里面最多有一种类型的Hierarchical Instance。这样就保证了这种方式渲染的物体,最多一个分块只贡献一个DrawCall。对于较远处的小物体、部分低精度的物体,我们是不投射阴影的。

还有我们会分类别设置不同的Culling Distance,我们光照的解决方案,是采用了动态和静态相结合的方式。

有一些游戏可能也是大场景,但是它是采用了全动态的。我们考虑到运行的消耗,想为玩家尽量创建比较真实的游戏体验。

那么比如树、远处的假山、石块都会有很多。如果我们也采用动态光照系统的话DrawCall会扛不住,我们采用的是静态加动态光照相结合的方式。远处采用完全的ShadowMap,近处是采用动态的阴影。我们没有采用LightingMap的方式,所以场景在户外其实是没有建立光照信息的。

大数据的创建和拆分,我们分为三种,三个层次的体系结构。

因为首先我们想象一下刚开始的工作流,要创建一个大世界的时候,肯定是要先把整个世界的轮廓创建出来。

11.png

这样我们会创建一个大世界.hda这样一个文件,这里面会有基本的形状,以及它基本的地貌的特征。然后我们会把这个大世界继续拆分成不同的区域,因为有些地方可能是雪地、有些可能是沙漠、绿洲。

分成不同的区域之后,我们会对区域的整体的风格会进行把控。拆分成很多不同的区域之后,同一个区域我们再继续拆分,分成最小的一个编辑单位,就是Tile.hda。这就是我们最小的单元,以及我们的最小的加载单元。

右边的这个图演示的过程,首先是大世界的一个Hda文件,它是最初世界的生成,最后它会被拆分成区域一、区域二、区域N这样很多个区域,每个区域再拆分成Tile1、Tile2、Tile3,每一个分块中一个Tile点,Hda将会对应到最终的UE4的一个ULevel,这是我们三层的解决方案,分化加载和编辑。

刚才是粗略地列举了一下,我们遇到的一些解决方案。下面会重点介绍每一个解决方案具体的一个实施的过程。

分块加载和编辑。刚刚说了最终的Tile.hda是我们最小的编辑单元和最小的加载单元Tile.hda,拆分以后将会对应UE4的一个level,这个level里面有一个叫UHoulandProxy。

12.png

这个Actor它是作为UE4对象和Hda共同交互的桥梁,在它的Proxy身上,我们会利用Houdini的各种插件提供的API,来编辑当前Tile.hda这个文件。编辑好之后通过Houdini的API,产生最终程序化的数据。这个最终的程序化的数据,再来刷新UE4的地形,这样通过UE4将地形地貌渲染出来。

需要注意的是,我们每次的编辑依然是以节点的方式保存修改,每一个节点就是一个以Hda文件形式保存的。一个Houdini的文件在这个Level里,按常规的方式,我们编辑Level里的其他对象——比如说在这个场景里放一个建筑、放一个光源,然后再放一些策划需要的东西,就是常规编辑方式的对象,我们是采用常规UE4编辑器的方式来编辑的。而地形地貌采用Houdini的方式编辑。

这是我们节点化的一个编辑器,当选中一个Tile.hda的时候,编辑的界面会出现出这样的界面,最上面是一个编辑地形的,还有编辑湖水、河流,然后修剪图,再修剪这个刷地表。所有这些,比如说我们的节点,像图里列到的Shape_ start, lh_curve_area_mask,这些节点都是对应着Houdini里的一种操作。

13.png

你可以把它想象成它是Houdini里编辑方式的简化版本。它提供的,只有给美术的若干Hda文件为基础的节点化编辑。

这种集中化编辑之后,每一个节点的作用都会用到这个 Tile.hda上,最终刷新Houdini里的数据,然后再用Houdini的数据,来生成UE4里我们看到最终的地形。

边界融合问题。因为我们多人编辑多个相邻的地块。那么地块之间会有一些不一样的地方。这些地方在编辑的时候是没办法保证的,那么要使用工具来做边界的融合。

14.png

边界融合分为这两个方面,一个是Tile的之间的融合,还有一个是区域之间的融合。

Tile之间的融合比较简单,它分为高度的融合和地表纹理的融合。高度的融合由工具进行自动高度的对接,高度对接上之后再做一些随机的处理。

地表纹理的自动过渡,就是左边这种地块和右边地块做一个混合的系数的从0~1的渐变区域的融合,会比较特殊一点,因为区域一般是场景风格差异比较大的地方。

除了这个 Tile的高度地自动缝合之后,我们会做一个比较,在高度上比较大的扭曲。因为Tile这个刚才讲了,也有一个高度的调整,不会很大。区域这个高度的随意扭曲会做的比较大一点,因为范围比较大,这样会给人真实的感觉。而不是区域里一个凹凸不平的地形,左边右边都是,唯独中间很平坦。这就比较僵了。

还有一个需要注意的是,这个两个区域之间的风格差异比较大,那么我们也要增加这种纹理过渡的范围,而不像两个Tile之间。因为两个Tile之间一般风格会比较相似,所以一两米之间的过渡就ok了。区域之间的过渡可能要放大一点,比如说10米甚至几十米这个可以有参数来调整。

下面给了两个示意图来演示这个Tile之间过渡的情况,你看左边这个图,它其实是简单的高度上的融合,没有做高度的调整。所以看起来其实很明显。有个绿色的边界,右边的图是做了一个高度的调整,也做了一个纹理的渐变,所以它看起来就比较真实。

15.png

再介绍一下跨分块对象。河流、海、路这些对象都是布置在一个分块里面。

那怎样编辑这些,首先我们也是离不开Hda文件的。在UE4里面有一个对应的Actor,它作为在UE4编辑器里提供编辑方式的图形化工具,你可以在这个对象上加入很多的节点,然后用贝塞尔曲线连出一条曲线出来,分别对不同的河流、海、路提供不同参数。

16.png

有了这些参数之后,这个Actor它会负责生成Houdini的数据文件,这些数据文件就是Hda文件。有了文件再调用Houdini的API,最终影响到大地形的生成。

比如路、河,其实路它不是一个新的对象、不是一个Mesh,它影响的是地表的生成。地表上通过贴图的混合,模拟出来一条路。这个是通过连的线还有各种参数,来影响地表生成的。

河流、海也是一样,河流也是需要在UE4的Actor里面负责编辑一条曲线,由这个曲线影响Houdini地形地表的生成。地表可以认为河床就是河底的一个形状,它会生成一个河道,河道底部的纹理是需要根据河流的各种参数来生成的,而河流需要生成一个Mesh,这个就可以理解成根据Houdini的信息。

我们主动生成一个Mesh,这个Mesh是和Houdini没什么关系的。但是它利用了曲线的数据。而河床的信息生成、地表上河床河道的生成是利用了Houdini的API,大范围的植被也会非常强地增加场景真实感。

17.png

由Houdini的程序化生成工具,这样功能来生成植被信息,然后由插件根据Houdini程序化生成的植被信息,来修改UE4的Landscape里面的Grass Type数据。

有了这些数据之后,用UE4原有的功能就可以生成每一个地表上的植被系统。比如说草、雪地或者是其他的一些植被。UE4编器里我们还要设计一些Grass Area,这个Grass Area是我们自己加入的一种类型。它用来决定哪里长草、哪里不长。

经过了这些比较复杂的操作之后,我们需要最终发布场景。开始的时候讲了,一个可编辑的Tile的Hda最终对应的是一个Level,那么这个Level里面其实直接操纵的是一个Hda文件,并不是可以用于发布到最终游戏包里的资源。

18.png

这就会涉及到发布过程,将Tile.hda上所有基于节点的修改应用最终产生的效果,然后用这些效果——比如说我们地表的混合的这种地貌、地表的形状,用这些数据形成UE4的相关的Uasset。这些Uasset就可以完全脱离Houdini的API,被UE4加载运行。

发布的过程,还涉及到计算Tile各个的LOD,因为一个Tile就是一个ULevel,那么它需要计算两级精度的LOD,LOD1和LOD2。LOD1用于在地块不太远的时候显示,LOD2就是一个纯粹的远景。

此外我们还要需要Cook每一个Tile的光照信息,最终做完之后,我们生成了每一个ULevel对应的发布的ULevel,这些ULevel可以最终打包发布。

发布之后,我们肯定还是需要对场景做修改,这就涉及到场景的迭代的过程。迭代的时候尽量不要修改整个世界的地貌,如果整个世界的地貌修改了也就修改了大世界.hda,那么就要涉及到重新批分、批拆。然后拆成不同的Area,再拆成不同的Tile。整个的工作流就会很长,影响面也会比较大。

19.png

如果确实有修改需要的话,我们这种供应链是可以支持的。因为修改完了之后。最终的Tile.hda编辑的时候,所生成的节点都是基于这个操作。最终在场景迭代的时候,对大世界的地貌、地形的修改。也是可以反映到最后的Tile.hda上去的。所以这也是我们使用节点方式操作,而不是保存最终修改状态值的好处。

文章评论
游戏葡萄订阅号