不用引擎、不用框架做一款 HTML5 游戏
· 阅读约 3 分钟
我们游戏页面上的前九款游戏里,有六款没有任何依赖:既不用引擎和框架,也不用构建期的库。最小的那款 Reefhold 在磁盘上只有 63 KB,这还是整个游戏连美术在内的大小。最新的三款有意打破了这条规矩,而两个方向的理由都值得说一说。
「不用引擎」到底是什么意思
实际上就是三样东西:一个 <canvas> 元素,一个由 requestAnimationFrame 驱动的循环,以及屏幕上的每一个形状都由代码画出来,而不是作为图片加载进来。没有场景图,没有实体组件系统,没有素材管线。一个精灵就是一个函数,接收一个上下文和几个数字,调用几次 fillRect。
听起来很原始,因为它确实原始。但这也是为什么你还没读完 Reefhold 的标题,它就已经加载好了。
最后得自己写的三样东西
引擎会免费把这些递给你。没有引擎,它们就是你要写的头三个文件,而且每款游戏里都是同样的三个文件:
- 固定时间步长的循环。
requestAnimationFrame给你的是挂钟时间的增量,而不是一个 tick。如果直接按这个增量去移动东西,游戏在 60 Hz 的笔记本和 144 Hz 的显示器上表现就会不一样,测试时手感正好的物理,到了外面就变得古怪。解决办法是把真实时间累积起来,按固定的切片推进模拟,渲染时在最近两个状态之间插值。 - 触屏和键盘一致的输入。 一半的流量来自手机。一个只读
keydown的游戏,对这些人来说根本没法玩;而一个处处给触屏写特例的游戏,代码量会翻倍。诀窍是把两者统一成一个小小的「意图」对象(一个方向,加上一组按住的动作),然后让游戏的其余部分只读这个对象。 - 能扛住自动播放策略的音频。 在用户和页面交互之前,浏览器不会让任何声音播放。如果你在加载时就创建
AudioContext,它一开始就是挂起状态,而且再也恢复不过来。改成在第一次指针或按键事件时创建,之后的每个声音都以此为前提。
这些加起来大约 150 行,在用到它们的游戏之间原封不动地共用。
换来的东西
- 一个门户永远不会质疑的包。 人工审核的门户有硬性的体积上限,超了就拒,没有申诉的余地。一个 63 KB 的游戏根本不用谈。
- 瞬间完成的首次加载,在移动数据下,这就是留住一个玩家和让他跳出之间的差别。
- 没有需要授权的东西。 当有人想授权其中一款游戏时,没有第三方许可证要念给他们听,没有署名文件,也没有「嗯,它依赖 Phaser,Phaser 是 MIT 协议,不过……」这种话。
界线在哪里
Scrapforge、ScaleSling 和 Husktide 是基于 Phaser 做的,对这三款来说这都是正确的选择。Scrapforge 和 ScaleSling 需要一个真正的物理求解器,而 2D 约束求解器不值得再糟糕地重写一遍。Husktide 要在屏幕上同时放几百个实体,靠的是纹理图集和一个调过的渲染器。
事后回头看,规律很清楚:纯手写的做法适用于以摆放和时机为核心的游戏,比如大多数解谜游戏、大多数放置游戏、大多数街机计分游戏。一旦你需要大量相互作用的物体,或者一个带棱角的物理模型,它就撑不下去了。到了那一步,引擎反倒不算额外开销了:用它做出来的包,会比手搓版本最终的体积更小。
如果你想看实际数字,每款游戏的体积都公开在授权页面上。
