本文作者:QHC
前言:
长久以来,传统前端的工作大多时候在与 DOM 打交道,近年来,浏览器厂商也在一直努力提高 DOM 渲染性能,以进步用户体验。然而更多简单场景的呈现,例如近几年随着在线直播、社交娱乐、各种小游戏的火爆,前端性能的关注度继续进步。特地是游戏场景,而咱们团队也面临着一大波 h5 游戏化场景,那么这个系列文章,将带读者敌人们一起理解,云音乐社交直播业务的游戏化场景解决方案的整体思路与落地案例分享。心愿能给大家在今后的开发中带来一些启发。
一、游戏开发的技术选型
其实,在后期咱们接到一些小游戏的需要时,我常常在想一个问题,就是为什么业界都主张应用 Canvas 来作为游戏开发的主旋律?咱们对于 DOM 的使用和了解,在某种程度上是比拟自信的。使用本人更相熟的伎俩去实现需求不就能够了么?
这里次要还是波及到性能问题和一些少见但会有的场景实现问题。上面就简略从性能和场景反对度两个角度来跟大家聊一下 Canvas 作为游戏开发主旋律的必要性。
1.1、性能比拟
首先为什么咱们能够这么必定地说 Canvas 的渲染性能比 DOM 来的优良。浏览器厂商明明在 DOM 渲染上曾经做了足够多的优化。比方渲染树的解决形式、重排重绘的机制优化、Chrome 浏览器通过预解析技术将 DOM 生成速度进步了 40% 等等看着都挺优良的优化。它还是不迭 Canvas 么?答案是必定的。因为尽管浏览器厂商在 DOM 渲染上做了很多优化,然而 DOM 元素是作为矢量图进行渲染的,每个元素的边距都须要独自解决,浏览器须要将它们全副解决成像素能力输入到屏幕上,计算量十分宏大。当页面上存在大量 DOM 元素时,这些内容的渲染速度就会变慢。相比之下,Canvas 实质上是一张位图,浏览器在渲染 Canvas 时只须要在 JavaScript 引擎中执行绘制逻辑,在内存中构建画布,而后遍历整个画布中的像素色彩,间接输入到屏幕上即可。无论 Canvas 外面的元素有多少个,浏览器在渲染阶段都只须要解决一张画布。
DOM:驻留模式 \
驻留模式(Retained Mode)是 DOM 在浏览器中的渲染模式。粗略工作流程如下(图片起源 https://zhuanlan.zhihu.com/p/400391575
Canvas:疾速模式\
Canvas 采纳了和 DOM 不同的疾速模式(Immediate Mode),粗略工作流程如下:
两者的区别在与驻留模式会生成一个(scene)和模型(model)存储到内存中,而后再调用零碎的绘制 API(如 Windows 程序员相熟的 GDI/GDI+),把这些两头产物绘制到屏幕。也就意味着场景中每减少一点货色就须要额定耗费一些内存。而这在即时渲染模式下是不会产生的。Canvas 绘制将这些场景和模型都交给开发者在开发阶段自我实现了。
1.2、场景反对能力
除了下面说的性能劣势以外。在一些特定场景下,Canvas 兴许是惟一解。是 DOM 所无奈代替的。比方经常出现在咱们游戏场景中的一些通明视频素材的动效或者其余比方 lottie 等格局的动效资源播控。
1.3、已有计划的抉择
咱们曾经晓得了整体的技术选型方向,接下来抉择一个适合的解决方案咱们认为须要思考到以下几点因素:
**1、对需要的反对能力(简而言之就是该技术栈是否可能让咱们把需要残缺的落地)\
2、页面性能(即游戏帧率、卡顿比、CPU 或 GPU 占用率等游戏相干指标)\
3、开发效率和保护老本 \
4、学习老本(这里指的学习老本应该是对于整个团队而言而非集体)\
5、技术撑持、技术生态 **
为此,咱们做了一些简略的比照
DOM + CSS | PixiJS | Eva.js | Cocos/Egret/Phaser* 业余的 H5 游戏引擎 | |
---|---|---|---|---|
页面性能 | 中 * 波及回流重绘时 | 高 | 高 | 高 |
开发效率 | 高 | 中 | 中 | 中 |
学习老本 | 低 | 中 | 中 | 高 |
技术团队撑持 | 社区 | 社区 | 社区 | 社区 |
性能反对 | 根底能力反对其余须要第三方库 | 短少原生 Flex 布局、通明视频、Lottie 等动效格局反对 | 根本与 PIXI 统一 | 较为全面反对公布微信小游戏等平台 |
思考到业务游戏场景的复杂度并没有十分高,轻量级的 js 库可能更适宜咱们这种业务场景,而业余的游戏引擎相对来说启动老本就比拟高了。所以咱们在后续的开发中,PixiJS 和 Eva.js 都有应用过。在这个过程中,积攒了一些教训的同时咱们体验到很多对于前端开发者来说十分不敌对的体验。
二、游戏开发中的痛点
1.1、社交直播业务中的游戏现状
与传统小游戏不同的是,在社交直播商业化玩法体系中。游戏往往是作为一个残缺的需要的一部分。在一个 web 页面中,不仅仅是只有一整片的游戏场景构建而成的,而是伴生着很多的传统页面元素的渲染和交互在外面。第二个特点是咱们的游戏场景中即时状态批改不会特地频繁(相似高频操作类),而根本都是线性的弱人机交互。以下几张截图是咱们曾经上线的一些小游戏
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/28021041026/e08c/a808/c191/8134306e5673913038b0102f530bc6cb.jpeg” alt=”” width=”40%”>
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/28021041023/0a76/7af7/edce/1f8f2a950131aba3b4eaa98b15595a93.png” alt=”” width=”40%”>
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/28021040537/b75c/820f/7230/067127918e2cfa083d156fbc589e83b2.jpeg” alt=”” width=”40%”>
<img src=”https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/28021041024/3734/95b3/4a94/0ba20c16276981bca8d17b0f6b4350aa.jpeg” alt=”” width=”40%”>
同时不难看出,在游戏界面中咱们还有大量的榜单、工作、聊天室、UI 弹窗等传统元素的绘制与交互。这也意味着在游戏开发过程中,如果咱们齐全应用三方游戏引擎如 Eva.js、PixiJS、cocos creator 来绘制页面的话,难免会损失肯定的 UI 排版、UI 细节解决上的效率。
1.2、痛点剖析
其实咱们面临的一个很大的问题是,PixiJS 也好、Eva.js 也好,它们无非是一套基于 Canvas 的渲染计划,而当前端开发者沉浸于 DSL 开发时(比方咱们团队就是以 react 为根底技术栈),PixiJS、Eva.js 并没有提供一套与之对应的 DSL 开发模式。这就使得咱们遭逢了几个重点难题:
痛点 1 、无奈高效的去绘制一些界面内容,各种元素的绘制都须要 append 节点来做,十分低效,而为了解决这个问题。咱们尝试将一个需要拆解为 DOM 层和游戏层这种分层设计,这样的确能够最大水平利用 DOM 的高效排版能力。可是这又带来了另外的问题;
痛点 2 、当 react 和这些渲染引擎的代码交叉呈现在业务中的时候,往往带来的代码治理老本是十分高的。比方状态治理就无奈在游戏侧和 UI 侧同时共享;
以一个卡牌类桌游场景为例。于是就有了以下这种很辣手的开发流程
痛点 3 、除此之外,代码里也须要有大量的订阅公布、面向对象开发、甚至有时须要独自保护一套状态机。在应用 Eva.js 的过程中,咱们还须要遵循 ECS 的架构思路来安顿本人的代码。这所有,都与 DSL 有所割裂。而齐全在需要中摒弃 DSL 却又会导致开发效率的直线下滑。
三、游戏 UI 要是能用 react antd 该多好
我想这应该是前端开发者在游戏开发过程中绕不开的一个想法。而其实 PixiJS 团队有提供一套 react-pixi 这样的库。于是咱们尝试去应用了。但咱们发现,它还是绝对比较简单。对于需要的实现咱们须要额定做很多别的事件。比方资源管理、事件治理、各种 css 布局能力、各种格局的动画素材播放能力、高效的缓动体系等。它都是不具备的。故此咱们自研 Alice.js 的想法萌芽了。这里分享以下三点对于 Alice.js 的外围观点
1、Alice.js 的指标是什么?
造成一套残缺的 H5 小游戏解决方案。在现有的 React 技术体系下,通过框架提供的游戏研发能力,让开发同学用相熟的 JSX 和 Hooks 语法编写动画、游戏场景的代码。
- 贴合理论业务,与 React 生态紧密结合(数据管理和 UI 构建)
- 反对 JSX 写法,学习成本低,会 React 就能疾速上手
- 轻量级、高性能、可扩大
- 造成一套残缺的 H5 小游戏解决方案
2:Alice.js 的应用场景是什么?
因为在渲染层咱们采纳了 PixiJs 来作为渲染引擎,所以如果要指定一个试用范畴,我想应该是所有 PixiJs 能够 cover 的场景,都能够应用 Alice.js 进行开发。而对于无奈单纯应用 PixiJs 实现的场景,通过 Alice 的高扩大能力也可能笼罩。当然了,因为 PixiJs 自身是一个 2d 渲染引擎。所以当咱们遇到 3D 场景时,目前是无奈笼罩的。
3:Alice.js 的劣势在哪里?
1、Alice.js 将渲染引擎和传统 UI 框架无效的进行了交融。这使得咱们能够用 JSX 标申明式开发游戏 UI 内容。也就是说,咱们提供一整套 DSL 游戏开发模式。\
2、咱们提供了一整套布局计划,你能够轻松的以 cssinjs 的模式对游戏元素进行排版和润饰 \
3、优良的可扩展性,反对了各种类型动效素材的播控,和各种游戏罕用组件的库的提供 \
4、提供了一整套游戏开发必备的资源管理体系,这使得游戏的资源管理变得十分高效 \
5、因为底层是借助 react-reconciler 编写自定义 renderer,所以人造反对应用各种状态治理库,技术栈割裂的景象将不复存在
四、Alice.js 的架构设计
Alice 的整体架构如下图:
篇幅起因,本文次要简略介绍 Alice.js 的整体架构设计,在本系列后续文章中,将具体为大家解说咱们是如何将这一整套架构的实现。敬请读者敌人们期待。
1、架构分层 - 桥接层
依据咱们的整体指标做一下拆解。首先,咱们心愿实现一整套基于 React 框架的申明式小游戏 DSL 开发模式。这也就意味着,咱们须要将传统的 eva.js 也好还是 PixiJs 的语法转为 React 框架下的 JSX 语法。例如,如下代码咱们实现一张 canvas 画布上绘制有蓝天白云、草地上有男孩、女孩。
<Stage>
<Sky>
<Cloud /> // 云彩是动静的
</Sky>
<Background>
<Boy /> // 人物能够做一些动作,这取决于动画素材
<Girl />
</Background>
</Stage>
1.1 买通 React 和 PixiJs 的桥梁
为了实现这一点,咱们利用了 react-reconciler
作为桥梁。react-reconciler
是一个形象层,用于实现自定义的渲染器。它容许你在 React 的根底上构建本人的渲染器,例如将 React 渲染到非 DOM 环境(如挪动端原生组件、Canvas 等)。
于是咱们领有了一个自定义的 renderer:
import Reconciler from 'react-reconciler';
const PixiFiber = Reconciler(hostConfig);
接下来须要实现 Stage 作为整个游戏界面的载体,咱们认为所有的游戏元素都应该在 Stage 里出现,而 Stage 组件自身输入的是一个 Canvas 元素而已。只不过咱们在 Stage 组件加载的各个生命周期里,须要调用咱们自定义渲染器能力,以 Stage 所输入的 Canvas 元素为画布,将各种游戏元素渲染到这张画布上。
自定义渲染器要害的调用节点在 <Stage />
组件的几个重要生命周期中:componentDidMount
、componentDidUpdate
、componentWillUnmount
。
1、当 <Stage />
在componentDidMount
阶段,调用PixiFiber.createContainer(PIXI.Application.stage)
办法创立 React reconciler
根节点,将 PixiJS 的舞台作为根节点。这样,PixiJS 的渲染后果就能够与 React Fiber 进行协调,实现将 PixiJS 和 React 联合起来的能力。并通过 PixiFiber.updateContainer
办法更新容器内容。值得一提的是 Pixi 的 Scene Graph 自身就是树结构,非常适合应用 JSX 语法构建。
2、在 <Stage />
的 componentDidUpdate
生命周期办法中,依据传入的属性通过 PixiFiber.updateContainer
办法更新容器内容。而在 <Stage />
外部的子元素状态更新时,因为这些子元素曾经处于 PixiFiber 创立的容器内了,<Stage />
作为 React Fiber 的根节点,任何对该容器内子节点的更新都会触发 React Fiber 的 diff 算法进行协调。也就人造反对子元素的自主更新了。换句话说,当舞台(stage)中的子节点发生变化时,PixiFiber 会应用 React Fiber 的协调机制来判断哪些子节点须要更新、增加或删除,并进行相应的操作。这包含比拟虚构 DOM(Virtual DOM)的变动、调度更新工作、执行生命周期办法等。
3、在 componentWillUnmount
生命周期办法中,通过 PixiFiber.updateContainer
办法清空挂载点里的所有内容,并销毁 PIXI 利用实例。
1.2 丰盛的 Pixi 原子
咱们曾经晓得,有一个 <Stage />
组件来作为主舞台对应 PIXI.Application.stage
,那么如何把一个 PIXI 元素作以子组件的形式增加到<Stage />
呢?比方一个根本的精灵图PIXI.Sprite
。
这里次要须要理解的是 react-reconciler
的参数HostConfig
对象,这个对象定义了自定义渲染器的行为。
hostConfig
对象的办法包含:
createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle)
: 创立新的节点实例。finalizeInitialChildren(parentInstance, type, props, rootContainerInstance, hostContext)
: 在创立初始子节点后,实现初始化操作。prepareUpdate(instance, type, oldProps, newProps, rootContainerInstance, hostContext)
: 在节点更新前,筹备更新所需的信息。commitUpdate(instance, updatePayload, type, oldProps, newProps, internalInstanceHandle)
: 执行节点的更新操作。appendChild(parentInstance, child): void
: 将子节点增加到父节点中。insertBefore(parentInstance, child, beforeChild): void
: 在指定子节点之前插入一个新的子节点。removeChild(parentInstance, child): void
: 从父节点中移除一个子节点。appendChildToContainer(container, child): void
: 将子节点增加到容器中。removeChildFromContainer(container, child): void
: 从容器中移除一个子节点。
这些办法的具体实现将取决于自定义的渲染器的需要和个性。createContainer
办法将应用你提供的 hostConfig
对象来执行相应的操作,并在容器中渲染和更新 React 元素。
再来看咱们所抛出来的问题:如何把一个 PIXI 元素作以子组件的形式增加到 <Stage />
呢? 当咱们调用 PixiFiber.updateContainer
时,就会对 <Stage />
里所有的子元素进行更新,比方 PIXI.Sprite
就是其中一个子元素,它的 JSX 表现形式为
<Stage>
<Image />
</Stage>
当咱们的自定义 PixiFiber
调度遍历到 <Image />
时,会执行咱们提前设计的 HostConfig
对象中的createInstance
, 在这个办法里,我能够做想做的任何事件,比方创立一个PIXI.Sprite
实例并。同样的情理对于树节点的插入、移除、更新都能够利用对应的HostConfig
对象属性来进行操作。以下是要害节点的伪实现代码
// 创立一个 react 虚构 dom 树节点对应的 pixi 元素
createInstance(type, props, rootContainer) {
// 创立实例
const instance = new PIXI.Sprite();
// 设置实例在要害生命周期和属性利用时所须要执行的办法
instance._customDidAttach = () => { ...}; // 在被挂载时执行
instance._customWillDetach = () => { ...}; // 在被卸载时执行
instance._customApplyProps = () => { ...}; // 将属性设置到 Pixi 元素上
// 将 props 属性增加给 Sprite 实例
instance._customApplyProps(props);
return instance
}
// 在一个父节点中插入子节点
appendChild(parent, child) {parent.addChild(child);
// 执行子节点被挂载自带办法
child._customDidAttach.call(child, child, parent);
}
// 在一个父节点中移除子节点
appendChild(parent, child) {parent.removeChild(child);
// 在被卸载时执行
child._customWillDetach.call(child, child, parent);
// 销毁子节点实例
child.destroy();}
...
如此,咱们就能够胜利的将一个 PIXI.Sprite
渲染到 Stage
之中。而以上代码只是简略阐明了如何将单个 PIXI.Sprite
渲染进去,在理论生产中,咱们可能会用到十分多的 Pixi 元素。所以把这些可能会用到的 pixi 原生元素都对立封装起来,造成一个 elements 汇合
这样便极大水平丰盛了 Alice 所反对的 Pixi 原子,而这些原子将来在引擎层能够封装成为更多具备定制能力的组件。
1.3 定制化元素的反对
咱们曾经晓得,在桥接层 Alice 提供了很多 Pixi 原生的元素,作为渲染节点。然而在理论生产中,往往咱们须要自定义的去扩大一些定制组件,比方须要实现一个能够播放 lottie
素材的动画组件<LottieSprite />
。那么该组件底层渲染肯定也是基于 PIXI 去实现的,于是咱们就需去做以下几个步骤来实现这样一个自定义的组件。(其实这个自定义的组件能够了解为相似于 DOM 里的<div />
)
第一步:申明一个类,该类继承于桥接层所提供的原生组件<Image />
, 并且在初始化的时候具备解析 lottie 素材为纹理的能力。以及能够监听参数变动时触发渲染内容的更新。
第二步:通过工厂函数,输入一个具备残缺生命周期的组件,这里就是在上文提到的节点的插入、移除、更新等办法。
第三步:createInstance
的过程中可能调用这些生命周期函数,从而达到渲染到画布的成果
为了达到这一目标,咱们在桥接层提供了 PixiComponent
这样的一个注册函数(工厂函数),以实现在下层(引擎层)创立自定义元素,能够很好的被底层 Pixi 所渲染。
1.4 桥接层的扩展性
在桥接层咱们能够做很多事件,因为在本计划中,咱们应用的 React 作为 DSL 技术计划,所以在桥接层咱们用了 react-reconciler
作为渲染调度器。如果咱们换成 Vue 的话呢?其实只须要将调度器换成 vue-next
根本就能够转换技术栈。
除此之外,因为咱们把握了虚构节点树转换为实在渲染节点的残缺周期,所以咱们能够在周期内赋予一些其余的游戏元素能力,比方物理成果。在元素被挂载时,就能够实例一个 Matter.Engine
实例,并将一个对应的刚体退出Matter.Composite
。同步刚体的坐标与 Pixi 元素的坐标,就能够实现一个简略的物理世界成果。
如果有需要,咱们甚至能够仍然应用这套框架去实现一个基于 3d 渲染的能力扩大
2、架构分层 - 引擎层
依据架构图,咱们能够发现桥接层的下层是引擎层,这也就意味着引擎层是与应用层更加靠近的一层。所以在引擎层,咱们减少一些业务能力比方:基于桥接层封装了更多游戏业务罕用的一些组件
、 依赖桥接层提供的自定义原生组件能力扩大了更多动画播控组件
、 基于 yoga 提供了弱小的 flex 布局能力
、 基于 tween.js 提供了高效的缓动成果开发能力
以及各种交互事件的转发和 元素之间碰撞检测能力
。这一块内容咱们会在本系列的后续文章中具体介绍其实现计划和原理。敬请期待。
3、齐备的资源管理体系
在架构图中,资源管理的确是最顶端的一层。而这一大节的题目,没有带”架构分层“的字眼。
先说一下咱们架构图中的资源管理层。在最开始,咱们的想法比拟间接,在游戏的开发过程中,个别须要应用到大量的图片、动画素材、音频等资源来丰盛整个游戏内容,而大量的资源就会带来治理上的艰难。因而提供了一套资源管理器来帮忙开发者治理其资源的应用。开发者编写游戏时,无需关怀资源的预加载、解析、纹理转换等工作。只须要在资源 Map 文件里申明须要预处理的资源即在我的项目中随处可用。与此同时,将提供丰盛的加载生命周期钩子、资源插入和销毁等 API。以及进度条的 UI 组件。
那么为什么现在咱们把它从整个 Alice 里拿出去了呢?次要思考到其实这套资源管理模式,不仅仅是在游戏开发中纹理转换场景能够应用。在咱们惯例的 H5 流动中,也能够把这套管理体系拿来应用以进步整个界面的晦涩度和用户体验的晋升。
在本系列后续文章中,会有专门一篇来介绍资源管理的计划和实现。会具体介绍它与目前咱们罕用的已有资源管理三方包的区别。敬请期待。
五、本篇小结
在本文中,咱们探讨了 Alice 游戏引擎的架构设计,并介绍了其分层构造中的要害设计理念和性能。通过桥接层、引擎层和资源管理层的划分,Alice 游戏引擎提供了灵便、可扩大的开发环境,满足了游戏开发和利用开发的不同需要。
在桥接层,咱们实现了 DSL 与 Pixi 渲染能力的联合。这为申明式的游戏场景开发提供的可能性。
在引擎层,咱们基于桥接层提供了丰盛的业务能力和组件,包含罕用游戏组件的封装、动画播放管制、灵便的布局能力以及高效的缓动成果开发能力,为开发者提供了便当和效率。
资源管理层则为开发者提供了一套齐备的资源管理体系,简化了资源的加载和治理流程,不仅实用于游戏开发,还能够在其余 H5 流动中应用,晋升界面的晦涩度和用户体验。
通过对 Alice 游戏引擎的架构设计和性能介绍,咱们能够看到它为开发者提供了比拟高效的游戏场景开发模式,帮忙开发者更高效地创立出丰盛、晦涩的游戏和利用。在后续的文章中,咱们将深入探讨每个层级的具体实现计划和原理,
在将来的倒退中,咱们会推动更多对于 Alice 游戏引擎的技术摸索和翻新,以及它在理论我的项目中的利用案例。通过不断完善和扩大,冀望 Alice 可能造成一整套 H5 游戏场景解决方案。
参考资料
- Eva.js:https://eva-engine.gitee.io
- pixi-react:https://github.com/pixijs/pixi-react
- react-konva:https://github.com/konvajs/react-konva
- 详解 Canvas 优越性能: https://zhuanlan.zhihu.com/p/400391575
本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!