如何重绘「江南百景图」?近300页 PPT 免费分享!

去年,古风模拟经营类手游《江南百景图》成功破圈,成为年度现象级爆款。 如何将它搬到小游戏平台?是转换还是重写?使用哪些技术方案,能在包体大小仅为原版 1/20 的同时,达到与 App 版相当的游戏体验? 椰岛小游戏研发负责人 大城小胖 ,带着他近 300 页的 PPT,在 Cocos 的两次线下活动中做了全面的技术分享。

转换 or 重写

《江南百景图》App 版游戏包大小有 600+M ,上线前期还有部分用户反映游戏运行时手机发热严重。而小游戏版在经过立项选型后,决定使用 Cocos Creator 重写,仅用了 1 天就做出了 Demo。经过 4 个月的优化,我们最后将包体压缩到 30M 左右,同时保证游戏体验与 App 版相当。

优化的过程中,我们做了以下工作,其中 代码 部分需要重新设计和编写。

渲染优化

原生版本的《江南百景图》移植到小游戏首先需要解决的就是 耗电高、易发烫、Draw Call 高 等问题。

合批

合批是降低 Draw Call 最快也是最有效的方式。优化同样的 Texture,将多张的图片合并到一张图集上,这样不论要生成多少张不同的图片,都不会打断合批渲染,Draw Call 也就降低下来了。

但是《江南百景图》的资源非常多,每个玩家使用资源的顺序也不尽相同,如果玩家使用的资源分别在不同的图集上,还是会导致合批渲染被打断,产生 Draw Call。因此,针对这一情况,我们采用了 Multi-Texture 的方式进行了优化,其原理是将传统的判断是否在同一张图集,转换为判断是否在 同一批图集 ,这样就大大减少了 Draw Call 产生。

另外,通过 gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) 这个指令,可以知道在一台设备中 Shader 最多支持几张图集。测试发现目前 90% 以上的手机至少支持8张,因此我们将批图集的数量设置为8张。因为一个批次有 8 张图集,所以我们是通过这个 idx 判断某张图用的哪个图集,代码也很简单。

动态合图

小游戏版本采用了 Cocos 的动态合图机制,这样在 CDN 下载的图片也能进行合图。而为了提高合图的效率,避免浪费空间,我们会将长度或者宽度特别大的图片进行裁剪。

例如左图中的旗杆,由于图片太长,在动态合图时会导致空间浪费,因此我们将这张旗杆的图片裁剪成两张,如右图所示,再在项目中进行拼接处理。

采用同一个材质资源

在《江南百景图》中,玩家移动地图时,原本在显示范围外的图片将从水墨色变为彩色。

传统的方案是改变图片材质,当地图移动到要显示的节点时,节点一个一个地进行材质的切换,达到一个 “淡入淡出” 的效果。但是在项目中尝试之后,我们发现这样会导致 Draw Call 上升,而且拖拽地图又是一个很频繁的操作,游戏中实际效果较差。

因此在这里我们将所有城市物体资源,无论是人物还是建筑、常态还是淡入状态,都用统一的 Material、并使用顶点数据传递“时间参数”,以此节约性能消耗,最终达到所有建筑和人物的创建、移动、销毁等全都只需要一个材质就能够完成。

很多人会觉得一个普通的图片也用这么复杂的方案,会影响性能,导致性能变差。但是实际测试效果并不差,这也告诉我们,在游戏开发中还是要以实践为准,不能想当然。

优化 Shader 的输入数据

由于《江南百景图》的图片资源中不会用到 Color 这个属性,因此在材质中,我们将原有的 Color 数据去除掉。

下图是一个正常的顶点数据:

接下来就是将原有的 Color 数据去除掉。用来存放项目中所需要的其它信息,这样做可以减少 CPU 与 GPU 互相传输的数据量。

层级规划

我们将不同的类型的资源,分别放置在对应的层级中。《江南百景图》共分了13个层级,下图只展示了部分比较重要的层级:

其中比较有意思的是 旗帜层 。旗帜是《江南百景图》中的一个常见元素,但因为项目实际技术限制,无法将一个旗帜制作在一个完整龙骨动画中,如果强行放在一起,就会导致在渲染到旗帜的时候出现断批。我们采用了 动态组织层级关系 的方式来解决这个问题。例如这是一个原来的旗帜预制体:

采用 动态组织层级关系 的方式,将旗杆与旗面拆开,旗杆放在下面的普通建筑物层,旗面则单独分为一层旗帜层放在上层,这样就很好地避免了渲染时一直被打断合批的情况。

UI 渲染优化

UI 部分我们没有使用动态合图或者 MultiTexture,动态合图我们留给了游戏中的人物和建筑、而没有使用 MultiTexture 主要是开发成本的原因。但在我们的优化下,现在游戏的 Draw Call 可以降得很低。

UI 方面我们也是做了分层,比如下面左边的图上我们的 button 层,里面都是按钮部分,右边是我们的标签牌层级。这样我们就可以根据功能区去划分图集,然后和游戏里的层级对应起来,而不会打断合批。

自定义引擎

Cocos 是个开源引擎,我们可以根据项目的实际需要,对引擎进行定制、修改,从而达到更好的效果。

增强 TiledMap

我们在 Cocos Creator 原有的 TiledMap 组件的基础上,拓展了新的功能,下图是 Cocos 自带的组件。

这里就不详细说了,有兴趣的可以去官方文档查阅,我们主要来说一下经过拓展的新功能。

1. Diamond Tile: 游戏中使用了很多 TiledMap 中的图块菱形方块, 但是引擎默认的传递方式是矩形,这样就会造成数据浪费和冗余。

这些图片首先都是 规则的菱形 ,所以很简单,直接 根据宽高进行进行计算。

将菱形周围多余的部分切割,这样很明显图片大小减少了一半.这里注意一下非标准图形就不能这么用了。

2. Share Culling: 《江南百景图》共有三层 TiledMap 地图层, 勾选时 将只对 TiledMap 的第一个地图层进行处理判断可视区域的范围,而其他的地图层将直接照搬第一个地图层的处理结果,这样能够节约不少性能。

3. With Color: 如果不需要颜色数据就可以勾选,减少数据量的传输。

将道路转为 Tile

游戏中的道路是不需要进行淡入淡出效果的,如果当作普通建筑物资源来用之前的材质进行渲染,会消耗相当多的性能。因此我们将道路作为 Tile Map 地图的一部分,让道路不需要用之前提到的材质进行渲染。

还有一个小细节,在 Tiled Map Editor 中设置的宽高,与实际项目中使用是无关的,因此在生成的时候可以将地图块按照实际项目需求进行缩小,减少资源使用。

资源压缩

将一个原版 600+M 的游戏压缩到最终的 30M 左右,资源的压缩工作必不可少。我们需要将游戏资源进行合理的压缩,使其更加适合小游戏运行,并且不影响游戏最终的显示效果。

图片缩放

对不同类型、不同清晰度的资源,我们可以设置不同的缩放比例。我们将大部分的建筑缩放到原来的 0.65 倍,背景中的山川则被缩放到原来的 0.3 倍。另外,就算是相同位置上使用的人物立绘,由于每个人物的自身和背景的颜色、精度不同,也都可以给它们设置不同的缩放比例。

于是我们将所有 Sprite 组件采用 Custom 模式,可以自由控制比例。不同的图片使用差异化配置,设置不同的缩放比例,用脚本控制缩放比例,这样便可以打包出任意画质和体积的各种版本,并且还提升了动态合图的利用率和部分性能。

图片减色

综合比较了大家比较熟知的 tinypng 和 pngquant 两种工具之后,项目最终选择使用 pngquant 对 PNG 图片进行批量压缩。pngquant 可以自定义压缩品质,而且 pngquant 开源,容易维护,风险可控。pngquant 也提供像 ImageAlpha 这样的工具,可以实时查看图片减色后的效果,方便调整参数。

需要注意的是,由于 Cocos 会进行合图处理,如果对 Build 前的图片做压缩,合图时前期的一些压缩工作可能就此无效化,所以我们要对 Build 后的图片 做压缩处理。

另外我们也建议程序多了解一下图片格式以及其原理。不是所有图片都要使用 PNG 格式,也会有使用 JPG 的情况。

场景剔除

这部分我们的需求是 只渲染可视物体 。那么用什么方法确定哪些物体是可见的呢?最开始我们使用了四叉树,但是在 JS 语言中的效果并不好。所以我们给地图划分格子,Grid 的单元格大小要适中,但单元格的边长应为 2的整次幂 ,便于利用 位运算 提升性能。

如下图所示,红框就是镜头,所以需要渲染的也就是这个红框里出现的格子。然后我们再根据建筑物的坐标、大小去进行计算,判断建筑在哪一行哪一列的格子里,从而确定该建筑物是否是需要被渲染的物体。

这是一段简单的检测函数 大家可以根据自己的项目需求去进行扩展。

除此之外,为了防止特殊情况出现,判断的可视范围需要比实际范围更大一些。

寻路

《江南百景图》使用的寻路算法,有针对单源单点的 A* 和单源多点的 Dijkstra 。但这里我们要讲的不是寻路算法,而是在游戏中的用法优化。

针对地图很大、建筑物和人物都很多的情况下,这些算法一起执行就会很损耗性能。所以我们用了 分时寻路 ,就是把寻路过程由一帧分到若干帧去进行计算,这样就不会在某一个时间段集中进行大量运算,对游戏性能也不会有太大的影响。

除此之外我们还在游戏里做了一个大胆的优化,就是统一管理寻路任务,同一时间只为一个角色服务。也许有人会问,那岂不是一个角色在哪里走、其他对象都在那边等着?其实真正在游戏里不会有这种奇怪的表现。首先每个角色寻路的起始和结束时间都不一样,再者这个同一时间是非常短的,就等于把角色寻路分配到了不同帧里,交替进行执行。

再谈性能

模糊特效

玩家在打开《江南百景图》的任意界面时,游戏的背景需要做模糊处理,而背景中的人物动画等仍需要正常播放。

模糊效果我们最常见的就是高斯模糊。高斯模糊的效果很好,但是性能却较为一般。下图是常见的一些模糊算法:

经过综合的考虑,在江南百景图项目中,最终采用了 Kawase 模糊,具有较高的效果的同时又具有较好的性能。关于更多的各种模糊的详细介绍,可以参考 十种图像模糊算法的总结

下面的是对于传统的高斯模糊与 Kawase 模糊的效果对比:

运用了高斯模糊的的画面很卡大概是每秒 十几帧 的样子。而采用了 Kawase 模糊 的画面是可以流畅运行的,可以跑满 60 帧

另外,还有一个可以适配任何模糊算法的方法,可以将要模糊的图片先渲染到一个小的 RenderTexture 上,然后将其 模糊后再放大 显示,这样做可以增加模糊算法的 处理速度。如下图所示:

RenderTexture 池

在小游戏或 Web 端 创建 RenderTexture 时,比较损耗性能。所以我们在游戏中使用完 RenderTexture 后,不是直接销毁,而是将其放在一个 缓存池 中,下次从缓存池中调用符合要求的 RenderTexture 即可。

点击检测

《江南百景图》中有很多建筑物,而在用户点击时,并非简单地通过地形上的块做判断,而是给每个建筑物画了一个 多边形检测区域 。但是建筑物是移动的,如果 多边形检测区域 也随之移动,从性能和逻辑上都不是好的处理方式。

于是在实际操作中,我们让建筑物移动,而对应的 多边形检测区域 不做移动,并将其设置在原点坐标上。用户点击操作时,将点击的坐标减去建筑物相对原点的坐标,就可以进行点击检测了。同理如果建筑物是反转状态,可以将点击坐标进行镜像,而 多边形检测区域 仍然可以不做调整。类似还有其他情况,大家也可以去了解一下各情况下对多边形的处理方式。

数组排序

数组排序是大家容易忽略的一个优化模块,很多开发者在开发中都直接使用 Array.sort() 这样的 快速排序 算法,但是其实每种排序算法其实他都有一个擅长的场景。在开发中应该针对实际的项目情况采用合适的排序算法。例如 Array.sort() 排序更适用于混乱无章的数据,而在江南百景图项目中 每一帧 都会对这些人物建筑物进行排序,而这一帧对于上一帧来说差异不会很大,也就是相对 有序的数据。因此,它更适合使用像下图这样的 插入排序 算法。能更好的提升运算的效率。

其他优化

「阅后即焚」

游戏中存在一些低频显示的大图,例如进入游戏时的公告、抽到的卡片等,玩家在游戏中看一遍就不会再出现了,对于这一类我们用了“阅后即焚”的思路。

像这些大图,我们通常先从远程服务器下载到本地缓存,产生 Image 对象,还有 cc.Texture2D、renderer.Texture2D

我们通过伪代码来简单讲解一下,加载图片时,将图片添加到我们自己创建的回收用工具类 TextureRecycle 中。

视图关闭时,通过工具类回收这些图片。

cleanUp

在图片的回收阶段中,就可以将以上所有用到的对象都清理干净了。

构建优化

在构建发布流程中,江南百景图项目使用了大量的自动化脚本来优化构建流程。包括 全平台构建、上传游戏平台、资源预处理和后处理、CDN 同步和版本控制和二次混淆加密 等。但成也脚本败也脚本,过长的构建时间也造成了不少困扰,因此我们也需要做一些额外优化。

Cocos 新版本添加了一个第三方开源压缩工具 Sharp,压缩级别是 0-9,数值越大压缩越久,而 Cocos 的默认参数是 6。由于我们已经进行过 图片减色 处理,因此我们将参数改为 0,这样就能减少很多构建的时间。

而在各平台构建时,总是格外漫长,原因是每次平台构建时,Creator 都要重新生成对应的平台图集。找到原因后,我们在每次构建前,将对应目录中的 info.json 中的 actualPlatform 参数先修改为 对应的平台名称 再打包,这个改动使我们的构建时间由之前的 15 分钟缩短到 10 分钟左右,提升了 30% 效率。

结语

在不懈的优化下,我们看到在现场演示时,这个用于官方演示游戏的高级账号,在游戏场景人物都很丰富的情况下,仍然只有 6 个 Draw Call。

同时,大城小胖也将本次 《重绘<江南百景图>》PPT 完整版 分享给大家,感兴趣的小伙伴可以 扫描下方二维码 添加 Cocos C姐 微信免费领取。再次感谢大城小胖的倾情分享!

1891631853645_.pic

137赞

肝完游戏,我还得继续肝游戏的PPT :innocent:

8赞

很多优化思路简直醍醐灌顶

2赞

超级干货!够看很长时间了~

PS:

7赞

**Multi-Texture*这个东西是好东西。能分享就好了

6赞

牛皮!!!

如何把一帧的计算量拆分到几帧内进行呢?

drawcall的优化不错, 这个项目的深度广度都达到了。

受益匪浅
特别是看到最后说到的dc只有6的时候更是大为震惊
如果里面的优化技术也可以仔细分享就好了

其实是不要把所有计算工作都压在同一帧里,造成卡顿。例如场景里的人物可以分组,而不同分组人物的寻路运算工作,就放在不同帧操作。

可以参考下我这边文章的 update 部分。

简直强到离谱啊
又学废了

强。这技术贴。

666 太强了

真的太强了

这就是大佬,这就是cocos吗?

太牛逼了仰望大佬

有道理 :grinning: