案例研究 - 制作史坦尼斯劳·莱姆的 Google 涂鸦

Marcin Wichary
Marcin Wichary

你好,(奇怪的)世界

Google 首页是一个迷人的编码环境。这带来了许多具有挑战性的限制:特别注重速度和延迟,必须迎合各种浏览器,能够在各种情况下工作,而且...没错,令人惊讶和令人愉悦。

我说的是 Google 涂鸦,这种特别的插图偶尔会取代我们的徽标。 虽然长久以来,我与画笔和画笔的这种关系具有独特的限制顺序,但我经常参与互动性调整。

我编写过的每幅交互式涂鸦(吃豆人朱尔斯·凡尔纳世界博览会)以及我帮助过的许多涂鸦,都是同等的未来主义和过时的涂鸦:空前地应用先进网络功能的绝佳机会...以及跨浏览器兼容性的“务实”理念。

我们从每幅交互式涂鸦中学到很多东西,近期推出的 Stanisław Lem 迷你游戏也不例外,它的 17,000 行 JavaScript 代码在涂鸦史上首次尝试了许多东西。今天,我想与您分享该代码,也许您会找到一些有趣的内容,或者指出我的错误,并简单介绍一下。

查看史坦尼斯劳·莱姆的涂鸦代码 »

值得注意的是,Google 的首页不是技术演示的场所。我们希望通过涂鸦来庆祝特定的人物和事件,而我们希望利用最出色的艺术和我们可以调用的技术来庆祝这些事件,但绝不会为了技术而赞美技术。这意味着,您需要仔细查看广为人知的 HTML5 版本,看看它能否帮助我们改善涂鸦,同时又不会干扰它或覆盖它。

接下来,我们来了解一些在 Stanisław Lem 涂鸦中发现它们的现代网络技术,还有一些却没有。

通过 DOM 和画布绘制图形

Canvas 功能强大,完全符合我们想要在这幅涂鸦中完成的事项。不过,我们关注的一些旧版浏览器并不支持它。虽然我正与一位组建优秀 Excanva 的人共用一个办公室,但我还是决定另选一种方式。

我组建了一个图形引擎,将称为“矩形”的图形基元抽象出来,然后使用任一画布、DOM(如果画布不可用)进行渲染。

这种方法带来了一些有趣的挑战,例如,在 DOM 中移动或更改对象会产生直接影响,而对于画布,则有一个特定时刻,即在同一时间绘制所有内容。(我决定只使用一张画布,然后清除它,从零开始从每一帧绘制。从字面上看,一方面移动部分太多,另一方面又不够复杂,不足以保证拆分成多个重叠的画布并进行选择性更新。)

遗憾的是,切换到画布并不像只是使用 drawImage() 镜像 CSS 背景那么简单:通过 DOM 组合在一起时,您会失去很多免费功能,最重要的是,使用 Z-index 和鼠标事件进行分层。

我已经通过一个称为“平面”的概念来抽象化 Z-index。该涂鸦定义了多个平面(从远处的天空到位于所有内容前面的鼠标指针),并且涂鸦中的每个演员都必须确定它属于哪个平面(平面内的小加号/减号可通过添加 planeCorrection 来实现)。

通过 DOM 渲染时,平面会直接转换为 Z-index。但是,如果我们通过画布进行渲染,则需要在绘制前根据矩形的平面对其进行排序。由于每次执行此操作的成本很高,因此仅当添加演员或演员移动到另一个平面时才会重新计算顺序。

对于鼠标事件,我也提取了出来...在某种程度上。对于 DOM 和画布,我使用了其他具有高 Z-index 的完全透明的浮动 DOM 元素,其功能仅用于响应鼠标移开/鼠标移开、点击和点按操作。

我们想在这幅涂鸦中做的一件事 就是打破第四面墙。利用上述引擎,我们可以将基于画布的 actor 和基于 DOM 的 actor 结合在一起。例如,结尾处的爆炸画面既存在于画布上(对于宇宙中),则是呈现在 DOM 中(对于 Google 首页的其余部分)。与任何其他演员一样,这只鸟通常在周围飞行,并被我们锯齿状的面具夹着,它决定在射击级别避免陷入麻烦,并坐在“手气不错”按钮上。这种实现方式是让小鸟离开画布并成为 DOM 元素(之后相反),我们希望这对访问者而言是完全透明的。

帧速率

了解当前的帧速率并对游戏速度过慢(和过快!)做出反应是引擎的一个重要方面。由于浏览器不会报告帧速率,因此我们必须自行进行计算。

我一开始使用的是 requestAnimationFrame,但当旧式 setTimeout 不可用时,我回退到旧版本。requestAnimationFrame 在某些情况下可以巧妙地节省 CPU(虽然我们自己就是这样做的,如下文所述),但它也让我们能够获得高于 setTimeout 的帧速率。

计算当前帧速率很简单,但可能会发生显著变化。例如,当其他应用占用计算机一段时间时,帧速率可能会快速下降。因此,我们仅计算每 100 个物理刻度点的“滚动”(平均)帧速率,并据此做出决策。

什么样的决策?

  • 如果帧速率高于 60fps,我们会对其加以限制。目前,Firefox 的某些版本上的 requestAnimationFrame 在帧速率方面没有上限,因此没有必要浪费 CPU。请注意,实际上我们所设的上限是 65fps,因为舍入误差会让其他浏览器上的帧速率略高于 60fps,我们不希望误开始限制这种限制。

  • 如果帧速率低于 10fps,我们只需减慢引擎的速度,而不是丢帧。这是一种“输赢”的主张,但我觉得,过度跳帧会比单调慢(但仍然有连贯性)游戏更令人困惑。这还有另外一个好处,那就是,如果系统暂时变慢,用户不会遇到奇怪的向前跳跃的情况,因为引擎会急切追赶。(我针对吃豆人采用的方法略有不同,但最小帧速率是更好的方法。)

  • 最后,我们可以考虑在帧速率极低时简化图形。除了鼠标指针(详见下文)之外,我们不会为 Lem 涂鸦执行相关操作,但假设我们会丢失一些无关动画,以便让涂鸦即使在运行速度较慢的计算机上也能流畅运行。

我们还有一个物理刻度线和逻辑刻度线的概念。前者来自 requestAnimationFrame/setTimeout。在正常游戏中,宽高比为 1:1,但为了实现快进,我们只需为每个物理 tick(最多 1:5)添加更多逻辑 tick。这样一来,我们可以为每个逻辑 tick 执行所有必要的计算,但只将最后一项逻辑指定为更新屏幕上的内容的对象。

基准化分析

我们可以(当然在早些时候)做出假设,即只要可用,画布就会比 DOM 更快。但情况并非总是如此。在测试过程中,我们发现 Mac 上的 Opera 10.0-10.1 以及 Linux 上的 Firefox 在移动 DOM 元素时实际上速度更快。

在理想情况下,涂鸦会静默地对不同的图形技术进行基准测试:使用 style.leftstyle.top 移动 DOM 元素、在画布上绘制,甚至可能使用 CSS3 转换来移动 DOM 元素

然后切换到帧速率最高的选项。我开始为此编写代码,但发现至少我的基准化分析方法不太可靠,并且需要大量的时间。首页上没有显示时间,我们非常关心速度,我们希望涂鸦立即显示,点击或点按就开始游戏过程。

最终,Web 开发有时归结为必须执行需要执行的操作。我进行了深入调查,确保没有人在看,然后我基于画布对 Opera 10 和 Firefox 进行了硬编码。在下一生中,我将作为 <marquee> 标记返回。

节省 CPU

你知道有一位朋友来到你家里看了《绝命毒师》 剧季最后一集,搞坏了它并将其从你的 DVR 中删除了?你不会想当作那个家伙,对吧?

没错,这是最糟糕的类比。但我们也不希望我们画的是那个人 - 允许访问某人的浏览器标签页就是一种特权,占用 CPU 周期或干扰用户都会使我们成为令人不快的访客。因此,如果没有任何人在玩涂鸦(没有点按、鼠标点击、鼠标移动或按键操作),我们希望它最终进入休眠状态。

时间

  • 18 秒后在首页上展示(街机游戏将这种模式称为吸引模式
  • 180 秒后(如果标签页获得焦点)
  • 如果标签页没有焦点(例如,用户切换到了另一个窗口,但可能仍在闲置标签页中观看涂鸦),30 秒后才显示该涂鸦
  • 如果标签页不可见,则立即处理(例如,用户在同一窗口中切换到另一个标签页,如果无法看到,就没有必要浪费周期)

如何判断标签页当前是否已获得焦点?我们将自己附加到 window.focuswindow.blur,我们如何知道标签页是否可见?我们将使用新的 Page Visibility API 并响应相应的事件。

上述超时对我们来说比平常更宽松。我将它们调整成了这个具有很多氛围动画(主要是天空和鸟类)的涂鸦。理想情况下,超时将受到游戏内互动的控制 - 例如,在降落后,小鸟可以向涂鸦反馈说它现在可以睡觉了,但我最后没有实现这一点。

由于天空始终是运动的,因此在入睡和唤醒涂鸦时不会停止或开始,它会在暂停之前减慢,反之亦然。

过渡、转换和事件

您可以自行改进 HTML 的强大之处之一就是:如果常规 HTML 和 CSS 产品组合中存在不足之处,您可整理 JavaScript 以对其进行扩展。遗憾的是,这往往意味着必须从头开始。CSS3 过渡效果非常好,但您不能添加新的过渡类型,也不能使用过渡效果来执行除元素样式设置之外的其他任何操作。另一个例子:CSS3 转换对于 DOM 来说很不错,但当移到画布上时,就会忽然独立开来。

这些问题以及更多的问题让 Lem 涂鸦有自己的过渡和转换引擎。没错,我知道,2000 年代有人称呼我,等等。我内置的功能远远不如 CSS3 那么强大,但无论引擎如何采用 CSS,它都能始终如一地运作,并赋予我们更多控制力。

我从一个简单的操作(事件)系统开始,这个时间轴无需使用 setTimeout 即可触发未来事件的时间轴,因为在任意给定时间点,涂鸦时间可能会与实际时间脱离,因为它变快(快进)、变慢(低帧速率或进入休眠状态以节省 CPU)或完全停止(等待图像完成加载)。

过渡只是另一种类型的操作。除了基本的移动和旋转之外,我们还支持相对移动(例如将某个对象向右移动 10 个像素)、自定义操作(例如抖动)以及为图片动画设置关键帧。

我提到了旋转,这些也是手动完成的:我们为需要旋转的对象设置了不同角度的精灵。主要原因是,CSS3 和画布旋转都会引入我们无法接受的视觉伪影,而且这些伪影因平台而异。

鉴于一些旋转的物体会连接到其他旋转的物体(例如机器人的手连接到下臂,而下臂本身则连接到旋转的上臂),这意味着我还需要以轴心的形式创建一个穷人的转换原点。

所有这一切都是大量工作,最终涵盖 HTML5 已经涵盖的范围;但有时,原生支持还不够好,现在正是重新发明轮子的时候。

处理图片和精灵

引擎不仅能用来运行涂鸦,也要用来处理涂鸦。我在上面分享了一些调试参数:您可以在 engine.readDebugParams 中找到其余参数。

拼图是一项我们在涂鸦中运用的广为人知的技术。它可以节省字节并缩短加载时间,并且使预加载变得更简单。 但是,这也增加了开发难度,对图像的每次更改都需要重新拼合(在很大程度上是自动化的,但仍然很麻烦)。因此,该引擎支持通过 engine.useSprites 在原始映像上运行(用于开发)和用于生产环境的精灵 - 两者都包含在源代码中。

吃豆人涂鸦
吃豆人涂鸦使用的精灵。

我们还支持在处理过程中预加载图片,如果图片未及时加载,则暂停涂鸦 - 还有一个人造的进度条!(错误,因为遗憾的是,即使是 HTML5 也无法告诉我们已加载图片文件的多少内容。)

加载图形的屏幕截图,其中包含进度条。
加载图形的屏幕截图,其中包含绑定的进度条。

对于某些场景,我们会使用多个精灵来提升使用并行连接的速度,这仅仅是因为 iOS 上图片存在 300 万像素/500 万像素限制

HTML5 在其中发挥了什么作用?上文没有详细介绍,但我编写的用于拼合/剪裁的工具是全新的 Web 技术:画布、blob[下载]。 HTML 令人兴奋的一点是,它慢慢包含以前必须在浏览器之外完成的功能;我们唯一需要做的就是优化 PNG 文件。

在游戏间隙保存状态

Lem 的世界总是给人一种广大、鲜活和逼真的感觉。他的故事一开始通常没有过多的解释,第一页从媒体报道开始,读者必须自行寻找方向。

Cyberiad 也不例外,我们希望在涂鸦中再现这种感觉。我们首先要尽量不过度解释这个故事。另一大部分是随机化,我们认为这适合图书宇宙的机制;我们有一些处理随机性的辅助函数,我们在许多很多地方都会用到这种函数。

我们还希望通过其他方式提高可玩性。为此,我们需要知道该涂鸦之前完成的次数。这个历史上正确的技术解决方案是 Cookie,但这并不适用于 Google 首页。每个 Cookie 都会增加每个网页的载荷,而且,我们非常关注速度和延迟时间。

幸运的是,HTML5 为我们提供了使用非常简单的网络存储数据,让我们得以保存和找回一般播放次数以及用户播放的最后一个场景,其优势远超 Cookie 所能提供。

我们如何使用此信息?

  • 我们会显示一个快进按钮,以便用户快速浏览之前看过的过场动画
  • 我们会在最后的视频中
  • 我们稍微提高了射击关卡的难度
  • 我们会在你的第三场和后续游戏中展示一个来自另一个故事的复活节彩蛋小龙

有很多调试参数可以控制此功能:

  • ?doodle-debug&doodle-first-run - 模拟首次运行
  • ?doodle-debug&doodle-second-run - 假装它是第二次运行
  • ?doodle-debug&doodle-old-run - 模拟旧跑步

触摸设备

我们希望涂鸦在触控设备上也能让用户感到家一般,最现代的涂鸦效果足够强大,因此涂鸦运行良好,通过点按来体验游戏要比点击游戏更有趣。

需要预先对用户体验做出一些更改。最初,鼠标指针是唯一能够传达过场/非互动部分发生的位置。后来,我们在右下角添加了一个小指示器,因此不必只依赖于鼠标指针(假设触摸设备上不存在鼠标指针)。

常规 旺季 可点击 点击了
进行中
进行中的常规指针
正在进行工作的忙碌指针
正在处理中的可点击指针
正在进行工作的点击的指针
最后
最终正常指针v
最终忙指针
最终可点击指针
最终点击指针
开发期间的鼠标指针,以及最终等效项。

大多数工具都可以开箱即用。不过,针对触摸体验的快速即兴易用性测试显示了两个问题:某些目标太难按,而快速点按因为我们刚刚覆盖了鼠标点击事件而被忽略。

单独设置可点击的透明 DOM 元素有很大帮助,因为我可以单独调整这些元素的大小,而不考虑视觉元素。我为触摸设备引入了额外的 15 像素的内边距,并在创建可点击元素时使用它。(为了让 Fitts 先生高兴,我还为鼠标环境添加了 5 像素的内边距。)

对于另一个问题,我只是确保附加并测试正确的触摸开始和结束处理程序,而不是依赖于鼠标点击。

我们还使用更现代的样式属性,移除 WebKit 浏览器默认添加的一些触控功能(点按突出显示、点按标注)。

我们如何检测运行涂鸦的给定设备是否支持触控?懒洋洋的。我们没有预先确定,而是在获得第一个触摸开始事件后,利用综合 IQ 推断出设备支持触摸。

自定义鼠标指针

不过,并非所有技术都是基于触摸的。我们的指导原则之一是在涂鸦的宇宙中尽可能多地放置各种物品。小边栏界面(快进、问号)、提示,甚至是鼠标指针。

如何自定义鼠标指针?某些浏览器允许通过链接到定制图片文件来更改鼠标光标。不过,这种方式得不到很好,并且存在一定的限制。

如果不是这样,那该怎么办?嗯,为什么不让一个鼠标指针只是 涂鸦中的另一位演员呢?这种方法可以运行,但需要注意一些事项,主要是:

  • 您需要能够移除本地鼠标指针
  • 您需要非常擅长将鼠标指针与“真实”指针同步

前者比较复杂。CSS3 允许使用 cursor: none,但某些浏览器也不支持此功能。我们需要采用一些机制:使用空的 .cur 文件作为后备方案,为某些浏览器指定具体行为,甚至对其他浏览器进行硬编码,无论浏览器如何。

另一个元素表面上相对无关紧要,但鼠标指针只是涂鸦宇宙的另一部分,它会继承它的所有问题。最大的一个?如果涂鸦的帧速率较低,那么鼠标指针的帧速率也会较低,这会产生严重的后果,因为鼠标指针是手的自然延伸,无论在什么情况下都需要感觉响应迅速。(过去使用过 Commodore Amiga 的用户现在正积极地点头。)

要解决该问题,有一个比较复杂的解决方案是将鼠标指针与常规更新循环分离。我们就是这样做的,在另一个我不需要睡觉的宇宙中有没有一种更简单的解决方案可以解决这一问题? 如果滚动帧速率降到 20fps 以下,只需还原为原生鼠标指针即可。(这时滚动帧速率就派上用场了如果我们对当前帧速率做出响应,并且恰好在 20fps 左右振动,用户会看到自定义鼠标指针始终处于隐藏和显示状态。)这让我们可以:

帧速率范围 行为
>10fps 放慢游戏速度,以免丢失更多帧。
10-20fps 使用原生鼠标指针,而不是自定义鼠标指针。
20–60fps 正常运行。
>60fps 限制帧速率,使帧速率不超过此值。
与帧速率相关的行为的摘要。

哦,我们的鼠标指针在 Mac 上是暗的,但在 PC 上是白色的。原因何在?因为即使是在虚构的宇宙中,平台战争也需要燃料。

总结

虽然这不是完美的引擎,但并没有试图成为完美引擎。它是与 Lem 涂鸦一起创作的,而且非常具体。这没关系。正如唐·克努斯 (Don Knuth) 的名言所说,“提前优化是一切邪恶的根源”,我相信先编写引擎没有意义,仅在以后应用引擎没有意义 - 实践会让理论提供信息,就像理论指导实践一样。就我而言,代码被舍弃,几个部分反复重写,许多常见部分在帖子中被注意到,而不是以前的事实。但最终,我们成功实现了自己的梦想:以我们能想到的最佳方式庆祝斯坦尼斯洛·莱姆的职业生涯和丹尼尔·莫罗兹的绘画作品。

希望以上内容能让您了解我们需要进行的一些设计选择和权衡,以及我们如何在实际的现实场景中使用 HTML5。现在,我们来试用一下源代码,然后告诉我们您的想法。

这是我自己做的 – 这幅画是在最后几天发布的,当时正倒计时到俄罗斯 2011 年 11 月 23 日凌晨,这是第一个看到莱姆涂鸦的时区。或许这是一款蠢货,但就像涂鸦一样,看起来无关紧要的东西有时还有更深的意义。这个计数器实际上是引擎的好“压力测试”。

Lem 涂鸦宇宙倒计时时钟屏幕截图。
Lem 涂鸦宇宙倒计时时钟屏幕截图。

这就是我们看 Google 涂鸦生命周期的一种方式:数月的工作、数周的测试、48 小时的烘焙,而这一切都是为了玩家玩五分钟而已。这数千行 JavaScript 代码中的每一个都希望这 5 分钟能很好地花时间。祝您观看愉快。