Hello, (strange) world
Google 首页是一个非常有趣的编码环境。它面临着许多具有挑战性的限制:特别关注速度和延迟时间,必须适应各种浏览器并在各种情况下正常运行,以及... 是的,惊喜和愉悦。
我指的是 Google 涂鸦,即偶尔会取代我们徽标的特殊插图。虽然我与钢笔和画笔之间的关系一直具有禁令书的独特风格,但我经常参与互动式内容的创作。
我编写的每个互动式涂鸦(Pac-Man、Jules Verne、World’s Fair)以及我协助编写的许多涂鸦,都既具有未来感,又具有时代感:前者是指利用前沿 Web 功能实现天方夜谭般的应用的绝佳机会,后者是指跨浏览器兼容性的务实主义。
我们从每款互动涂鸦中都能学到很多东西,最近的 Stanisław Lem 迷你游戏也不例外。这款游戏的 17,000 行 JavaScript 代码在涂鸦历史上首次尝试了许多新事物。今天,我想与大家分享这段代码,也许您会发现其中有趣的内容,或者指出我的错误,并对其进行一些讨论。
值得注意的是,Google 首页不适合展示技术演示。我们希望通过涂鸦来纪念特定人物和活动,并希望使用我们能调动的最优质的艺术和技术来实现这一点,但绝不会为了技术而纪念技术。这意味着,我们需要仔细研究广泛使用的 HTML5 的各个部分,并确定这些部分是否有助于我们改进涂鸦,而不分散注意力或盖过涂鸦。
接下来,我们来看看斯坦尼斯瓦夫·莱姆涂鸦中出现的一些现代 Web 技术,以及未出现的一些技术。
通过 DOM 和画布绘制图形
画布功能非常强大,非常适合我们在这张涂鸦中想要实现的效果。不过,我们关注的一些旧版浏览器不支持它。虽然我与开发出非常出色的 excanvas 的人员同事,但我还是决定选择其他方式。
我组合了一个图形引擎,用于提取名为“矩形”的图形基元,然后使用画布或 DOM(如果画布不可用)来渲染这些基元。
这种方法会带来一些有趣的挑战 - 例如,移动或更改 DOM 中的对象会立即产生后果,而对于画布,则有一个特定的时刻,所有内容都会同时绘制。(我决定只使用一个画布,并在每一帧中清除画布并从头开始绘制。一方面,移动的部分太多;另一方面,复杂性不足以分成多个重叠的画布并选择性地更新它们。)
遗憾的是,切换到画布并不像使用 drawImage()
镜像 CSS 背景那样简单:您会失去通过 DOM 组合内容时免费获得的许多功能,最重要的是,您无法使用 z 轴和鼠标事件进行分层。
我已经使用一个名为“平面”的概念提取了 z 轴索引。涂鸦定义了多个平面,从远处的天空到所有物体前面的鼠标指针,而涂鸦中的每个角色都必须决定自己属于哪个平面(可以使用 planeCorrection
在平面中进行微小的正负修正)。
通过 DOM 渲染时,平面会直接转换为 z 轴索引。但是,如果我们通过画布进行渲染,则需要先根据矩形的平面对矩形进行排序,然后再绘制它们。由于每次执行此操作的开销较大,因此系统仅在添加了某个 actor 或该 actor 移至其他平面时重新计算顺序。
对于鼠标事件,我也进行了抽象化处理…对于 DOM 和画布,我都使用了额外的完全透明的浮动 DOM 元素,它们的 z-index 较高,其功能仅为响应鼠标悬停/离开、点击和轻触。
我们希望通过这幅涂鸦尝试打破第四面墙。借助上述引擎,我们可以将基于画布的 Actor 与基于 DOM 的 Actor 结合使用。例如,结局中的爆炸效果既在画布中(适用于宇宙内对象),也在 DOM 中(适用于 Google 首页的其余部分)。这只鸟通常会像其他角色一样飞来飞去,并被锯齿状遮罩剪裁。但它决定在拍摄关卡期间避开麻烦,并坐在“我要试试运气”按钮上。实现方法是让鸟离开画布并成为 DOM 元素(稍后反之亦然),我希望对访问者完全透明。
帧速率
了解当前帧速率并在帧速率过慢(和过快!)时做出响应,是我们引擎的重要组成部分。由于浏览器不会报告帧速率,因此我们必须自行计算。
我开始使用 requestAnimationFrame,如果前者不可用,则回退到传统的 setTimeout
。requestAnimationFrame
在某些情况下会巧妙地节省 CPU(尽管我们自己也会执行一些节省 CPU 的操作,如下文所述),但它还能让我们获得比 setTimeout
更高的帧速率。
计算当前帧速率很简单,但可能会发生剧烈变化,例如,当其他应用占用计算机一段时间时,帧速率可能会快速下降。因此,我们仅在每 100 个物理滴答后计算“滚动”(平均)帧速率,并据此做出决策。
什么样的决策?
如果帧速率高于 60fps,我们会对其进行节流。目前,某些版本的 Firefox 中的
requestAnimationFrame
没有帧速率上限,因此没有必要浪费 CPU。请注意,我们实际上将帧速率上限设为 65fps,因为由于舍入误差,其他浏览器上的帧速率略高于 60fps,而我们不希望误加限流。如果帧速率低于 10fps,我们只会降低引擎速度,而不是丢帧。这是一个两败俱伤的方案,但我认为,与游戏速度变慢(但仍保持连贯性)相比,过度跳帧会更令人困惑。这还有一个不错的副作用:如果系统暂时运行缓慢,用户不会因为引擎拼命赶上而遇到奇怪的跳转。(我对《Pac-Man》的做法略有不同,但最小帧速率是更好的方法。)
最后,当帧速率变得危险地低时,我们可以考虑简化图形。我们不会对 Lem 涂鸦执行此操作,但会对鼠标指针执行此操作(详见下文)。不过,假设我们可以移除一些多余的动画,这样即使在速度较慢的计算机上,涂鸦也能流畅运行。
我们还提出了物理滴答和逻辑滴答的概念。前者来自 requestAnimationFrame
/setTimeout
。正常游戏中的比率为 1:1,但对于快进,我们只需在每一次物理滴答中添加更多逻辑滴答(最多为 1:5)。这样,我们就可以针对每个逻辑滴答执行所有必要的计算,但只指定最后一个逻辑滴答来更新屏幕上的内容。
基准比较
我们可以假设(事实上,在早期就假设了)只要可用,画布就比 DOM 更快。但这并不一定正确。在测试过程中,我们发现在移动 DOM 元素时,Mac 上的 Opera 10.0-10.1 和 Linux 上的 Firefox 的速度实际上更快。
在理想情况下,涂鸦会静默对比不同的图形技术,例如使用 style.left
和 style.top
移动 DOM 元素、在画布上绘制,甚至可能还会对比使用 CSS3 转换移动 DOM 元素
然后,切换到帧速率最高的选项。我开始为此编写代码,但发现至少我的基准测试方法非常不可靠,而且需要花费大量时间。我们在首页上没有那么多时间,因此非常注重速度,希望涂鸦立即显示,并在您点击或轻触后立即开始游戏。
归根结底,Web 开发有时归结为必须要做的事情。我回头看了看,确保没有人看着,然后直接在画布中对 Opera 10 和 Firefox 进行了硬编码。下辈子,我会以 <marquee>
代码的身份重返人间。
节省 CPU
您有没有那种朋友,会来您家,看《绝命毒师》的季终集,然后把剧情剧透给您,最后还从您的 DVR 中将其删除?您不想成为那种人,对吗?
是的,这是史上最糟糕的比喻。不过,我们也不希望涂鸦成为那种人。我们能进入用户的浏览器标签页是一种特权,如果占用大量 CPU 周期或干扰用户,我们就会成为不受欢迎的访客。因此,如果没有人玩涂鸦(没有点按、鼠标点击、鼠标移动或按键操作),我们希望它最终进入休眠状态。
时间
- 在首页上显示 18 秒后(街机游戏将此称为吸引模式)
- 如果标签页有焦点,则在 180 秒后
- 如果标签页没有焦点(例如,用户切换到了另一个窗口,但可能仍在非活动标签页中观看涂鸦),则在 30 秒后
- 如果标签页变为不可见(例如,用户切换到同一窗口中的另一个标签页),则立即停止
如何知道当前有焦点的标签页?我们会附加到 window.focus
和 window.blur
上。如何知道标签页可见?我们使用的是新的 Page Visibility API,并会对相应事件做出响应。
我们对上述超时时间的宽限度比平时更大。我将这些元素改编成了这个特定涂鸦,其中包含大量氛围动画(主要是天空和鸟)。理想情况下,超时应该取决于游戏内互动情况,例如,在着陆后,鸟类可以向涂鸦报告它现在可以休息了,但我最终没有实现这一点。
由于天空总是在运动,因此在进入休眠状态和唤醒时,涂鸦不会只是停止或开始 - 它会在暂停前减速,反之亦然,根据需要增加或减少每秒物理滴答数的逻辑滴答数。
转场效果、转换、事件
HTML 的强大之处之一在于,您可以自行改进它:如果 HTML 和 CSS 的常规组合无法满足需求,您可以使用 JavaScript 扩展它。很遗憾,这通常意味着需要从头开始。CSS3 转场效果非常棒,但您无法添加新的转场类型,也无法使用转场效果执行除设置元素样式之外的任何操作。再举一个例子:CSS3 转换非常适合 DOM,但当您转到画布时,就必须自行解决问题。
正是出于这些原因,Lem 涂鸦才有自己的转换和转换引擎。是的,我知道,2000 年代已经过去了,等等。我内置的功能远不如 CSS3 强大,但无论引擎执行什么操作,都能始终如一,并让我们拥有更大的控制力。
我先构建了一个简单的操作(事件)系统,即一个时间轴,它可以在未来触发事件,而无需使用 setTimeout
,因为在任何给定时间点,涂鸦时间都可能会与实际时间脱节,因为它可能会变快(快进)、变慢(帧速率较低或进入休眠状态以节省 CPU)或完全停止(等待图片加载完毕)。
转场只是另一种类型的操作。除了基本移动和旋转之外,我们还支持相对移动(例如将某个对象向右移动 10 像素)、抖动等自定义操作,以及关键帧图片动画。
我提到了旋转,这些也是手动完成的:我们为需要旋转的对象提供了各种角度的 Sprite。主要原因是,CSS3 和画布旋转都会引入我们无法接受的视觉伪影,此外,这些伪影因平台而异。
鉴于某些旋转的对象连接到其他旋转的对象(例如,机器人的手连接到下臂,而下臂本身连接到旋转的上臂),这意味着我还需要创建一个以轴心形式的“穷人”转换起点。
所有这些工作都需要花费大量精力,最终涵盖 HTML5 已经涵盖的领域。但有时,原生支持还不够好,这时就需要重新发明轮子。
处理图片和精灵
引擎不仅用于运行涂鸦,还用于处理涂鸦。我上面分享了一些调试参数:您可以在 engine.readDebugParams
中找到其余参数。
贴图是一种众所周知的技术,我们也将其用于涂鸦。这让我们能够节省字节并缩短加载时间,还能简化预加载。不过,这也增加了开发难度,因为对图像进行的每项更改都需要重新提取精灵(虽然大部分是自动完成的,但仍然很麻烦)。因此,该引擎支持在开发时在原始图片上运行,以及通过 engine.useSprites
在生产环境中运行精灵 - 这两者都包含在源代码中。

我们还支持边绘制边预加载图片,并在图片未及时加载时暂停涂鸦,同时还会显示一个模拟的进度条!(伪造,因为遗憾的是,即使 HTML5 也无法告诉我们图片文件已加载了多少。)

对于某些场景,我们使用多个精灵,目的并非是使用并行连接来加快加载速度,而是因为 iOS 上的图片限制为 3/500 万像素。
在所有这些中,HTML5 的地位如何?上面没有太多这方面的内容,但我为创建/剪裁精灵图所编写的工具全部采用了新的 Web 技术:画布、blob、a[download]。HTML 的一个令人兴奋之处在于,它会逐渐取代之前必须在浏览器之外完成的工作;我们只需优化 PNG 文件,
在游戏之间保存状态
Lem 笔下的世界总是宏大、生动而真实。他的小说通常在开头就没有过多说明,第一页就开始讲述故事,读者必须自行寻找线索。
《赛博罗迪亚》也不例外,我们希望在涂鸦中重现这种感觉。首先,我们会尽量避免过度解释故事。另一大部分是随机化,我们认为这符合该书中宇宙的机械性质;我们有许多用于处理随机性的辅助函数,这些函数在很多地方都用到了。
我们还希望通过其他方式提高游戏的可重玩性。为此,我们需要知道涂鸦之前完成了多少次。过去,解决此问题的正确技术方案是 Cookie,但 Cookie 不适用于 Google 首页,因为每个 Cookie 都会增加每个网页的载荷,而我们非常重视速度和延迟时间。
幸运的是,HTML5 为我们提供了简单易用的 Web Storage,让我们能够保存和调用一般播放次数以及用户上次播放的场景,比 Cookie 更方便。
我们会如何使用这些信息?
- 我们会显示一个快进按钮,让用户可以快速跳过之前看过的过场动画
- 我们会在决赛期间显示不同的 N 项
- 我们略微提高了射击关卡的难度
- 在您第三次及之后的游戏过程中,我们会显示来自其他故事的彩蛋概率龙
有多个调试参数可用于控制此行为:
?doodle-debug&doodle-first-run
- 假设这是首次运行?doodle-debug&doodle-second-run
- 假装是第二次运行?doodle-debug&doodle-old-run
- 假设是旧运行
触摸设备
我们希望涂鸦在触控设备上能给人宾至如归的感觉。最先进的触控设备足够强大,可以让涂鸦顺畅运行,而且通过点按体验游戏比点击更有趣。
我们需要对用户体验做出一些预先更改。最初,只有鼠标指针会显示游戏正在播放过场动画/非互动部分。后来,我们在右下角添加了一个小指示器,这样就不必单独依赖鼠标指针(因为触控设备上没有鼠标指针)。
正常 | 忙碌 | 可点击 | 已点击 | |
---|---|---|---|---|
正在进行 | ![]() |
![]() |
![]() |
![]() |
决赛 | ![]() |
![]() |
![]() |
![]() |
大多数功能都可以直接使用。不过,对触控体验进行的临时快速易用性测试发现了两个问题:某些目标太难按,而且由于我们只是替换了鼠标点击事件,因此系统会忽略快速点按。
在这种情况下,单独创建可点击的透明 DOM 元素非常有用,因为我可以独立于视觉效果来调整它们的大小。我为触摸设备引入了额外的 15 像素内边距,并在每次创建可点击元素时都使用它。(我还为鼠标环境添加了 5 像素的内边距,只为让 Fitts 先生高兴。)
至于另一个问题,我只是确保附加并测试了适当的触摸开始和结束处理脚本,而不是依赖于鼠标点击。
我们还使用了更现代的样式属性,移除了 WebKit 浏览器默认添加的一些触控功能(点按突出显示、点按宣传信息)。
如何检测运行涂鸦的给定设备是否支持触控?以懒惰的方式。我们并非预先推断出这一点,而是在收到第一个触摸开始事件后,利用我们的综合 IQ 推断出设备支持触摸。
自定义鼠标指针
但并非所有内容都是触控的。我们的一个指导原则是,尽可能在涂鸦世界中添加更多内容。小侧边栏界面(快进、问号)、提示,甚至鼠标指针。
如何自定义鼠标指针?某些浏览器允许通过链接到自定义图片文件来更改鼠标光标。不过,这种方式的支持不太完善,而且也有些限制。
如果不是这样,那是什么?那么,为什么不将鼠标指针作为涂鸦中的另一个角色来处理呢?这种方法虽然可行,但存在一些限制,主要包括:
- 您需要能够移除原生鼠标指针
- 您需要非常熟练地让鼠标指针与“真实”指针保持同步
前者比较棘手。CSS3 允许使用 cursor: none
,但某些浏览器也不支持它。我们需要采取一些极端措施:使用空 .cur
文件作为后备,为某些浏览器指定具体行为,甚至对其他浏览器进行硬编码,以便在任何情况下都获得一致的体验。
另一个问题看起来相对简单,但由于鼠标指针只是涂鸦世界的一部分,因此它也会继承所有问题。最大的难点在于,如果涂鸦的帧速率较低,鼠标指针的帧速率也会较低,这会产生严重后果,因为鼠标指针是手的自然延伸,无论如何都需要具有响应能力。(过去使用过 Commodore Amiga 的用户此时正在使劲点头。)
解决此问题的一种稍微复杂的解决方案是将鼠标指针与常规更新循环分离。我们就是这样做的,在一个我无需睡觉的平行宇宙中。这个问题有更简单的解决方案吗? 如果滚动帧速率低于 20fps,只需改用原生鼠标指针即可。(这时,滚动帧速率就派上用场了。如果我们对当前帧速率做出响应,而帧速率恰好在 20fps 左右振荡,用户会看到自定义鼠标指针一直在隐藏和显示。)这让我们来看看:
帧速率范围 | 行为 |
---|---|
大于 10 fps | 放慢游戏速度,以免丢失更多帧。 |
10-20 帧/秒 | 使用原生鼠标指针,而不是自定义指针。 |
20-60 fps | 正常操作。 |
>60fps | 节流,以使帧速率不超过此值。 |
哦,我们的鼠标指针在 Mac 上是深色,但在 PC 上是白色。为什么呢?因为即使在虚构的宇宙中,平台之争也需要燃料。
总结
这并不是一个完美的引擎,但它也不会试图成为一个完美的引擎。它是与 Lem 涂鸦一起开发的,与之密切相关。没关系。正如 Don Knuth 的著名名言所说:“过早优化是万恶之源”,我不认为先单独编写引擎,然后再应用它是明智之举。实践有助于形成理论,理论也有助于指导实践。在我的情况下,代码被丢弃了,一些部分被反复重写,许多常见的部分是在事后发现的,而不是事前发现的。但最终,我们还是实现了自己的目标,以我们能想到的最佳方式颂扬了 Stanisław Lem 的职业生涯和 Daniel Mróz 的画作。
希望上述内容能让您了解我们需要做出的一些设计选择和权衡,以及我们如何在特定的实际场景中使用 HTML5。现在,请试用源代码,并告诉我们您的想法。
这是我自己做的 - 下面的倒计时在过去几天内一直在显示,从 2011 年 11 月 23 日凌晨开始,这是第一个看到 Lem 涂鸦的时区。这可能是一个愚蠢的做法,但就像涂鸦一样,看似不重要的事情有时具有更深层的意义 - 这个计数器确实是对引擎的很好“压力测试”。

这是一种看待 Google 涂鸦生命周期的方式:数月的工作、数周的测试、48 小时的打磨,只为让人们玩上 5 分钟。成千上万的 JavaScript 代码行都希望这 5 分钟能发挥作用。祝您愉快。