分享

大世界技术浅析,WOW的梦幻影歌!

 秋刀录 2020-09-24

2005年开始做MMORPG的时候,国内游戏行业还刚刚起步,市场上大部分游戏出产自韩国,唯一一款火爆的3D游戏奇迹,正因为外挂猖獗而摇摇欲坠。主流的游戏市场上由盛大代理的传奇还光焰照人,但陈天桥在那一年不吝宣发费用所推广的却不是什么复制传奇的MMO,而是一款纯粹女性向的游戏——《梦幻国度》。伴随着这款游戏商业化的失败,玩家和从业者都在骂,骂盛大眼光短浅骂的震天响。”女性向游戏”这一母题尚需更多时日在国内游戏行业里酝酿发酵,而后分裂出以Q萌、美型吸引女性玩家渗入再而吸引男性玩家的MMO,或是以宫斗、Avatar、恋爱为主旋律而满足女性玩家日常情感消费之需“她游戏”。时至今日,大概不再有人怀疑“女性向游戏”是伪命题,而陈天桥的眼光前瞻性和壮为先烈这一事实,却在后来的机顶盒战略上,还会继续重演。那一年的《梦幻西游》和《大话西游 II》,做为网易最好成绩的网游,它们的成绩不过PCU 75万。

那年5月,魔兽世界(WOW)测试,技术出身的PM要求人人都魔兽世界必须40级,不限职业,公司全额报销点卡。年初1月刚进公司的时候,用的还是自制脚本,等在WOW中发现他们用的是Lua的时候,项目组不惜成本的丢掉了自己做的一整套脚本和编辑器,转向Lua。Lua之于国内游戏行业首席脚本的地位,当由WOW所确立。其世界观自魔兽争霸以下,大多承接自DND的种族设定,其叙事却因史诗般的宏大而震撼人心。WOW在体验上不光带来了40人低容错的超大团队副本,也带来了它巨大的野外世界及野外世界来自部落和联盟的血腥厮杀,从荆棘谷到南海镇,从阿拉希到黑石山口无乎处处都是热血疆场,这一巨大的世界在体验上和传统MMO不同,它在场景连续行进过程中不需要切出Loading界面,它是一个体验中无缝连接的世界,无缝世界。一个游戏,影响了一个行业,跨越一个时代。

MMO史不遑多论,我想扒的是大世界的基础技术框架。而既已题名梦幻,是因为诸多因由看法为一家之言,有些做法多次上线验证,但因团队和个人能力有限,或未及远虑而漏洞百出,另一些不过是一些闲下来几个好友小斟微酌,茶余饭后的日常YY,其或无实用之功,或离真实的问题南辕北辙而失之万里,权且聊作笑谈。

客户端——地图加载

游戏的技术问题,从整体上来说大体上可以归结于一些子问题集合,问题本身的解决方式就对应着这个子集的求解。这些问题的求解一方面依赖于对游戏系统本身的理解及其模型建构的契合度,另一方面又依赖工程师对于数学、算法和软硬件的熟悉程度。而做为游戏客户端程序员尤其是引擎程序最大的苦恼无外乎两条:一条是实时图形只求解各类问题的次优解或看起来像的解,但这些求解又不具备基本的艺术性,从而不具备永恒存留的可能,另一条则是硬件厂商遮遮掩掩总是让人把大量的时间花在对问题解决毫无意义的硬件工作原理的猜测验证上。

大世界最容易被注意到又最早被广泛以各种方式解决的技术挑战是地图加载问题。显然,当地图上资源总量超过运行内存总大小的时候,我们就没办法一次性把地图资源加载到内存。资源按需加载的需求应运而生。

第一个被广泛应用的地图按需加载策略是:

九宫格地图加载策略

九宫格

如上图所示,红点表示当前玩家所在地图块,周围8块浅灰色区域表示同时加载在内存中但不显示的地图块,深灰色的区域表示的是存储在磁盘上的地图块。

当角色从红块移动到橙色区域,右侧的3块蓝色区域会被加载到内存,左侧的3块黑色区域会被从内存中卸载,从而保持内存中一直只持有9块地图。

上述过程是九宫格加载核心的思想,在实际应用中需要平衡分块大小、内存占用量和磁盘IO的速度和频繁程度。

大多数2D游戏中九宫一格等于一屏的大小,而3D游戏中则总是取一个和视距相关的值,而且块数也不限于九块,也可能变种为加载5*5或7*7格,格子的边界也可能从硬边界扩张为软边,这样当角色在边界上来回晃动的时候不反复触发加载和卸载。

UE4所提供的大世界分块组件:

WorldComposition地图加载

Epic的UE4提供了一个大世界加载策略——WorldComposition,它的功能简述如下:首先把世界划分许多层,每一层包含一些关卡也包含一个配套的加载策略——XY平面的加载距离;它的关卡划分和面积无关,也无需等比。

如上图所示,世界地图被拆分为7个关卡,3个红色关卡(A,B,E)和4个橙色关卡(C,D,F,G)表示的是每个关卡所覆盖的范围,容易看出他们的面积不对等。

WorldComposition还允许把不同的关卡设置到不同的层中,而因为每层可以有不同的加载距离,这就可以实现不同的类型的子关卡加载距离不同的策略。比如地形,建筑、大型植被、结构性地图物体放到500米就加载的层中,而花草、小物体则放到200米才加载的层中,就可以很容易实现只加载近处的花草和小物体,而远处则只加载影响整体观感的地图结构性组件的策略。

当我们把WorldComposition只使用一层,而且每个关卡的划分都是等面积的,它就退化成了经典的九宫格地图加载方式。

基于触发器的地图加载

对于地图两点间大多数存在通路的情况下,WorldComposition和九宫格不失去良好的加载策略,但对于两块区别之间间只存在极少通道和可见性的情形,则上述加载方式就存在很大的资源浪费,这一类的地图如地下城、室内陆图、山洞、管道等迷宫式的设计。

如上图所示的地图,红蓝区域相互之间既不可见也不存在快速通道,所以当玩家活动在约色区域时不需要加载蓝色区域;当玩家从红色区域走到绿色箭头处的时候才开始加载蓝色区域,同样,红色区域相对于蓝色区域来说亦是如此。这样就可以在上边的绿色箭头处放一个触发器,当玩家触发该触发器时,加载蓝色关卡,在下边的绿色箭头的过道放一触发器用于加载红色关卡。

触发器除了可以用于地图加载,也同样可以用于地图卸载。

UE4同样也提供了一个触发式地图加载功能组件——LevelStreamingVolume。

资源加载的基本假设

从抽象层面上看,分层分类的地图加载策略 触发式的地图加载似乎已解决大世界的地图加载问题。但在实际的资源加载应用中,其表现如何呢?

答案是远远不够,第一个碰到的问题是:世界上没有两片相同的树叶,一个人也不能两次步入同一条河流,而资源加载在不同的运行环境(包括硬件和软件)下的时间也大相径庭。而在上述加载策略中,关卡的划分是静态的,所以他们在同一输入条件下,其加载的资源总量不会动态发生改变。

要使玩家在移动过程中远处不出现空洞和突然刷出物体,需要满足以下不等式

即需要在物体显示之前,完成对它的加载。式中左端和可用加载距离成正比,和加载速度成正比,和玩家移动速度成反比,而左端需要加载的资源总量静态确定。加载速度则依赖运行环境。

要满足上述不等式,不外乎改变上述三方的值:要么加大加载时长,要么加快加载速度,要么缩减必须加载的资源量。事实上这三条就是资源加载优化的出发点。

  • 在内存可控的情形下,可以适当提前进行资源加载,即内存中保持一定的冗余加载量。这可以在关卡设计时,就保持在更远的距离进行资源加载。最理想的情况下:保证即使支持的最低硬件配置也能在加载时间内可以加载完所有必须的数据。

  • 加快资源加载速度,资源加载链路可用的优化包括:优化数据存储、IO模型,尽可能减少数据解析和复制的开销、并发执行等。

  • 减少必需资源总量,这一部分可用的优化包括:优化资源重用、减少不必要的资源冗余、资源加载优先级、未加载完成时使用占位资源等等。实际上不管在使用何种方式划分关卡,总存在资源冗余的情况,而如果使用九宫格式的资源加载,其冗余资源占有量和格子大小的成正比。

地图加载的常见优化手段

资源代理

故名思义,就是当资源未完成加载的时候使用一个全局的资源占位符做为它的代理对象存在。早年的WOW在其它角色未完成加载的时候,先显示了角色名称和他脚下的一个椭圆形的小黑影用于标识该角色的存在。

对于大多数显示数据而言,其几何数据相对于纹理和Shader数据加载往往更快,也存在有游戏在加载完几何Mesh先显示白模的做法。

而对于UE4来说,其做法更进一步,它支持了一个叫做Level Lod的特性——它允许你将一整个关卡使用一个简单的模型做为它的代理。这个模型在几何表示和材质上可以随意简化,就可以达到快速加载显示而不出现场景空缺一块的问题。

加载优先级

和代理资源一样,加载优先级同属于减少必需加载资源总量的范畴,它认为资源的高级LOD一定比低级别的LOD数据量更小加载更快,故它优先加载显示资源的最高一级LOD的几何数据和纹理数据并提交给显示层。而后继续加载当前所需要的最恰当的这一级LOD的几何数据和纹理数据,当它们都加载完成之后才切换到正常的LOD级别。

因为这一过程往往耗时在毫秒级,所以对用户的体验来说顺滑无感知。

IO模型

对于大数据量的文件读取来说,使用内存映射大多比直接文件IO要快50%以上。而对于写文件来说,是否使用带缓冲的文件读写API,单次写入文件的Buffer Size不同,其效率也可能会有倍数级的差距,大多数设备的文件写入API在写入Buffer大小约为内存页面大小时效率最佳。

数据存储——格式与顺序

因为现代计算机硬件结构的原因,文件的顺序读取速度远超随机读取速度。如果资源在使用中的关联性和其在磁盘中存储关系的临近性相关,则其命中页面缓存的可能性就高,读取性能就高,反之则可能出现缺页的情形。针对这一情形,可以把资源在存储中的临近性按到其在地图中的空间临近性进行存储。但这样会带来一个权衡,如果一个资源在多处或多张地图中同时被使用,则如此存储可能带来一定的磁盘资源冗余。

数据存储的另一个可考虑点是选取恰当的数据格式,数据格式的优化至少包含两部分:一是压缩和加密方式的选取,另一个是单个资源存储格式的选取。

传统引擎大多采用zip文件压缩整体资源包,但实际上zip在压缩比率和解压效率上都比不上zstd。而如果适当损失一些压缩比率,则lz4的解压效率则数倍碾压zip。商业压缩库oodle号称速度更快,但其价格贵让人自惭形秽。

而对于资源本身说,其格式解析过程也往往会占有很大一部分耗时。最优解是不需要解析,存储的数据格式就是使用时的内存结构,其次是解析的是二进制的数据,其效率要远优于解析文本格式数据,这一点在配置文件和显示数据格式的设计中,对性能影响尤为可观。

并发——利用多核

现代游戏运行的硬件已消灭了单核的存在,资源加载过程有效的利用多核是现代游戏资源管理系统的基本要求。

一般来说因为IO总是慢的,所以IO大多在工作线程中执行。

大多数逻辑相关的数据、配置文件都不存在多线程依赖关系,他们的数据读取、对象序列化都可以在工作线程里完成。

显示数据的显示资源初始化在很多设备上需要在单独的渲染线程进行初始化,所以它们加载往往需要分成三步:IO,对象序列化和初始化、渲染线程的显示资源初始化。谨慎处理好三者的关系既能提高加载速度,又可以减少游戏卡顿的发生。

减少加载过程中的数据拷贝

在最理想的情形之下,数据格式和驱动层所需要的格式一致,至多只需一次内存映射和一次数据拷贝到显示专用内存(或显存)即可达成数据初始化之目的。

但大多数游戏或游戏引擎的处理都存在这样或那样的冗余步骤。如UE4加载一个模型的顶点数据,从IO层到真正创建VertexBuffer还可能经历高达4次数据拷贝。

早在10几年前,做高性能服务器的网络工程师就在尝试Zero-Copy方式的网络消息收取和转发……

中我们从宏观上介绍了几种可用于大世界的地图分块、加载策略和常见的优化手段。本文将深入细节,探讨大世界地图上的主要的资源分类,及它们的加载粒度和策略。

世界

在玩家的眼中,游戏世界是这样的:丰富多变的地貌、美轮美奂的光线、一望无际的落叶林、到处耸立的玉宇琼楼、傻乎乎的NPC,一些分布的采集点、定时刷新的怪物和宝箱……

在游戏程序员的眼中,世界是这样的

大世界,是这样的

东西

世界由许多关卡组成,关卡由“东西”组成,东西是世界上的基础的构成,像真实世界中的分子,它们由原子组成,却因原子的不同组织方式,表现出自己独特的物理和化学特性。这些东西在游戏建模里有时被叫做'Entity',有时被叫做'Object'。除了东西之外,世界还有一份全局共享的世界数据。关卡也有一份全关卡所有东西共享的关卡数据。

从资源加载和使用场景来分类,这东西所含数据大致如下所示

其中标灰的剧情和视频数据在游戏中是不常见数据

图中的Shader状态与参数,Shader,纹理在游戏引擎里常被抽象为材质(Material)和材质实例(MaterialInstance)

除纹理之外,上述对象在Vulkan/D3D12中近似的被抽象为Pipeline

然后还提供了Pipeline做为一个整体数据的存储和加载的通路(Pipeline Cache)

数据

回顾一下九宫格分块加载策略

第一个问题是:需要加载这九个关卡的所有Entity的所有数据吗?

在绝大多数情况下是否定的。因为那些对玩家来说太过遥远的小东西,在非FPS品类的游戏里,它们对玩法的贡献大多为0,在视觉上因为透视的关系贡献也收效甚微。

关于游戏场景中哪些对象对于游戏玩法来说意义重大或无关紧要,有一个基本要素概念是:热区(Area of Interseting),即玩家感兴趣的范围。对于非FPS来说,角色战斗范围不过几十米之内,大多数情况下,信息获取范围不足百米。而对于FPS来说,因为枪械有非常远的攻击距离,加上配套的多倍放大瞄准镜的设定,战斗范围可能会延伸到4、500米。

故这个问题的答案是:我们可以优先加载影响玩法的东西和近处的东西,而远处那些不影响玩法的东西可以在加载时做分类策略:

  1. 当其是不含显示数据的功能性数据,且其不影响全局的功能状态时,不加载它。

  2. 当其包含显示数据但其画面占比贡献小(投影面积小或透明度低等)的时候,不加载它

  3. 当其包含显示数据但其画面贡献大的时候,加载它不那么精细的数据。

分类策略3带来了一个新的问题

第二个问题是:Entity中的哪些数据可以且需要分步、按需加载?

那些体量非常小的数据做分步加载中意义不大,如设计良好的数据配表,Skeleton数据、相机数据

那些影响全局玩法的数据必须无条件加载,如寻路数据

那些可能影响玩法的数据可以区分条件进行逐步加载,如脚本数据

那些多媒体数据因其在人类舞台上粉墨登场时就大多不能一次性加载,从而就一直支持顺序的连续加载一部分,播放一部分,如此循环往复,直到播放完成。这一加载模式又叫流式加载(Streaming)

因为渲染时切换状态(切换Pipeline)非常昂贵,是妥妥的性能杀手,处理不妥会导致CPU使用率高、掉帧和卡顿,所以现在大多数游戏会严格控制Pipeline总量,换句话说:一份Pipeline数据会被世界中许多东西所共享

Pipeline数据往往在游戏启动或是进入游戏世界的一刻,大多数已预加载并常驻内存。

余下的数据,都可以分步、按需加载:

流式加载(Streaming)

在这儿我们沿用流媒体(多媒体)数据加载的述语,称分步按需加载为流式加载 

几何网格(Mesh/SkinMesh)

模型流式加载基于以两个假设

1. 以单个模型为例,其显示所需要的细节程度(Lod)是以玩家视点为圆心向外扩展的同心圆,如下所示

2.大世界中,可见的世界总是远远大于可玩的热区,所以大部分的Entity对我来说既无关紧要,在未来的很长一段时间内也不可到达

所以远处的Entity模型只需要加载需要显示那级Lod而不是加载它的全部Lod等级。

高级别的Lod好处不光可以降低内存开销,它因为体积小同时还能加快加载速度,并同时减少渲染光栅化的GPU压力。对于那些小于等于一像素的三角形来说,它们的光栅化因为GPU需要在像素着色器中计算ddx/ddy,会有额外的消耗。

纹理(Texture)

游戏中所使用的纹理绝大多数都开启了Mipmap,Mipmap的选择算法和采样器的滤波算法及uv的ddx/ddy有关。但不管是什么算法,总是可以确定当前所需的最低Mipmap等级,这样纹理数据的流式加载可以很自然的确定为:只加载当前所需要的最低纹理等级及比它高的所有Mipmap等级

纹理流式加载和Mesh Lod加载不同之处在于,需要加载的Mipmap等级数量是一个由最低需求等级起始的Mipmap集合([RequriedMipLevel , MaxMipLevel]),而Mesh 流式加载则可以只加载当前所需的这一级Lod。这是因为渲染时所需的Mipmap等级是逐像素确定的,但Mesh Lod等级则是逐Entity所确定。

题外话:UE4的Mesh Lod Streaming和Texture Streaming一样是加载[RequiredLodLevel ,MaxLodLevel]的Lod集合,是因为它的流式加载实现机制中没有记录单模型被多引用的情形,所以它无法知道有哪些Lod级别是当前渲染所必须的。

题外话2:在UE5这种 1三角形/像素 的设定下,Mesh Lod似乎也等同于Texture Mipmap来考量更恰当。

物理数据

对大多数MMORPG来说,对物理的要求不太高,使用粗略的凸碰撞盒,这样占用小,运算也快捷。但那些高品质的FPS、ACT或开放世界对物理真实性要求高,需要精确碰撞、模拟场景破碎、布料运动、流体等。这样的物理数据,其体量很大,大到需要流式加载来减少其内存消耗。

这样碰撞也可以和场景中的模型LOD一样,创建不同的数据精度层级,每级物理数据LOD可以选择不同精确程度的碰撞体和破碎数据、确定不同的物理模块在当前LOD是否开启。这样在根据Entity和热区的距离、可见性来确定加载哪一些物理数据LOD。

遗憾的是,目前市面上大多数游戏引擎对物理数据的处理没有LOD或分层机制,物理数据的流式加载需要自己手工实现。

动画(Animation)

动画是比较容易被忽视的一种可流式加载的数据类型。但在手游中,单动画数据量许多都能超过100KB,大于一张8*8压缩的512*512的ASTC纹理。

但动画的流式加载不同于纹理和几何网格:一般的动画很难去从算法上上评估哪一帧比另一帧更重要,或者这根骨骼的动画比那根骨骼的更富有表现力。

动画的流式加载类似于流媒体,它是加载完一小段,就开始播放,而后播放和后台加载并行,通过不停的交换播放使用的动画数据Buffer来进行动画动画。动画流式加载的数据加载过程也是线性和顺序的。它适用于长段的动画播放,如表演性的动作、剧情动作等。

关卡数据

关卡中数据不同引擎有不同的考量,如寻路数据、静态的光照数据(Lightmap、Shadowmap、环境反射纹理、球谐探针)大多是在关卡这一级进行组织。其中寻路数据往往是独一无二的,而Lightmap、Shadowmap、EnvironmentCubemap则大多可以和通用的纹理等同视之。球谐探针数据介入两者之间,需要额外的设计才好做细粒度的流式加载。

但在实用中(如UE和U3D),Lightmap和Shadowmap为加大其有效使用面积,减少Drawcall往往会进行区域性合并,这样所带来的好处是减少渲染纹理切换,却让其不可重用。

在游戏中有大量重用的的室内场景的前提下,把Lightmap/Shadowmap当成普通纹理进行重用,无论对包体还是内存,都有很大的收益。

是否要把上述数据放在关卡中,更进一步是否要在关卡中保存Entity Layout之外的其它数据,值得在技术选型时谨慎论证。

关卡?

使用恰当的数据结构来确定要加载哪一些Entity及这些Entity的具体哪一些数据,是大世界客户端资源加载最核心的议题。事实上“子关卡”这一概念的存在是否存在必要性都值得探讨,因为它存在带来的最大好处是加快了加载决策过程——用于快速确定要加载的数据边界。但它也不可避免的带来了数据冗余的问题,这一冗余在某些情形下表现出色,在另一些情形下可能就非常糟糕。

如:把子关卡退回到空间划分,只保留其空间边界和索引到的Entity和其它数据,则它不光可以担当子关卡的相关工作,而其数据结构也更为轻量,上述所谓关卡的数据组织方式亦更加灵活。

来源知乎专栏:图形游戏和宅

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多