Shopee Games 游戏引擎演进之路
本文作者: Shopee Games 前端团队。摘要
Shopee Games 团队致力于丰富 Shopee 电商内的互动性和娱乐性,让用户在购物之余获得更多愉悦感,同时游戏也能为 Shopee 带来持续的活跃用户和更多的优惠券发放渠道。在这个背景下,从游戏诞生之初,我们希望游戏足够轻量,而且能够快速迭代,持续给用户提供多种多样的游戏体验,同时又不会对 Shopee App 的体积造成较大影响。因此,我们需要选择合适的游戏引擎,并打造适合 Shopee Games 的工具链。1. 游戏引擎选型 Shopee Games 当前以休闲类游戏为主,为了减少对 Shopee App 体积的影响,技术选型上会偏向于 H5 游戏。而如何选择 H5 游戏引擎,我们主要考量以下几个方面的因素:
本文将介绍 Shopee Games 团队如何选择游戏引擎,如何扩展游戏引擎以提高生产效率,如何让游戏开发流程和成熟的前端工程化体系结合,实现游戏规范化和研发质量的提升。Shopee Games 是内嵌在 Shopee App 的游戏,所以对于有同样内嵌游戏需求的业务团队,本文总结的经验会有一定借鉴意义。
- 2D 还是 3D 游戏?
- 是否对开发友好?包括是否支持 TypeScript、文档是否完善、研发流程是否适合以开发为主导。
- 性能和兼容性如何?
- 官方工具链是否完善?
- 是否开源?
- 是否有成功的游戏案例?
- 官方是否有客服支持?
- 官方是否有持续更新?
接着,从可持续性、性能方面考虑,可以先把较老的 lufylegend.js、Cocos2d-JS 排除。而 CreateJS 实际并不是一个完整的游戏引擎,它更接近于一个精简的渲染引擎,缺少整体的工具配套,难以支持大型游戏,也排除。
那么,最后我们重点对比 Egret、Cocos Creator 和 Phaser。Phaser 的渲染引擎就是 Pixi,后续用 Phaser 代表这两者。
文章图片
以上三款游戏引擎都支持 TypeScript 和 WebGL,性能差异不大。对于 Shopee Games 团队而言,Egret 有较大优势:
- Egret 支持 canvas 模式,因此东南亚市场中的一些低端手机用户也能够运行我们的游戏;
- 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 事件的监听和移除;
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. 游戏研发工程化 【Shopee Games 游戏引擎演进之路】虽然 Egret 引擎能满足 Shopee Games 的基本业务需求,官方也提供了一系列工具来满足开发者的开发需求。但在使用 Egret 引擎的过程中,我们还是遇到了以下一些痛点:
- 缺乏模块概念:采用默认的 TypeScript 方式编译,不支持文件顶层 import 和 export,所有编译文件内容被视为全局可见,容易造成变量污染以及安全问题;
- 无法使用 npm:业务项目根目录下不支持 package.json 文件,不支持模块化的第三方库;
- 缺乏工程化方案:没有提供工程化的相关方案,如代码审查、单元测试等,项目也无法轻易接入常规的 Web 前端工程化方案;
- 部署流程复杂:代码编译工具依赖于官方工具,没有提供命令行版本,无法在服务器上单独部署。
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. 更多研发问题 4.1 iOS 审核问题背景 在 Shopee Games 推出早期,用户量和访问量都不大,苹果公司没有着重针对 HTML5 版本的 Shopee Games 提出意见。但是,随着 Shopee 业务不断发展,用户量和访问量持续增加,2021 年初,苹果公司对 Shopee 内嵌的 HTML5 游戏提出了意见,认为 Shopee App 违反了《App Store Review Guidelines》的 4.7 条款。
苹果公司的审查条款原文是这样的:
4.7.1 Software offered under this rule must:
- be free or purchased using in-app purchase;
- only use capabilities available in a standard WebKit view (e.g. it must open and run natively in Safari without modifications or additional software);
and use WebKit and JavaScript Core to run third-party software and should not attempt to extend or expose native platform APIs to third-party software;
- be offered by developers that have joined the Apple Developer Program and signed the Apple Developer Program License Agreement;
- not provide access to real money gaming, lotteries, or charitable donations;
- adhere to the terms of these App Store Review Guidelines (e.g. do not include objectionable content);
and
- not offer digital goods or services for sale.
归纳起来,包含以下几点要求:
- 全免费的 H5 游戏或使用苹果支付;
- 仅使用 WebKit 自带功能,不允许扩展,例如 JSBridge;
- H5 开发者需要加入苹果开发者计划;
- 不允许涉足金钱赌博,而且内容要符合其他审查条款一般限定。
我们参考了 Egret 引擎官方的建议,改变了 iOS 平台上 Shopee Games 的技术架构,从原来的 HTML5 改为了 Native,使用的是 Egret Native Runtime。Egret 官方工具支持一键生成 iOS 工程并发布对应的 App 安装包,这能够满足独立游戏的需求。但是 Shopee Games 需要内嵌在 Shopee App 中,并不是独立的游戏 App,所以无法直接简单使用官方的一键发布机制,我们需要进一步研究 Egret Native 的原理,从而和 Shopee App 做整合。
4.2 Egret Native 原理 分析 Egret Native 工具创建的 iOS 模板工程,可以发现:
- Egret Native Runtime 依赖一系列的系统库和独立封装的 libEgretNativeIOS.a,主要核心逻辑和网络功能都在这个库中。由于 Egret Native 并不开源,从模板工程无法得知这里的具体实现;
文章图片
- 固定以 App 根目录的 assets 文件夹作为资源目录,H5 版本的生成文件需要固定存在此处,而且这里只支持一个游戏;
- 通过 libEgretNativeIOS 库,可以创建相应的 EgretNativeIOS 实例和对应的 view。
文章图片
虽然 Egret Native 并不开源,但根据官方文档,再结合模拟器断点调试分析,还是可以对 Egret Native 有进一步的发现。
Egret Native 游戏架构包括三层:前端游戏层、Egret Native Runtime 和 iOS Native 层。
- 前端游戏层保持和 H5 版本的文件内容一致;
- Egret Native Runtime 是核心的适配层,它使用 JSCore 对游戏包内的 JS 文件进行解析,搭建 JSBridge 实现 JS 和 Native 两侧的通信。运行时渲染的方式和 Web 有所区别,Egret Native 有一个针对 Native 的 JS Polyfill 和 JS Engine 补充,并不是在 Runtime 层实现了全部浏览器功能;
- iOS Native 层,主要是管理 Egret Native Runtime 生命周期和管理视图。
文章图片
启动 Egret Native 游戏时,先从 iOS Native 层初始化 Egret Native 实例,并创建对应的游戏 view,挂载到主界面上。然后,Egret Native 初始化 JS 引擎,绑定 JSBridge,读取前端游戏层的游戏资源,解析 HTML 和 JS,调用 OpenGL 接口,最终显示游戏画面。
4.3 Egret Native 和 Shopee App 结合 从模板工程来看,把 Egret Native 游戏内嵌到 Shopee App 的方式是比较清晰的,把 libEgretNativeIOS.a 和其他必要的依赖库添加到 Shopee App 项目工程中,再把游戏包存放到 assets 目录中,最后绑定必须的 Shopee JSBridge,供游戏逻辑实现登录、支付、跳转等操作,整个结合工作就完整了。
但是,在最终实现过程中,发现一些深层次问题,需要通过业务侧的方式解决或规避。这些问题包括:
1)模板工程不支持多个游戏
Shopee Games 包含多个游戏,而上述 assets 目录只支持存放一个游戏的资源。并且 Egret Native Runtime,也就是上述的 libEgretNativeIOS.a 没有开源,无法做针对性的修改。
最后,我们在官方的《热更新方案说明》中,发现了 Egret Native 可以设置 preload 目录路径,而 preload 目录内的资源优先级比 assets 目录高。
于是,我们可以在 Shopee App 内置多个游戏,分别存放于一个单独的目录中,在启动 Egret Native 前设置 preload 路径为对应游戏的路径。Egret Native 启动后会优先读取 preload 目录的资源来启动游戏,忽略后续 assets 的内容。
2)网络缓存文件无限增大
Egret Native Runtime 对网络请求做了缓存,但没有完善的清理机制,导致本地缓存目录会随着游戏下载的资源增多而无限增大。这部分需要在 Shopee App iOS Native 逻辑层面进行补齐。
3)部分第三方库命名冲突
libEgretNativeIOS.a 自带了一些第三方库,例如 SocketRocket,而 Shopee App 也引入了这个库,双方都没有对这个库做别名处理,导致命名冲突编译失败。由于 libEgretNativeIOS.a 无法修改,只能在 Shopee App 业务层面自行修改 SocketRocket。
4)Egret Native 存在内存泄漏
由于在 Shopee App 中,Egret Native Runtime 需要反复启动和销毁,只要 Egret Native Runtime 对对象、纹理处理稍有不当,都会引起内存泄漏。而对于 Egret Native 独立游戏来说,这个问题就不存在,因为每次关闭游戏都是整个 App 的销毁。这个问题已经反馈给 Egret 官方,但由于官方团队不会专门针对我们的使用场景做处理,所以目前暂无进展。庆幸的是,这部分内存泄漏很小,暂时不构成大的问题。
通过解决上述一系列问题,最终我们实现了 Egret Native 和 Shopee App 的结合,能够在 Shopee App 上运行多款自研游戏。在苹果审核的角度,我们这个方式脱离了 Webkit,而且所有代码逻辑内置在提审的安装包中,完全符合审查条款的要求。因此,方案上线后,Shopee App 和 Shopee Games 顺利通过了 App Store 的审核。
4.4 未来规划 虽然 Egret Native 已经能够结合在 Shopee App 中,但在持续运营半年后,我们发现 Egret Native 还是存在一些问题:
- Runtime 不开源,存在无法解决的问题;
- Runtime 只支持 Egret 游戏,无法支持其他游戏引擎。尤其是,Egret 引擎对 3D 游戏开发来说,暂时还不是最佳选择。
这个解决方案采用类似微信小游戏的整体架构,但在 JS 层面会做进一步的封装,方便 Shopee 内各个游戏团队快速接入。技术层面概要来说,就是 Native 基于 JS 引擎向 JS 侧暴露对齐 WebGL 标准的接口和必要的 BOM 接口,从而普通 H5 游戏引擎就可以无缝运行在浏览器和我们自研的 Runtime 上。
文章图片
这个方案的好处有:
- 兼容性好,能同时支持多家游戏引擎;
- 方便存量 H5 游戏转移;
- 能利用各家引擎成熟的开发工具链。
- iOS 12 之后的版本正逐步放弃对 OpenGL 的支持;
- 由于兼容多家引擎,难以使用新的图形图像标准,例如 Metal、WebGL2、Vulkan。
目前这套方案还在预研开发中,预计 2022 年内会实现并全面使用。之后不单是 Shopee Games,我们希望在更多的电商场景也能复用这套 3D/GPU 渲染的方案,给用户创造更丰富多彩的互动玩法。
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,顺利实现游戏的 Native 化,通过了 App Store 的审核。
文章图片
推荐阅读
- Unity游戏开发学习路线和资源分享
- 历史上的今天|【历史上的今天】3 月 4 日(美团网正式上线;Dropbox 的创始人出生;PS2 游戏机问世)
- C++实现三子棋游戏详细介绍(附代码)
- C语言实现简单的猜数字游戏
- 【游戏测试】客户端性能 - drawcall 工具链
- 利用Matlab复刻扫雷小游戏
- 《游戏人工智能中A*算法的应用研究》阅读笔记
- 基于Matlab制作一款简单的龙舟小游戏
- 教你用Matlab制作黄金矿工小游戏
- Three.js系列:|Three.js系列: 写一个第一/三人称视角小游戏