Unity 技术开放日|壳木游戏介绍SLG手游的优化策略
在 Unity 技术开放日-北京站活动中,我们有幸邀请到壳木游戏为大家介绍 SLG 手游的优化策略。壳木 CTO 邝圣凯和壳木技术平台主管陈石以《Infinite Galaxy》为例,详细介绍了太空 SLG 游戏的优化思路和细节。
本文节选了部分精彩内容,完整录播已上传至 B 站。
B 站录播:
https://www.bilibili.com/video/BV1r44y1z7X3?p=3
邝圣凯:大家好,感谢 Unity 的邀请,让我们壳木游戏有机会跟大家分享一下我们在 SLG 手游开发里面的一个经验。
我会先大概介绍一下联网 SLG 游戏及一般优化策略,再结合《Infinite Galaxy》进行详细介绍。然后我们的技术专家陈石会介绍具体的优化策略,包括阴影、体积光、面部捕捉等等。
SLG 游戏大家都很熟悉了,它是模拟、策略、群体对抗的游戏形式,前期个人养成,中后期以联盟+多人对抗的游戏方式,包括探索、扩张、采集、资源竞争等等。
先来看一下个人养成的部分,首先是基地,基地的部分有很多种包装形式,城堡、城市、基地、控制室等等,负责前期个人养成的部分,主要是单机项资源建设、资源生产。现在很多 SLG 项目为了吸引前期的泛用户,也会在前期加入单机的玩法来进行包装。
接下来是世界地图的场景,这是 SLG 游戏的核心场景。在这个上帝视角的地图中,有玩家基地、联盟领地、资源点、特殊建筑、事件等等,供玩家进行多人对抗。上面主要的动态元素就是“行军队列”,很多军队在上面远征、战斗,这是我们游戏战斗实时的截图。
有些 SLG 游戏还提供高层世界地图视角,主要为了高层次的快速浏览跳转,有些游戏还有引入拓扑的结构逻辑。
我们先来分析基地这个场景。
基地是养成的功能入口,以中远景为主,里面的对象是静态的,建筑、地标、功能对象等等,功能对象有可能数量比较多,这是我们需要注意的一点。
基地场景汇总还会有辅助性动态对象,这样能让场景不那么死板,总体而言这个场景不会有太重的计算逻辑或者动态渲染的需求。
接下来是策略宏观视角,这里是以总体的氛围为主来提升游戏画面表现,因为镜头的角度比较固定的,所以我们可以用一些比较取巧的方式,比如用 2D 方式模拟 3D 的光影效果等等。动静结合时要注意配比平衡,避免动态内容太多造成性能负载。如堆叠数量较多的建造类对象,包括对应的 UI 都是性能上需要注意的坑。
世界地图是我们游戏表现需要优化的一个核心场景,也是以远景为主,伴有静态动态的对象。这里的瓶颈是,在集中地域内短时间内爆发的行军战斗,还有一个玩儿到后期联盟的玩家倾向于聚焦在比较集中的地域,因此基地的渲染也会成为一个瓶颈。
我们对应这部分的策略,首先是进行多层 LOD 的优化,因为涉及到镜头推进拉远。拉远时候是很大尺度,有很多细节的表现,所以我们需要做一个非常大的模型方面的简化(后面会详细介绍)。
包括带有地表的地图可以考虑 GPU 做地表的快速构建,装饰对象用一些优化。
行军队列,首先控制队列里面兵种数量,目的促进小对象进行合批。合批的话,对应的动画方案也要考虑,一种是用 2D 序列帧,把 3D 的内容渲染为 2D 的序列帧,做序列帧对话促进合批。还有一种需要 3D 的表现,有一些视角微变化。这个时候考虑把 3D 动画烘成固定的mesh序列,针对简单的原理做队列的显示效果,这个本身的合批效果还是不错的。
基于同屏队列数量多的时候,我们会考虑在数量特别大,一个屏幕里面有几十个玩家在行军战斗的时候,我们可以偷偷地做数量上面的裁减,把数量降低,比如兵少渲染几个,这个时候玩家是觉察不出来的。
最后结合到硬件的并行发展,我们也可以考虑用多核对一些天然适合并行计算的队列移动等等的 case 进行优化,利用到 Unity 最新的 Job System。
接下来介绍一下《Infinite Galaxy》(以下简称IG)。这是一款太空科幻题材的 3D SLG 手游,写实风格的。因为立项比较早,早期是基于 built-in 管线构建的,2019 年下半年转成了 URP。转成 URP 一方面是官方推荐,我们也做了些功课,感觉这是未来的趋势。用过之后发现它确实有很多优点,可以在手游上做之前做不到的后处理,同时能够满足功耗支撑游戏正常进行。
IG 的主要场景包装跟 SLG 游戏是一致的,也是三个结构,基地对应控制室概念,里面的元素比较固定,比较好控制;世界地图分两层,一层是中间恒星系,里面基地对应每个玩家,第二层还有拓扑图,里面是拓扑关系,有一个节点的图,联盟什么的都在里面,对应的是联盟可以占领很多恒星系。
说到优化,最主要的就是光,基地没什么大问题,主要是恒星系的光源,该光源是居中的,如何补光是个问题。
这个项目有一个利好的点是场景的舰船不需要做动画,所以这方面算力可以节省出来,让我们在运动表现、光影表现里面做一些额外的加强。比如战斗时的实时光影反馈等,之后都会解读。
再就是综合的点,包括场景的低延迟切换,使用一些场景预加载包括场景内大量对象优化时候采用分帧策略,使得重度场景在内存大的时候做快速切换;功耗体验流畅度的平衡,这也是最近几年出现的新矛盾。我们的算力往往可以满足性能的要求,但功耗往往支撑不住,用着用着电池没电了。所以我们引入了动态 FPS 概念,按照两种情况区分,一种是按用例区分,有的计算不需要高频度的更新去完成,我们会降低它的计算更新频率;另一种,操作的时候我们需要达到一个平滑的体验,所以在操作的时候把帧率提高,非操作时候降帧。效果还是非常好的。
我们现在已经整体过了一遍,接下来我们会具体的讲一些细节。接下来部分由我们的技术专家为大家进行。
陈石:我直接接着邝圣凯刚才介绍的部分直接进入一些细节上的讨论。
我们先看一下刚才介绍到的恒星系在 IG 游戏里面大概是什么样的。
这个是一个 30 级玩家的城堡,拉远的时候看到 LOD 切换过程当中的变化,图中的聚集程度还是比较常见的。在恒星系里面还有很多的环境因素,会给负载带来压力的主要是陨石块,其数量是非常可观的。
我们先介绍一下光照。
我们的游戏场景在外太空,所以我们把主光源抽象成为一个点光源,但实际操作时,直接用点光源会得到这张图的样子,它导致受光面积比较小,背面特别黑。
然后我们尝试用了一些比较常规的办法,比如说用天光盒作为环境光输入,提升环境光的亮度。但是这样再提升环境光亮度的话,细节就会被吞掉。
还有一个问题是光面的问题,其实我们整个恒星环境,主光源的体积非常大,我们不是把它当成一个单纯的点光源,而是当成球光源。我们最后引入了类似于基于图片的这种光照思想,把 Matcap 引进来作为环境光的输入,对光照方程也做了修改,最后得到下面三个结果。
这三个结果基本就是在不同星系里面玩家建筑的表现,大家可以看到在背光的情况下,其实这些光照的细节还是能够得到保留的。
有了光之后,就是处理阴影。
先看一下这个图,这个图里面是没有阴影的,没有阴影会导致玩家是没有方位感的,他们不知道现在这个建筑跟我的主光源的位置关系是什么样的,所以阴影对于我们来说也很重要。
IG 项目带给我们的挑战在于它是点光源,和传统 SLG 不一样。传统的 SLG 大部分是方向光,只要方向不变,烘培的结果是可以支持建筑在一定程度上移动,改变位置的。但点光源不行,点光源一旦烘焙,只要建筑的位置有移动,这个阴影就失效了。同时,如果打开实时阴影,产生的开销实在太大。IG 项目中唯一的好处是,在太空环境中没有地表,所以我们其实只要考虑自阴影就行了。
对于阴影我们是这样处理的,烘了 16 个阴影贴图。大家可以看到下面这张白色的图,我们是根据模型的正方向,将其切成 16 个等分,得到 16 张阴影贴图。在整个使用过程当中,当模型的正方向和光源的正方向发生改变的时候,我们就会把阴影贴图切换到最接近的那一张去。
但这个最初的方案会导致阴影变化的时候会有一个跳变,极不自然。
所以我们再一次引入了 Job system,这样我们不光能减少性能上的压力,最重要的是对 16 张光照贴图进行压缩。在 Job system 进行实时解压,同时解压两张——解压与当前的角度最接近的两张,用这两张作差值,这样它的阴影就不会跳变,而是渐变的过程,类似实时阴影,同时开销又比实时阴影小非常多。
建筑方面基本上就是这样,我们接下来聊一下我们的小行星带。
这条小行星带是在缓慢转动的,我们的每个恒星系里的陨石个数大致是 4K 到 1W 之间,再加上每个对象都是实时的,每一帧都需要转动,所以这个开销还是蛮大的。
我们首先想到的用 Draw Instance 的方式来做,但是这个方式的上限是 1024。除此之外,它其实是我们提交多少它就绘制多少,最后到了三角面剔除的时候才会剔除,这个部分的带宽及各方面的开销是很大的。
最终的方案是,既然要分块,我们就分得细一点,分块结束之后用四叉树组织起来,这样 CPU 计算 clipping 的时候也会快一些。我们实际上是在 CPU 计算 clipping 之后,把需要显示的陨石分块提交到 GPU,也会带上 LOD,在材质里面直接把自旋转的问题解决了,最终优化下来这块开销就基本可以忽略不计了。
下面我们介绍一下玩家城堡的优化,IG 这个项目特殊的地方在于太空里面有大量的能量效果。我们的镜头支持拉近,拉近时候有大量飞行轨迹——模拟小飞行器巡逻的状态。但我们最开始拿到的美术设计,美术给我们的资源是一个六切面的模型,一整套 PPR 贴图还有 220 个 Particle system,最终造成在一个屏幕当中显示 50 个左右玩家城堡的时候,仅 Particle system 就能达到 1W 多个。
我们首先分析了一下 Particle system,发现其实最终使用 Particle system 的部分只有 8 个,是非常有限的,其他的部分可以通过把发生的片来作为合并,然后分布到 8 个动画材质上去,就可以达到最终的效果。剩下的 LOD 过程就是普通操作。
接下来介绍一下动态的部分,主要就是 IG 里面的战斗。
这其实是一个常见的战斗过程,一个成熟的联盟大概有五十个玩家,在特定时段联盟里的 50 个玩家会派出自己最好的舰队一起攻打其他联盟或者某一个关卡,我们现在看到的就是每个玩家最强的舰队,派出来是 20 到 50 艘船的规模。实际上这样一场战斗至少是 1000 个舰船模型的规模,而且舰船还都不一样。进入到战场之后,压力也不会减轻。
这种情况下,我们会在 UI 的元素上面做一些简化,但模型,包括光影这些细节还是一直保持一致的。大家可以看到刚才邝圣凯介绍的战斗过程当中光影的变化,这些变化其实主要是在一些对于玩家来说比较关键的节点,比如自己的旗舰损毁了这样一些场景的反馈。
针对这样的游戏体验需求,我们做的第一件事情就是对我们的舰船做更精简的优化,因为其实它的数量和玩家的城堡比起来是更高的一个数量级。这个情况下除了对模型和特效材质做优化我们还加了更简化一级的 LOD,参照 Impostor 概念是 2D 的片,我们还是保留了法线和高光,实际上它跟模型使用的是同一个材质,这么做的好处是我们在镜头快速拉远的时候不会看到大面积的像素跳动,光感还是一样的。
这过程中我们遇到一个坑,就是 Trail Renderer,我们用它来表现舰船的尾焰。Trail Renderer 的特性是在全生命周期,都有 Mesh 更新存在,即便隐藏掉也会存在。所以在隐藏时必须手动关闭 emitting,如果不关闭的话,CPU 的负载就都一直在。
下面看一下 IG 舰队移动的细节。
我们刚才也聊到,舰队本身船体是没有动画的,这会带来一些便利,但实际上这个舰队的整个形态在整个运动过程当中是需要保障的。
我们也试过很多方案,比如类似鸟群、鱼群模拟,甚至 LVO,最后综合开销下来,我们还是以舰队的固定形态加上曲线差值的方式来实现最终舰队的移动。
大家也看到即便是这样,即便我们在设计上和算法上进行很多优化,但其实一旦到了刚才 1000 艘船的量级,它也是会卡顿的。针对这个情况,我们是把整个运动和曲线拟合的算法全部移动到 Job system 中,这样的表现效果还是比较让人满意的。
战斗的这块除了可见的优化之外还有一些优化是不可见的,我们看到这个视频里面是一个比较常见的 IG 项目里面会发生的本地战斗。
本地战斗里面会出现类似于 100 个舰船打另外 100 个舰船的场景,这种场景带来的 AI 方面最大的开销就是会突发地需要寻找目标,在对象一旦出现的时候,比如 100 寻找目标时间的复杂度就是 O(n 平方),无论是什么样的机型都会有瞬间的帧率波动。
针对这个情况,我们首先想到用 KDTree 来简化目标查询,大家知道它是二分差,实现复杂度降低很多,但是 KDTree 构建也是蛮大的开销。我们把 KDTree 的构建放到了不同帧中,因为这是主线程逻辑,不可能放到另外一个线程,所以我们放到不同帧里面去,每帧只算一部分,算好之后再切换,马上开始计算下一个 KDTree 的结果,这种方式最终实现帧率的稳定,这个会对战斗结果有一定影响,但目前我们的需求里面看到的就是几帧的延迟是完全可以满足设计需求的。
陈石老师还分享了世界地图、体积光、面部捕捉等方面的精彩内容,欢迎大家前往 B 站观看完整版进行学习。