共计 11472 个字符,预计需要花费 29 分钟才能阅读完成。
本文作者:小林
前言
在系列上一篇文章中,咱们介绍了自研 H5 小游戏引擎 Alice.js 的理念与架构设计,以及外围性能的实现。通过联合 React 生态与 WebGL 渲染能力,咱们能够让相熟 React 的开发人员低成本地入门 H5 游戏开发,在复用现有组件资产的同时,提供高性能的游戏画面,实现更简单的视觉效果。
在本篇中,咱们会联合一个理论的案例,来介绍如何通过 Alice,应用 React 的形式高效开发 H5 小游戏。
一、场景构建
在游戏开发中,场景(Scene)的搭建是非常重要的环节。就像电影中的一个场景一样,游戏场景是一系列游戏元素的汇合,表白了游戏世界的一部分内容,也是咱们开发时组织游戏内容的核心。
本节咱们将借用 Cocos Creator 官网的示例,制作一款简略的 2D 平台跳跃类游戏。游戏规则也很简略:
- 开始游戏后,在地面随机生成肯定数量的冰块,每两块冰之间可能空 1 格或者不空
- 企鹅一次能够向右跳 1 格或者 2 格
- 企鹅跳到冰块上不会掉下去,跳到空白处就会掉下去
- 跳齐全部的格子即游戏通关,中途落下则游戏失败
▲ 背景图素材来自 OpenGameArt.org
1.1 用 React 组件组织游戏物件
作为相熟古代前端框架的前端开发,拿到下面这样的一个页面,首先想到的是什么?没错,组件化!天上的云能够是组件,地面的冰块能够是组件,两头的企鹅也能够是组件,它们独特形成了这样的一个游戏场景。组件化、可复用 是 React 的核心思想,这在 Alice 引擎中天然也实用。
将下面的场景形象为组件树后,就是这样的:
这样的树状构造也称为 场景图(Scene Graph)。场景图是一种通用的数据结构,通常用于组织 2D/3D 图形场景中节点的逻辑与空间示意。怎么样,是不是和感觉和咱们的前端框架有些共通之处呢?
让咱们更进一步,将其拆分为具体的 React 组件。用 JSX 示意进去就是这样的:
// 背景图层
const Background = () => (
<View>
<Image src="assets/bg.png" />
<Image src="assets/cloud.png" />
<Image src="assets/tree.png" />
</View>
);
// 咱们的配角小企鹅
const PenguinHero = () => (<Image src="assets/penguin.png" />);
// 冰块们
const IceBricks = () => (
<View>
{array.map(() => <Image src="assets/brick.png" />)}
</View>
);
// 组合成一个游戏场景
const Scene = () => (
<View>
<Background />
<PenguinHero />
<IceBricks />
</View>
);
That’s it! 在 Alice 中,创立、组合组件就是这么天然,所有都和你相熟的一样。
1.2 场景渲染与相机管制
一个游戏通常由许多场景形成,比方主菜单是一个场景,游戏界面是一个场景,结算界面也是一个场景。那么在 Alice 中,咱们要如何管制游戏场景之间的切换呢?
答案也很简略,React 怎么做,咱们就怎么做。你能够 if-else 一把梭:
function Game() {const [currentScene, setCurrentScene] = useState(SCENE.MAIN_MENU);
// 游戏场景
if (currentScene === SCENE.GAMEPLAY) {return <Gameplay />;}
// 结算界面
if (currentScene === SCENE.RESULT) {return <Result />;}
// 主菜单
return <MainMenu />;
}
也能够间接前端路由走起,丰俭由人。因为咱们通过 react-reconciler 实现了残缺的自定义 React renderer(具体能够参考本系列的上一篇专栏),所以咱们齐全能够复用现有 React 生态中的成熟类库。
用户须要做的就是管制组件,剩下的交给 Alice 引擎实现:
import {BrowserRouter, Routes, Route} from 'react-router-dom';
function Game() {
return (
<BrowserRouter>
<Routes>
<Route path="main-menu" element={<MainMenu />} />
<Route path="gameplay" element={<Gameplay />} />
<Route path="result" element={<Result />} />
</Routes>
</BrowserRouter>
)
}
除了场景的构建,在游戏中,相机管制也是一个重要的步骤。游戏中的「相机」概念相似于事实世界中的相机,次要用于捕获场景画面,管制场景的出现,如可视范畴、投影、缩放等。
在 Alice 中,相机默认捕获整个 Stage。不过这里咱们须要让相机随着角色跳跃主动向前挪动,也就是实现横向卷轴成果。步骤也很简略,将整个游戏场景都包裹在 <CameraView />
中,并指定要追随的元素即可:
<CameraView follow={penguinRef}>
{/* 不论整个场景有多大,咱们的企鹅始终都固定在屏幕两头 */}
<Box ref={penguinRef} style={{position: 'absolute'}}>
<Image src={resources.penguin} />
</Box>
{/* 剩下的地图场景组件…… */}
<Map />
</CameraView>
这里的底层原理是监听指标元素的地位变动,将其位移的绝对量弥补到整个场景的容器上,即可实现追随成果。同时配合剔除(Culling)技术,防止屏幕外不可见的对象节约渲染与计算资源。这样咱们就能够制作出比屏幕尺寸大得多的游戏场景,并让角色在其中自由行动。
1.3 联合原生 DOM 编写 UI
在游戏开发中,除了游戏场景、角色、动画这类频繁更新的元素之外,还有绝对动态的 UI(用户界面)元素,它们独特形成了一个残缺的游戏。UI 承载了游戏状态信息的展现,以及承受用户交互的性能,相干的元素包含标签、按钮、滑块、菜单、文本框等。
其中局部 UI 是动态的,或者很少更新,比方 HUD、跳转按钮、布告、设置页面等。如果是传统游戏引擎,咱们可能须要应用引擎提供的 UI 组件将这些界面画进去,比拟繁琐。既然咱们抉择了依靠于 React 框架去开发游戏,那是否意味着咱们也能够间接应用原生 React DOM 来编写这些 UI 呢?
答案是必定的!Alice 反对 canvas 元素和 DOM 元素的混合开发,联合前者的高性能与后者开发速度快的长处。在开发中还能够间接复用现有的 React 组件库,编写 UI 高效快捷。
<div id="root">
<!-- HUD 叠加在游戏场景的下层 -->
<div className="hud-wrapper">
<p>Write any HTML here</p>
</div>
<!-- 我是分割线,上面就是 canvas -->
<Stage className="game-wrapper">
<Image src="xxx">
<Text>Inside game we are canvas elements!</Text>
</Stage>
</div>
但须要留神,因为渲染程序的限度,DOM 元素只能呈现在 canvas 的下层。
二、节点元素
在 Alice 引擎中,元素(Element)是游戏场景的根本组成单元,这一点和 HTML 相似。
在场景中,所有物件都由元素组成,其中包含容器、图片、文字、图形等根底元素,以及帧动画、Lottie、视频、骨骼动画等动效元素。所有元素组成了树状的 Scene Graph。
<!– 为了不便管制,咱们心愿所有的元素都派生自一个基类,并由一个对立的容器包裹,即 GameObject
。这样的设计为未来的扩展性提供了保障(比方可视化编辑器就能够间接在其中增加操作柄与相干事件)。–>
2.1 根底元素
在 Alice 中,根底元素包含:
- 根底容器
Box
- Flex 容器
View
- 图片
Image
(反对精灵图集 Spritesheet) - 文本
Text
- 图形
Graphics
- 遮罩
Mask
- 点九图
NinePatch
应用这些元素构建游戏界面就像编写传统 React 利用一样合乎直觉:
因为这些元素都是标准的 React 组件,循环渲染、条件渲染等性能天然也不在话下:
{/* 搁置一排冰块 */}
{map.map((isBrick, index) => (
<Image
key={index}
src={isBrick ? resources.brick : Texture.EMPTY}
style={{
position: 'absolute',
top: 25,
left: -35 + index * BOX_WIDTH,
width: 76,
height: 67,
}} />
))}
2.2 动效元素
动效是游戏体验中非常外围的一环,适当的动效能够为游戏增色不少。Alice 目前反对增加以下格局的动效:
- 序列帧动画
FrameAni
&Apng
- Lottie 动画
Lottie
- 一般视频
Video
- 通明视频
AlphaVideo
- 骨骼动画
Spine
&DragonBones
- 基于关键帧的过渡动画
用户能够依据须要,抉择不同的动效格局。各种格局的比照大抵能够参考下表:
序列帧 | Apng | Lottie | 一般视频 | AlphaVideo | 骨骼动画 | 过渡动画 | |
---|---|---|---|---|---|---|---|
视觉还原度 | 高 | 高 | 高 | 中: 不反对通明通道 | 高 | 高 | 低: 开发实现 |
资源文件大小 | 中: 不适宜大尺寸动图 | 大: 压缩比不高 | 小 | 中: 看码率 | 中 | 小 | 很小 |
JS 体积 | 小 | 中: 须要引入解码模块 | 很大: 须要引入播放库 | 很小 | 小 | 大 | 小 |
内存占用 | 小 | 大: 须要额定存储解码帧 | 中 | 小 | 小 | 中 | 很小 |
渲染性能 | 很高 | 低 | 中 | 高 | 高 | 中 | 很高 |
内容动静替换 | 否 | 否 | 是 | 否 | 否 | 是: 非常灵活 | 是 |
兼容性 | 好 | 差: 低端机可能内存不足 | 好: 取决于播放库 | 好 | 中 *WebGL | 好 | 很好 |
在本系列的后续文章中,咱们会介绍是如何将这些动效接入 Alice 引擎的。从咱们团队的实践经验来看,在应用了 Spritesheet 格局的状况下,序列帧解析简略、实现不便、渲染性能好。在动效较短时,举荐应用序列帧作为首选动效格局(能够应用 TexturePacker 或者 Free Texture Packer 等工具生成)。
这里咱们应用一个动静的企鹅动画,替换掉之前的动态图:
-<Image
- src="assets/penguin.png"
+<FrameAni
+ src="assets/penguin-spritesheet.json"
+ ref={aniRef}
+ loop
+ autoplay
+ onComplete={() => console.log('播放实现')}
style={{
scale: 0.5,
anchor: 0.5,
}} />
+aniRef.current.play()
+aniRef.current.stop()
+aniRef.current.currentFrame
+aniRef.current.totalFrames
三、属性与变换
当初,咱们曾经能够通过相似 HTML 的语法组织游戏元素了。那么你可能会想,既然如此,那能不能用相似 CSS 的语法来管制这些元素的款式呢?
能够!Alice 反对了大部分的根底 CSS 款式和关键帧动画,甚至提供了基于 Flexbox 的动静布局能力。
2.1 CSS 款式转换
要实现应用 CSS 编写款式,其外围就是将 CSS 的语法转换为底层的 PixiJS 对应属性。比方:
font-size
->PIXI.Text#style.fontSize
height
->PIXI.Sprite#height
left
->PIXI.DisplayObject#position.x
opacity
->PIXI.DisplayObject#alpha
background-color
->PIXI.Sprite#tint
为此,咱们编写了专门的款式转换器,用户能够应用相似 React Native 的语法间接书写大部分的 CSS,无需额定的学习老本。
<Text
style={{
fontFamily: "Chalkboard,'Comic Sans MS', sans-serif",
// 以下几种写法等价
// color: 0xf0f8ff,
// color: '#f0f8ff',
// color: 'rgb(240, 248, 255)',
// color: 'hsl(208, 100%, 97%)',
color: 'aliceblue',
fontSize: 30,
fontWeight: 'bold',
fontStyle: 'italic',
}}>
Will be rendered with Canvas2D internally
</Text>
2.2 Flex 布局零碎
在传统小游戏引擎中,元素的排布个别应用相对定位(写死宽度、高度、XY 坐标),或者反对无限的主动布局。既然咱们款式属性曾经能够用 CSS 来写了,那么元素布局是不是也能用 CSS 的 Flexbox 弹性布局那一套呢?
能够!Alice 底层接入了 React Native 所应用的跨平台高性能 Flex 布局引擎,即 Yoga Layout,并集成在框架中作为可选性能提供。Yoga 引擎提供了欠缺的 Flex 布局性能,反对 WebAssembly,以及在旧版本浏览器上回退到纯 JS 版本。配合 justifyContent
、alignItems
等 CSS 语法,简直能够完满再现传统 React 利用的开发体验。
<View
name="OuterView"
style={{
width: 500,
height: 500,
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'center',
padding: 50,
}}>
<View
name="ProfileCard"
style={{
width: 300,
height: 80,
marginBottom: 30,
flexDirection: 'row',
}}>
<View
style={{
width: 80,
height: 80,
backgroundImage: 'assets/avatar.png',
}} />
<View
style={{
justifyContent: 'space-around',
padding: '10 0 10',
marginLeft: 20,
}}>
<Text style={{fontSize: 20, color: '#0f172a'}}>
Lorem Ipsum
</Text>
<Text style={{fontSize: 16, color: '#64748b'}}>
Alice in Wonderland
</Text>
</View>
</View>
<View name="Content" style={{flexWrap: 'wrap', flexDirection: 'row'}}>
{Array(14).fill(0).map((_, index) => (
<Image
key={index}
style={{width: 60, height: 60, margin: 10}}
src="assets/marshmallow.png" />
))}
</View>
</View>
▲ 以上所有元素都渲染在 canvas 中。图片素材来自 eiyoushi-hutaba.com
在这里,咱们通过为每一个 Flex 子元素创立伴生的 Yoga 节点的形式,结构了一棵与组件树同构的布局树。当组件的布局属性产生批改(大小、地位、排列形式等)或者有节点增删时,就能够从布局树中计算出对应节点的布局信息。在本系列后续文章中,咱们会另花篇幅介绍如何接入 Flex 布局引擎,并将其与 React、PixiJS 交融,敬请期待。
2.3 关键帧动画
说到属性与变换,天然绕不过关键帧动画这一概念。简略来说,关键帧动画就是在一组给定的「关键帧」之间(定义了工夫点与属性值),对属性值的变动做插值和过渡解决。比如说这样的一个动画:
- keyframe[0]: 0ms, x: 0, y: 120
- keyframe[1]: 20ms, x: 0, y: 40
- keyframe[2]: 40ms, x: 0, y: 120
就代表这个元素在 0~20ms 的区间内,y 值从 120 过渡到 40;在 20~40ms 的区间内,y 值从 40 过渡到 120。也就是说,这个元素原地蹦跶了一下。如果没有关键帧之间的插值和过渡,那么你看到的可能是这个元素忽然闪现到了下面,又忽然闪现了回来。而有了过渡帧,这个过程就是平滑的。
除了上文介绍的动效元素之外,关键帧动画也是罕用的游戏动效模式之一。因为关键帧动画是间接依据缓动函数批改某工夫点对应的属性值,能够说它有着所有动效中最好的性能(不过这也意味着它只能用于实现一些较为简单的动画)。在 Alice 中,咱们能够通过相似 CSS3 @keyframe
的模式定义关键帧动画:
const {play} = useAliceTransition(penguinRef, {
jump: {
0: {position: [0, 120],
rotation: 0,
tween: 'linear',
},
300: {position: [40, 40],
rotation: 180,
tween: 'linear',
},
600: {position: [80, 120],
rotation: 360
},
},
});
// 类比 CSS 代码:// @keyframes jump {// 0% { transform: rotate(0deg) translate(0, 120); }
// 50% {transform: rotate(180deg) translate(40, 40); }
// 100% {transform: rotate(360deg) translate(80, 120); }
// }
// .penguin {
// animation-name: jump;
// animation-timing-function: linear;
// animation-duration: 600ms;
// }
随后,在用户点击屏幕时触发播放定义好的关键帧动画即可:
onClick={() => play('jump')}
过渡动画也反对传入自定义参数,这里咱们定义企鹅跳一步和跳两步的动画办法:
const {play} = useAliceTransition(
penguinRef,
{
// 容许传入自定义参数,跳一步和跳两步的间隔、高度不同
jump: ({currentX, targetX, jumpHeight}) => ({
0: {position: [currentX, 0],
tween: 'linear',
},
300: {position: [(currentX + targetX) / 2, -jumpHeight],
tween: 'linear',
},
600: {position: [targetX, 0],
},
}),
// 能够同时播放多个动画,当跳两步的时候就让企鹅旋转跳跃闭着眼~
rotate: {0: { rotation: 0},
500: {rotation: 360},
},
},
(name, args) => {
// 动画完结后的回调,在这里能够判断企鹅有没有掉下去
if (name === 'jump') {onJumpEnd(args.jumpSteps);
}
}
);
// 跳一步(第二个参数是播放次数)play('jump', 1, {
jumpSteps: 1,
jumpHeight: 40,
currentX: penguin.position.x,
targetX: penguin.position.x + BOX_WIDTH,
});
// 跳两步
play('rotate');
play('jump', 1, {
jumpSteps: 2,
jumpHeight: 60,
currentX: penguin.position.x,
targetX: penguin.position.x + BOX_WIDTH * 2,
});
另外,除了当时定义好的关键帧,Alice 也反对通过 tween()
缓动函数间接静止指定的元素。比如说,当缓动的属性与工夫值常常变动时,应用缓动函数会更加灵便。上述两种办法次要是写法上的差别,在性能上是统一的。
这里咱们定义企鹅踩空后掉下去的动画办法:
// 企鹅掉出屏幕外,游戏完结
const fallToGround = useCallback((cb) => {
const penguin = penguinRef.current;
// 外部是 tween.js 的简略封装,在 500ms 内将 y 从原始地位静止到屏幕外
tween({y: penguin.position.y})
.to({y: 250}, 500)
.easing(TweenEasing.Cubic.In) // 加速度
.onUpdate((obj) => {penguin.position.y = obj.y;})
.onComplete(cb)
.start();}, []);
四、脚本与事件
游戏场景搭建得差不多了,当初咱们须要让角色动起来,这就波及到脚本与交互事件的解决。
脚本是游戏引擎中不可短少的一部分,它的主要用途是响应玩家的输出,并做出对应的解决,如管制场景中游戏对象的行为。或者通过注册特定的回调函数,来创立、更新、销毁元素等。比方玩家点击屏幕,企鹅须要向前跳动一格,这里的操作就是由脚本实现的。
在 Alice 中,咱们没有设计独立的「脚本」类型,而是将其融入了 JavaScript 与 React Hooks 中。比方咱们心愿在玩家点击左侧屏幕时,企鹅跳 1 步,点击右侧屏幕时,企鹅跳 2 步:
const Scene = () => {
// 游戏分数
const [score, setScore] = useState(0);
// 游戏后果
const [gameResult, setGameResult] = useState('ready');
// 保留地位状态
const [currentPos, setCurrentPos] = useState(0);
// 以后游戏的地图信息,true 示意有冰块,false 示意空气
const [map, setMap] = useState([]);
// 游戏从新开始后,重置分数和地位,生成新的随机地图
useEffect(() => {resetGame();
}, [resetGame]);
// 跳跃和旋转的动效
const {play} = useAliceTransition(/* ... */);
// 企鹅跳办法
const jump = useCallback((steps) => {if (gameResult !== 'playing') {return;}
const penguin = penguinRef.current;
if (!penguin) return;
// 上锁,避免连点
if (lockRef.current) return;
lockRef.current = true;
// 跳一步和跳两步的动画参数不一样
if (steps === 1) {play('jump', 1, { /* ... */});
} else {play('rotate');
play('jump', 1, { /* ... */});
}
}, [gameResult, play]);
return (
<React.Fragment>
{/* 游戏场景略 */}
<CameraView />
{/* 触控区域,一层通明的热区盖在最上层 */}
<View name="TouchPanel">
<View onClick={() => jump(1)} />
<View onClick={() => jump(2)} />
</View>
</React.Fragment>
);
};
Alice 反对 click
、pointerup
、pointerdown
、pointermove
等用户输出事件,事件的监听也和 React 一样简略。跳跃完结后,还须要判断以后游戏是否完结,也就是企鹅是不是掉下去了或者跳完了所有方块:
const onJumpEnd = useCallback((steps) => {
// 找到跳到的格子
const targetBlock = map[currentPos + steps];
setCurrentPos(s => s + steps);
lockRef.current = false;
// 是否跳齐全部的格子
if (currentPos + steps >= map.length) {setGameResult('win'); // 这会触发 DOM 层的弹窗展现
setScore(s => s + steps);
return;
}
// 掉下去了
if (!targetBlock) {fallToGround(() => {setGameResult('lose');
});
return;
}
// 没跳完也没掉下去,更新游戏分数
setScore(s => s + steps);
}, [map, currentPos, setCurrentPos, setScore, setGameResult, fallToGround]);
如果心愿在多个组件之间复用这些「游戏脚本」,同样能够遵循 The React Way —— 封装成 Hooks/HoC。在传统游戏引擎中,咱们应用可复用的脚本组件为实体增加交互等能力,在 Alice 中咱们则是通过 Hooks/HoC 为组件增加能力,他们底层的逻辑其实是相似的,composition over inheritance。
加上跳跃后的成果如下(GIF 动图):
五、调试
任何软件的开发都离不开调试,游戏天然也是一样。因为咱们的渲染层基于 PixiJS 与 WebGL,能够应用现有的工具构建咱们的调试流程。
- 浏览器 DevTools
- React DevTools
- pixi-inspector
- Spector.js
Alice 实现了 React custom renderer,因而能够间接应用 React DevTools 查看组件树、状态等调试信息。配合 pixi-inspector,能够很不便地查看以后场景下的所有底层元素和层级关系,疾速检查和批改物件的属性值:
Spector.js 能够剖析以后 canvas 在渲染一帧中发动的所有 WebGL 指令、用到的顶点着色器与片元着色器、纹理、Draw Call 的次数与调用参数等。WebGL 程序的渲染性能与 Draw Call 非亲非故,所以这个工具在做性能优化时十分好用:
结语
到这里,咱们的平台跳跃小游戏就根本成型了,是不是感觉和传统的 React 开发其实并没有特地大的区别呢?
而且因为咱们的渲染基于高性能的 canvas 与 WebGL,能够实现很多传统 DOM 难以实现的成果。比方将企鹅跳跃动画换成骨骼动画、纸娃娃换肤零碎、增加粒子成果、蒙皮和网格等等,甚至是渲染超级大的地图(demo 来自 gl-tiled):
同时,Alice 基于 React 也带来了这些益处:
- 团队学习成本低,上手无需学习新技术新语法
- 存量我的项目疾速接入、渐进式接入,试错成本低
- 复用已有的 H5 打包构建流程,无需额定流水线
- 可复用团队现有的 React 组件库等资产
- etc.
当然用 React 写游戏必定也不是所有货色都和以前一样,还是有一些须要额定留神的中央。比方 React 状态的应用,家喻户晓在 React 中状态的更新会导致组件重渲染,引发 Fiber Tree 的更新(render/commit phase),以及 side-effects 副作用的执行。然而在一个每秒都要更新 60 甚至更屡次的游戏中,为了缩小不必要的性能开销,过于频繁的组件重渲染是应该防止的。例如,更新频率高的属性能够思考应用 ref 保留,或者应用 zustand 等反对 Transient updates 的状态解决方案。
篇幅无限,这里其实还有很多相干的内容没有探讨,比方:性能优化、资源管理与预加载、场景分包、渲染性能优化、降级渲染,等等。这些问题咱们会尝试在本系列的后续文章中持续探讨。
总体来说,Alice 游戏引擎通过联合 React 理念与基于 WebGL 的高性能渲染管道,提供了丰盛的游戏开发元素、相熟的应用办法与心智模型,能够应答咱们在直播游戏化趋势中遇到的绝大部分中小型 H5 游戏开发需要。
目前,Alice 曾经在云音乐社交直播团队的多个我的项目中落地。在将来,咱们会继续摸索 React + WebGL 游戏开发的可能性,优化框架的功能性与易用性,心愿为 H5 游戏开发的场景提供新的思考与实际。
本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!