热门
《行尸走肉:行军作战》移动端优化经验
来自Disruptor Beam的图形和客户端架构师Jason Booth,拥有超过25年创作游戏的丰富经验。本文将分享他参与开发《行尸走肉:行军作战》所积累的,一些让大型游戏在低端移动设备上也能拥有优秀运行体验的经验技巧。
如果需要《行尸走肉:行军作战》这款游戏高品质的运行,需要移动设备支持OpenGLES 2.0且RAM至少1GB,这样的配置约占所有Android设备的40%。这款游戏中有细节丰富的美术资源、完整的昼夜和天气循环,还有上千个对象,但它的容量却不到100MB。这是如何达到的呢?
游戏介绍
《行尸走肉:行军作战》是一款紧张刺激的多人策略移动游戏,游戏背景设定是基于Robert Kirkman的长期连载漫画系列《行尸走肉》。该游戏能支持5万玩家够同时在线游玩。游戏发生于弗吉尼亚州和华盛顿特区,是一个庞大、复杂且可自由探索的世界,成千上万个丧尸、僵尸横行于此,到处都会传来爆炸声。
世界由多个区域组成,载入和渲染大小为32x64,游戏平铺大小为2048x1024,单位为米。在给定时间内,每次会渲染4~6个区域。游戏中的地图是通过结合手动放置内容和程序化内容来创建的。
手动放置内容和程序化系统都在地图编译器(Map Compiler)中进行编译。每块32x64区域都会生成程序化内容,然后使用手动放置的数据来生成布尔值。地图编译器还会编译地形数据和AI系统的导航信息。将完整的结果保存为预制件流格式,这是工作室自己的预制件系统。最后,所有内容都会通过Asset Bundle资源包流式传输给用户。
大型空间节省工具:自定义预制件流
Disruptor Beam的自定义预制件解决方案或称为预制件流,在设计和运行时使用,并支持嵌套。它存储InstanceEntry的数组,用于构建和传输。
在编辑时,预制件流的额外代码支持随机处理等操作。对于特定部分,预制件流可以随机生成多个元素,并在这些元素上创建简单的变体。例如:房子和车子这类物品是从小型部件组成的,这些部件可以互相组合并匹配,从而在同样的对象上产生不同变体。
在编译时,预制件流会分为三个细节层级(LOD):对象可以被指定为高、中、低三个层级,以便可以在低端设备上删除某些对象。当所有内容都编译好后,会进行扁平化处理,所以在传输给用户的时候不存在任何层次结构。
在运行时,预制件流的功能类似图形引擎的底层绘图列表,它会指定要用什么材质在哪个位置绘制哪些网格,以及存放Transform位置的列表,该列表指定要放置特定预制件的位置。
预制件流会打包Transform,由于现在部件大小已知,约占160平方米,它可以将Transform打包为七个部分:三个用于存储位置,三个用于存储旋转,还有一个存储缩放。
这会减小流式传输给用户的场景数据大小。如果一块区域被保存为场景,它的大小约为3.6 MB;如果被保存为预制件,大小约为2.1 MB,而如果是预制件流,则只有41KB。
使用网格图像序列为角色制作动画
《行尸走肉:行军作战》中的角色不会被近距离观察。它们仅有60像素高,动画设置有限。团队必须对角色批量处理,因为低端设备上的绘图调用效果不好。正常的处理流程是将角色放入纹理中,在顶点着色器采样,然后在着色器中处理所有动画。但由于OpenGLES 2.0不支持在顶点着色器上采样,所以需要另一种解决方案。
于是,他们将动画转换为网格图像序列(Mesh Flipbook)。载入时,图像序列接收每个动画,然后将所有帧都烘焙为独特的网格,其中1帧 = 1个网格。然后在每个角色上交换网格,制作动画。不过这个方法需要大量内存。
紧密打包的导航系统
导航数据通过地图编译器计算得出。该系统会把光线投射用于确定范围内角色将要进入的开放空间。它会考虑像天桥这类对象,因此角色可以在它们下面行走。
最终结果是每个区域有64x64位数据,数据都被紧密地打包起来,因此整个世界大约有1MB的数据。
类似粒子系统的架构更新循环
为了编写出快速而紧凑的更新循环代码,请避免使用Update()、虚函数和面向对象的开销。实际上,你也许想通过内嵌大量函数来减少开销。这就像模仿粒子系统来编写或构造代码。
在粒子系统中有粒子数组,你可以通过数组压缩粒子,一次更新所有内容。通常如果你将要拥有数千个对象,例如:僵尸。这正是正确的代码塑造方式。
善待缓存
CPU很擅长通过内存按线性顺序来压缩并处理数据。所以可以设置一个大型区域,合理安排里面的对象,给它们运行相同的例程,这样的做法类似着色器:着色器会接收并处理一整块像素。如果你在设计时尽可能保持较小的数据量,这些数据会被高效地缓存,从而尽可能缩短CPU处理时间。
在《行尸走肉:行军作战》中,64x64位数据网格上的光线投射几乎是没有任何消耗的,因为这些数据都在缓存中,并且整个导航数据区域小于1KB。
慎用线程
不管是通过多处理器还是单处理器进行处理,慢代码还是慢代码。如果不确保数据结构尽可能紧凑,你只需将低效结构复制到多处理器中。
请考虑使用摊销,因为这个方法比线程处理更简单。然而,如果确实需要用线程处理代码,代码高效且能够摊销处理,该过程可以变得更简单。
如何获得漂亮的地形
为了在游戏中展现漂亮的地形,该团队使用JPEG中的yCbCR颜色空间。在JPEG压缩格式中,这种颜色空间提供了高分辨率亮度值和低分辨率色度(CbCR)值。
他们将四个亮度纹理压缩到一个纹理中,从而得到四种通用地形类型,可以根据其颜色生成多个地形。例如:棕色使地形看起来像泥土,绿色像草地。
他们通过顶点RGBA颜色通道(RGBA)添加Splat权重遮罩。通过将亮度通道用于高度映射和基于高度的混合效果,从而产生漂亮的过渡效果。最后将低分辨率色度层应用于亮度高度图,实现出美观的地形效果。
总体结果是一个用于亮度数据的1024x1024纹理,整个世界和所有Splat映射一共使用了3.1MB数据。
重用地形网格制作水的网格
水的网格是克隆地形网格得到的。没有使用深度图,而是选择通过移动顶点到水面高度(vertexHeight=waterHeight)得到了“免费的”深度缓冲区。
处理结果的差异是让高度变成了深度。这样做大大节省了资源,只用了一个纹理采样/地形绘图调用和一个纹理采样/水绘图调用就制作出了水的效果。
快速光照解决方案
对移动设备来说,渲染完整的PBR消耗的资源太多,所以他们使用了一种名为球面光照近似处理(Spherical Lighting Approximation)的方法。
通过这种方法将完整的PBR光照环境渲染为球面映射纹理,用于漫反射和镜面反射效果。对于后续的mip等级,将平滑度数值的1/2存储为之前的mip等级,然后使用对数空间编码来实现4x强度的HDR。
然后在运行时,可以选择一个mip等级并在纹理中查看。而不是进行光照计算。通过使用text2Dlod以适当的光滑度等级对光照纹理进行采样。
该方法的优点是可以使用任意数量的光线,只需要照亮球面并渲染即可。这样能够得到任意复杂的光照环境,里面带有无数的光线、天空盒等。这是一个定制的完整PBR工作流程,比标准PBR工作流程快20%。
阴影
针对高性能阴影,该团队提出了一种解决方案,他们将阴影从上到下进行渲染,高度在阴影平面之上。这样会高效地创建距离场并支持基于高度的模糊效果,用于制作柔和阴影和自身阴影。
此外,由于存储了对象的高度,因此它们可以将数值固定到接近阴影平面的位置,使阴影模糊,从而创造出适当的环境遮蔽近似效果。
多分辨率渲染
通过使用多分辨率渲染功能,使UI保持在高分辨率,同时让3D世界从降低的填充率获益。他们通过使用DPI来在多种设备尺寸上得到一致的效果,根据目标DPI,范围从200 DPI到400 DPI)来设置分辨率。
关于Asset Bundle资源包自定义构建的一些小技巧:
1、只标记要从代码载入资源包的数据
2、解析依赖关系
将大型资源(如纹理和音效)放入指定资源包中
将共享资源放入指定资源包中
独特的资源包含在父资源包之中
通过路径命名资源包
3、不要使用资源包变体
为每个变体级别构建唯一的清单
用Perforce检查,预处理所有纹理和音效,构建资源包,用Perforce恢复。
电话:010-50951355 传真:010-50951352 邮箱:sales@souvr.com ;点击查看区域负责人电话
手机:13811546370 / 13720091697 / 13720096040 / 13811548270 /
13811981522 / 18600440988 /13810279720 /13581546145