Shopee Games 游戏引擎演进之路(上)
Shopee Games 团队致力于丰富 Shopee 电商内的互动性和娱乐性,让用户在购物之余获得更多愉悦感,同时游戏也能为 Shopee 带来持续的活跃用户和更多的优惠券发放渠道。在这个背景下,从游戏诞生之初,我们希望游戏足够轻量,而且能够快速迭代,持续给用户提供多种多样的游戏体验,同时又不会对 Shopee App 的体积造成较大影响。因此,我们需要选择合适的游戏引擎,并打造适合 Shopee Games 的工具链。
本文将介绍 Shopee Games 团队如何选择游戏引擎,如何扩展游戏引擎以提高生产效率,如何让游戏开发流程和成熟的前端工程化体系结合,实现游戏规范化和研发质量的提升。Shopee Games 是内嵌在 Shopee App 的游戏,所以对于有同样内嵌游戏需求的业务团队,本文总结的经验会有一定借鉴意义。
目录
1. 游戏引擎选型
2. Egret 引擎优化和公共库
2.1 Egret 引擎优化
2.2 公共库
2.3 定制化引擎同步更新
3. 游戏研发工程化
3.1 Egret 前端工程化
3.2 Egret-Webpack-CLI 实现
3.3 Egret 前端工程化现状
4. 总结
1. 游戏引擎选型
Shopee Games 当前以休闲类游戏为主,为了减少对 Shopee App 体积的影响,技术选型上会偏向于 H5 游戏。而如何选择 H5 游戏引擎,我们主要考量以下几个方面的因素:
2D 还是 3D 游戏? 是否对开发友好?包括是否支持 TypeScript、文档是否完善、研发流程是否适合以开发为主导。 性能和兼容性如何? 官方工具链是否完善? 是否开源? 是否有成功的游戏案例? 官方是否有客服支持? 官方是否有持续更新?
首先,我们的休闲类游戏主要为 2D 游戏,所以我们先聚焦在针对 2D 设计的游戏引擎上。虽然 3D 引擎可以通过正交视角来实现 2D 效果,但渲染性能和轻量化都不如专门的 2D 引擎,所以我们先把 3D 为主的引擎排除掉,例如 Unity3D、LayaBox、Three.js、Babylon.js。而 2D 引擎,国内主要有老一些的 lufylegend.js、Cocos2d-JS 和持续更新的 Egret、Cocos Creator,国外有 Phaser/Pixi 和 CreateJS。
接着,从可持续性、性能方面考虑,可以先把较老的 lufylegend.js、Cocos2d-JS 排除。而 CreateJS 实际并不是一个完整的游戏引擎,它更接近于一个精简的渲染引擎,缺少整体的工具配套,难以支持大型游戏,也排除。
那么,最后我们重点对比 Egret、Cocos Creator 和 Phaser。Phaser 的渲染引擎就是 Pixi,后续用 Phaser 代表这两者。
以上三款游戏引擎都支持 TypeScript 和 WebGL,性能差异不大。对于 Shopee Games 团队而言,Egret 有较大优势:
Egret 支持 canvas 模式,因此东南亚市场中的一些低端手机用户也能够运行我们的游戏; Egret 的理念是面向开发者的,而我们团队有较强的研发能力,以开发者为导向能够让整个游戏性能更好; 在工具链上,Egret 有自研的龙骨动画和编辑器,非常适合我们的游戏开发。
因此,综上所述,我们选择 Egret 作为主流引擎,并在 Egret 的生态基础上,持续优化和打造能够提高游戏开发效率的工具链。
2. Egret 引擎优化和公共库
2.1 Egret 引擎优化
Egret Engine 是白鹭时代研发的遵循 HTML5 标准的开源游戏引擎,包含 2D/3D 渲染核心、EUI 体系、音频管理、资源管理等游戏引擎的常用模块。
目前我们使用 Egret Engine 开发了 Shopee Candy、Shopee Pet、Shopee Fruit、Shopee Link 这四款游戏,在项目开发和迭代过程中,我们发现官方引擎存在一些问题,无法完全满足业务需求和性能标准。于是,我们对 Egret 引擎做了定制开发,下文称之为“定制化引擎”。
2.1.1 性能上报
针对游戏的一些数据指标,如 FPS、DrawCall、First Paint、GPU Size 等等性能指标进行上报。
上报流程如下:
通过分析上报的游戏性能数据,我们能更好地分析性能瓶颈,从而有侧重性地提升游戏性能。其中,涉及到的详细性能指标如下:
2.1.2 性能优化
我们在官方引擎基础上针对性能做了一些优化,帮助开发人员提升游戏性能。
静态合图
在开发过程中将散图合成一张大图的图集,达到降低 DrawCall 的目的。
动态合图
在项目运行时,动态地将贴图合并到一张大贴图中。当渲染一张贴图的时候,动态合图系统会自动检测这张贴图是否已经被合并到了图集(图片集合)中。如果没有,并且此贴图符合动态合图的条件,就会将此贴图合并到图集中。
动态合图是按照渲染顺序来选取要将哪些贴图合并到一张大图中的,这样就能确保相邻的 DrawCall 能合并为一个 DrawCall。
和前面的静态合图原理一样,都是以合图纹理代替碎图纹理,从而减少 DrawCall。而动态合图最大的好处是提高了一些无法提前静态合图的场景,例如用户的装扮。
节点顺序调整
引擎底层的性能优化,目的是保证相同纹理的渲染顺序。例如在同级 addChild 时,如果原始顺序为 img1>text>img1
,引擎能自动优化成 img1>img1>text
,降低 DrawCall。
DrawCall 优化工具开启
游戏 Main 函数开启 Benchmark.init(null,null,true)
;或者 Benchmark.optimizeDc
设为 true 即可。
2.1.3 引擎瘦身
官方引擎默认包含所有模块,其中有一些在我们的实际项目中使用不到。因此,为了减少引擎包体积大小,需要剔除掉用不到的代码,例如:
Native 代码; Runtime 代码; WX 等小游戏端兼容代码; KTX 纹理相关代码; ETC Loader 代码。
修改前后对比:
最终,游戏前端 JS 加载量共减少 16KB,约 7%。这个体积看起来很小,但对于部分网络较差的地区,少量的体积优化也是有价值的。
2.1.4 Bug 修复
对于项目遇到的一些引擎层面的 bug,由于引擎官方可能更新修复会不及时,很多时候需要我们自己去修复。例如:iOS 14/15 渲染卡顿问题、龙骨库渲染问题、网络以及音效问题等等。其中,我们解决 iOS 14/15 卡顿问题后,很荣幸贡献了代码帮助 Egret 官方团队解决这个问题。
2.1.5 API 增强
官方引擎的一些用法过于繁琐,不够友好,如设置节点宽高等。因此我们在官方引擎基础上,扩展了方便快捷的 API,供大家使用。
2.2 公共库
为了提高开发效率,避免大家重复造轮子,基于优化后的 Egret 引擎,我们做了公共库的开发,封装通用工具类、通用模块、通用 UI 组件等等。
2.2.1 工具库
我们封装游戏中常用的一些工具库:
SoundUtil:音乐播放工具类,支持音效/背景音乐的播放/暂停/倍数播放等; DragonUtil:龙骨工具类,负责龙骨动画的创建/销毁等,隐藏龙骨创建细节,简化龙骨动画使用难度; ResUtil:游戏资源管理类,方便开发者加载/释放游戏资源; SmartEvent:封装的消息通知类库,方便大家使用,便于模块之间的解耦,包含自定义事件/UI 事件的监听和移除。
封装工具库是为了降低开发难度,以及避免不同团队重复造轮子。目前已在 Shopee Games 的四款 Egret 游戏中使用,平均节约人力 2 周以上。
2.2.2 基础 UI 组件
我们对 Egret 基础组件进行了扩展,并提供了生命周期等一系列钩子函数,降低开发难度,提升开发效率;同时,提供了一些各项目通用的组件,如:分享界面/好友界面/小怪兽弹窗等公共 UI 组件。
Egret 基础组件扩展
我们为 UI 组件提供了一些生命周期的钩子函数方便游戏业务使用,开发者实现每个 UI 类时不必再单独实现事件监听和移除。同时,内置的事件管理也避免了开发者可能因开发遗漏而导致的内存泄漏问题。
具体的钩子函数如下:
2.3 定制化引擎同步更新
随着定制化引擎的修改越来越多,随之而来的问题是:如果官方引擎更新了,我们怎么快速合并官方引擎版本?
这里采用的方案是 git 双 remote 的方案,流程图如下:
详细步骤如下:
为了表示方便,我们把 Egret 引擎开源库定义为 A,我们自己的定制化引擎仓库为 B; 通过 git clone B ,拉取修改项目 B; 通过 git remote add A <repository>,以及 git fetch A,增加 A 远程并获取 A 的仓库信息; 假设 B 的开发分支是 dev,切换到此分支;假设我们需要合并的是 A 的一个 tag,如 v5.4.0,使用 git merge v5.4.0 --allow-unrelated-histories
,强制合并。
由于需要同步源框架项目代码,我们的改动会受到一些限制,否则每次合并都会有重复的工作量:
尽量不要重命名或者删除原本的文件,或者改动代码里面的函数及变量名; 如果需要拓展一个类的功能,尽量采用原型链拓展的形式; 自定义内部工具类可以内部自行定义,只要不重名即可; 行内代码尽量采用增加的模式,尽量不改动原本的代码; 有些库代码会增加很多渠道兼容的代码,我们可以适当减少,合并时会基于从共同祖先分析改变的机制,因此不会每次都要 diff。
通过以上方案,我们就可以实现官方仓库和定制化引擎的快速同步。
3. 游戏研发工程化
虽然 Egret 引擎能满足 Shopee Games 的基本业务需求,官方也提供了一系列工具来满足开发者的开发需求。但在使用 Egret 引擎的过程中,我们还是遇到了以下一些痛点:
缺乏模块概念:采用默认的 TypeScript 方式编译,不支持文件顶层 import 和 export,所有编译文件内容被视为全局可见,容易造成变量污染以及安全问题; 无法使用 npm:业务项目根目录下不支持 package.json 文件,不支持模块化的第三方库; 缺乏工程化方案:没有提供工程化的相关方案,如代码审查、单元测试等,项目也无法轻易接入常规的 Web 前端工程化方案; 部署流程复杂:代码编译工具依赖于官方工具,没有提供命令行版本,无法在服务器上单独部署。
显然 Egret 工程无法满足我们的工程需求。即便现在的 Web 前端工程化技术十分成熟,我们仍处于石器化时代,因此决定把 Egret 工程前端工程化。
3.1 Egret 前端工程化
3.1.1 支持根目录 package.json
package.json 文件可以说是目前前端项目必备的一个文件,Egret 引擎起家比较早,当时的前端工程化还没有那么成熟,Egret 引擎的构建是官方自己写的一套构建系统。
不支持根目录下 package.json 文件,很多事情也很难执行下去,还好 Egret 引擎的构建工具代码也是通过 JS 编写,而且跟引擎代码一样开源。
通过源码断点调试,我们发现 Egret 项目不支持根目录下 package.json 的原因是:Egret 构建的时候,通过判断根目录下是否存在 package.json 来区分工程项目和库项目,从而使用不同的构建流程,构建出不同的产物。
为了做到最小化的改动,且也能支持工程项目根目录下存在 package.json,我们把构建项目的判断修改为判断 package.json 内自定义字段的值,来区分是否为工程项目。
支持根目录下存在 package.json,后续的一些工程化改造就比较容易进行下去了。
3.1.2 引擎 npm 包
官方构建依赖于本地机器上的构建工具,每次的部署发布,都需要在本地构建完成后再上传到服务器上,与 Shopee 业务的部署规范和流程不太相符,并且严重阻碍了项目快速迭代的节奏。
为了使构建能够支持在服务器上单独部署,我们把定制化引擎的代码进行改造和封装,发布成一个 npm 包的形式,项目依赖从一个本地的构建工具变成 npm 包。
"dependencies": {
"@egret-engine/egret-core": "1.6.2-alpha.1",
}
npm 包主要包含两部分:
build 目录:引擎相关的库文件; tools 目录:构建编译相关工具。
发布成 npm 不仅使得项目的编译运行脱离本地环境,也能更好地去做项目的版本管理。但是仅发布成 npm 包是不够的,还需要结合以下的 Webpack 打包构建才能达到我们的目的。
3.1.3 Webpack 打包构建
为了支持模块化编译以及在服务器上单独部署,我们选择了成熟的 Webpack 构建方案接入到 Egret 项目中。
改造 Egret 项目构建前,首先需要分析一下 Egret 项目的依赖以及构建产物:
*.js
:代码构建产物。*.ts
:TypeScript 业务代码文件。res
:项目资源文件。例如:图片、音频、JSON 文件等。egret libs
:Egret 项目依赖模块,即相关的 JS 库文件。*.exml
:Egret 特有的标签语言文件类型,用作 UI 布局,可编译成 JS 文件和 JSON 文件。
官方的构建类似于 gulp ,按照一定的顺序执行每个任务。虽然官方也提供了自定义任务插件的方式,让开发者自定义构建流程,但这都需要开发者重新去开发,比较耗费人力。
exml 文件类型是 Egret 引擎特有的文件类型,目前前端生态没有相关的解析编译工具;res 文件处理也没有必要重新造轮子,所以我们沿用官方的工具,封装到 @egret-egine/egret-core/tools
上,作为构建工具的一个依赖。
而 egret libs 依赖处理和 *.ts 代码编译,我们都能在前端生态上找到更好的方案,根据需求使用即可。
通过 Webpack 去打包 Egret 项目,构建依赖来源于 npm,这样就可以脱离本地环境,直接在服务器上部署构建。而且产物也跟官方打包产物保持一致,做到良好兼容。
3.1.4 工程化配置
经过以上改造,其实 Egret 工程项目跟普通的 Web 前端工程没有太大区别,成熟的 Web 前端工程化方案在我们的项目中能得到很好的实践,不仅能够实现在服务器上单独部署,也能轻松接入质量把控的工具,例如 eslint、jest 等,提高代码质量。
3.2 Egret-Webpack-CLI 实现
在项目初期,我们主要根据业务和工程需求,基于 Egret 和 Webpack 搭建了项目脚手架模版。但在创建新项目和创建 demo 项目的时候,仍需要从仓库 clone 模版仓库下来,并且根据项目进行一定的人工配置。在目前的使用上看,问题不大,但仍然比较繁琐,也有可能会遗漏一些配置,新建项目不能做到开箱即用。因此开发脚手架工具,能够快速生成对应的模版项目。
3.2.1 CLI
一般脚手架工具主要分为 CLI 和 Template 两部分。脚手架模版内容并没有与 CLI 一起放到同一个仓库,而是分别放到不同的仓库进行管理和迭代。通过分离,可以确保两部分独立维护,不会互相干扰;模版配置或依赖更新只需要更新项目模版即可,无需影响 CLI 部分,导致重新发包。
参考其他脚手架的思路,模版作为独立资源发布到远程仓库上,然后运行的时候通过 CLI 工具下载下来,经过 CLI 的交互信息,作为交互的输入元信息渲染项目模版。
终端执行 egret-cli 后,即可根据交互信息,生成对应的 Egret 工程项目。
3.2.2 Template
由于我们需要对应不同的需求,且业务相关的配置较多,导致模版业务配置差异比较大,暂不能完全做到一个统一的模版。同时,为了保证一个模版内没有冗余的配置,我们做了区分,主要提供了 base、standard、Shopee、native 四种项目模版。
需要做小 demo 的时候,可以直接使用 base 模版,比较简洁;如果需要研究跟业务有关的功能,可以选择 Shopee 模版;如果是新项目的成立,则直接使用 native 模版。
3.3 最终 Egret 游戏开发流程
Egret 项目与常规 Web 前端工程接轨,既解决了开发痛点,满足了工程需求,也让我们从石器时代正式步入工业时代,从开发到部署都有很好的工具去辅助执行,提高了代码质量和开发效率,新来的同学也能很好地上手项目。
成熟的 Web 前端工程不仅有利于我们的业务扩展,也赋予了项目更多的可能性。一些在前端很容易实现而在原来游戏引擎比较难实现的功能,例如动态逻辑代码加载、多页应用、Egret+React 混合页面等,在我们的项目中也得到了很好的实践。
4. 总结
通过在多款 H5 游戏引擎中做比较,Shopee Games 选择了更适合业务特点和团队人才特点的 Egret 引擎。
在长期的业务开发运营中,我们为了更好地支持业务需求,对 Egret 引擎进行了定制化改造,包括 bug 修复和公共库的修改。
优化引擎的同时,为了和官方仓库保持同步,我们利用了 git 多 remote 仓库的特性,实现了双仓库代码合并。
再进一步,为了复用成熟的前端 Webpack 构建体系和 CI/CD 流程,我们自研了 Egret-Webpack-CLI,把 Egret 游戏从原来单机本地打包的模式,改为了服务器 Webpack 打包,从而方便复用大量的优秀前端 npm 库。
上述这些创新,都给 Shopee Games 的研发带来了重大提效。
除了在引擎优化和工程化上做出改进外,我们也成功解决了一些游戏研发当中的问题,例如 iOS 审核问题。下一期文章将通过对 Egret Native 原理的分析,以及与 Shopee App 的结合,来分享我们的解决方案。
本文作者
Shopee Games 前端团队。
加入我们
我们不仅会做一些游戏化的营销工具,而且还做真正的游戏!经营、养成、关卡、AR、PVP 等,多达几十种并不断增加。“好玩”是我们做游戏的初衷和目标,因为我们相信,游戏能建立情感、拉近距离,给用户带来快乐,为平台创造价值。
目前大量 iOS、前端、后端、测试、大数据开发岗位空缺中,感兴趣的同学可将简历发送至:vicky.zeng@shopee.com(邮件主题请注明:Shopee Games - 来自技术博客)。