事例紹介 - Stanisław Lem Google Doodle の作成

Hello, (strange) world

Google のホームページは、コードを記述するのに魅力的な環境です。速度とレイテンシに特に重点を置き、さまざまなブラウザに対応し、さまざまな状況で動作する必要があるなど、多くの制約があります。そして、驚きと喜びも提供する必要があります。

Google Doodle とは、Google のロゴに代わる特別なイラストです。長い間、ペンとブラシとの関係は、拘束命令のような独特の味わいがありましたが、私はインタラクティブな作品にもよく参加しています。

私がコードを書いたインタラクティブな Doodle(パックマンジュール ヴェルヌ万国博覧会)と、私がサポートした多くの Doodle は、未来的でありながら時代遅れでもあるものでした。最先端のウェブ機能を空想に満ちたアプリケーションに活用する絶好の機会であり、また、クロスブラウザ サポートの現実的な実用性も兼ね備えていました。

Google は、各インタラクティブな Doodle から多くのことを学んでいます。最近の Stanisław Lem ミニゲームも例外ではなく、17,000 行の JavaScript コードで、Doodle の歴史上初めて多くのことを試しました。本日は、そのコードを皆さんと共有し、興味深い点や間違いを見つけていただくとともに、そのコードについて少しお話ししたいと思います。

スタニスワフ レムのドゥードルのコードを表示»

なお、Google のホームページは技術デモの場所ではありません。Google は、特定の人物や出来事を祝うために、最高の芸術と最高のテクノロジーを使って Google ドゥードルを作成しています。ただし、テクノロジー自体を祝うことはありません。つまり、広く理解されている HTML5 のどの部分が利用できるか、そして、その部分がドゥードルの邪魔になったり、ドゥードルを覆い隠したりすることなく、ドゥードルをより良くするのに役立つかどうかを慎重に検討します。

では、スタニスワフ レムの記念イラストに登場した、または登場しなかった、最新のウェブ技術をいくつか見てみましょう。

DOM とキャンバスによるグラフィック

キャンバスは強力で、このドゥードルで実現したいことを実現するために作られました。ただし、サポート対象の古いブラウザの中には、これをサポートしていないものもあります。優れた excanvas を作成した人物と、私は同じオフィスで働いていますが、別の方法を選択することにしました。

「長方形」と呼ばれるグラフィック プリミティブを抽象化し、キャンバス(キャンバスを使用できない場合は DOM)を使用してレンダリングするグラフィック エンジンを作成しました。

このアプローチには興味深い課題があります。たとえば、DOM 内のオブジェクトを移動または変更するとすぐに結果が反映されますが、キャンバスの場合は、すべてが同時に描画される特定のタイミングがあります。(私はキャンバスを 1 つだけ用意し、フレームごとにキャンバスを消去して最初から描画することにしました。一方で、文字通り動く部分が多すぎ、他方で、重複する複数のキャンバスに分割して選択的に更新するほど複雑ではありません)。

残念ながら、キャンバスに切り替えるのは、drawImage() で CSS 背景をミラーリングするだけではありません。DOM を介して要素を組み合わせるときに、多くの機能が失われます。最も重要なのは、z インデックスによるレイヤリングとマウスイベントです。

私はすでに「プレーン」というコンセプトで z インデックスを抽象化しています。このドゥードルは、遠く離れた空から、すべての前にあるマウス ポインタまで、いくつかのプレーンを定義し、ドゥードル内のすべてのアクターが、どのプレーンに属するかを決定する必要があります(プレーン内の小さなプラス/マイナスの修正は planeCorrection を使用して可能でした)。

DOM を介してレンダリングする場合、プレーンは単に z インデックスに変換されます。ただし、キャンバスでレンダリングする場合は、描画する前に、プレーンに基づいて長方形を並べ替える必要があります。これを毎回行うとコストがかかるため、順序は、アクターが追加されたとき、または別のプレーンに移動したときにのみ再計算されます。

マウスイベントも抽象化しました。DOM とキャンバスの両方で、z インデックスの高い完全に透明なフローティング DOM 要素を追加しました。この要素は、マウスオーバー/アウト、クリック、タップに対してのみ反応します。

このドゥードルで試してみたかったことの 1 つが、第四の壁を破ることです。上記のエンジンにより、キャンバスベースのアクターと DOM ベースのアクターを組み合わせることができました。たとえば、フィナーレの爆発は、ユニバース内のオブジェクトのキャンバスと、Google ホームページの残りの部分の DOM の両方に存在します。通常は飛び回り、他のアクターと同様にギザギザのマスクで切り抜かれる鳥は、撮影レベルではトラブルに巻き込まれないようにし、[I’m Feeling Lucky] ボタンに止まります。鳥がキャンバスから離れて DOM 要素になり(後でその逆も)、訪問者には完全に透過的であることを期待していました。

フレームレート

現在のフレームレートを把握し、フレームレートが遅すぎる(または速すぎる)場合に反応することは、エンジンの重要な部分でした。ブラウザはフレームレートを報告しないため、自分で計算する必要があります。

まず requestAnimationFrame を使用し、前者が使用できない場合は従来の setTimeout にフォールバックしました。requestAnimationFrame は、状況に応じて CPU を賢く節約します(ただし、一部は Google が行っています。後述)。また、setTimeout よりも高いフレームレートを実現することもできます。

現在のフレームレートの計算は簡単ですが、急激な変化が生じる可能性があります。たとえば、別のアプリがしばらくの間コンピュータを占有すると、急激に低下することがあります。そのため、物理的なティックごとに 100 回だけ「ローリング」(平均化)フレームレートを計算し、その結果に基づいて判断します。

どのような意思決定でしょうか

  • フレームレートが 60 fps を超える場合は、スロットリングされます。現在、一部のバージョンの Firefox の requestAnimationFrame にはフレームレートの上限がないため、CPU を無駄に消費する意味はありません。ただし、他のブラウザではフレームレートが 60 fps よりわずかに高いという丸め誤差があるため、実際には 65 fps に制限されています。誤ってスロットリングを開始しないようにするためです。

  • フレームレートが 10 fps 未満の場合は、フレームをドロップするのではなく、エンジンを遅くします。どちらに転んでも損失ですが、フレームを過度にスキップすると、単に(連続性のある)ゲームが遅くなるよりも混乱を招くと考えました。これには別の副次的な効果もあります。システムが一時的に遅くなった場合、エンジンが必死に追いつくために、ユーザーが奇妙なジャンプを感じることはなくなります。(パックマンでは若干異なる方法で処理しましたが、最低フレームレートの方が良い方法です)。

  • 最後に、フレームレートが危険なほど低下した場合にグラフィックを簡素化することも考えられます。マウス ポインタを除き、Google は Lem の Doodle でこれを行いません(後述)。ただし、低速のパソコンでも Doodle がスムーズに動作するように、余分なアニメーションを削除する可能性はあります。

また、物理的なティックと論理的なティックという概念もあります。前者は requestAnimationFrame/setTimeout から取得されます。通常のゲームプレイでは比率は 1:1 ですが、早送りの場合は、物理的なティックごとに論理的なティックを追加します(最大 1:5)。これにより、論理ティックごとに必要な計算をすべて実行できますが、画面上の更新は最後のティックのみに指定します。

ベンチマーク

キャンバスが利用可能な場合は常に DOM よりも高速であると想定できます(実際、初期の段階ではそうでした)。必ずしもそうとは限りません。テストの結果、Mac 版 Opera 10.0 ~ 10.1 と Linux 版 Firefox では、DOM 要素の移動が実際に速いことがわかりました。

理想的には、この Doodle は、style.leftstyle.top を使用して移動する DOM 要素、キャンバスでの描画、さらには CSS3 変換を使用して移動する DOM 要素など、さまざまなグラフィック手法をサイレントでベンチマークします。

フレームレートが最も高い設定に切り替えます。そのためのコード作成を開始しましたが、少なくとも私のベンチマーク方法は非常に信頼性が低く、時間もかかりました。ホームページでは時間がかかりますが、Google はスピードを重視しており、クリックまたはタップするとすぐに、その場でドゥードルが表示されてゲームプレイが開始されるようにしています。

結局、ウェブ開発は、やるべきことをやらなければならないということになります。後ろを振り返って誰も見ていないことを確認してから、Opera 10 と Firefox をキャンバスからハードコードしました。次の人生では、<marquee> タグとして生まれ変わります。

CPU の節約

家に遊びに来た友人が「ブレイキング バッド」のシーズン フィナーレを見て、ネタバレを言い、DVR から削除してしまうような人はいませんか?そのような状況は避けたいはずです。

はい、史上最悪の例えです。ただし、Google の Doodle がそのような存在になることも望んでいません。ユーザーのブラウザのタブにアクセスできるということは特権であり、CPU サイクルを占有したり、ユーザーの注意をそらしたりすることは、迷惑なゲストになることになります。そのため、誰もドゥードゥルを操作していない場合(タップ、マウスクリック、マウスの移動、キー入力なし)、最終的にはスリープ状態にする必要があります。

時期

  • ホームページで 18 秒経過後(アーケードゲームではこれをアトラクト モードと呼びます)
  • タブにフォーカスがある場合: 180 秒後
  • タブにフォーカスが当たっていない場合(ユーザーが別のウィンドウに切り替えたが、非アクティブなタブでドゥードルを視聴している場合など)30 秒後に終了
  • タブが非表示になった場合(ユーザーが同じウィンドウ内の別のタブに切り替えたなど。表示されない場合はサイクルを無駄にする意味がない)直ちに

現在フォーカスが設定されているタブを確認するにはどうすればよいですか?window.focuswindow.blur に接続します。タブが表示されていることをどのように確認しますか?新しい Page Visibility API を使用して、適切なイベントに反応します。

上記のタイムアウトは、通常よりも緩和されています。これらのアニメーションを、アンビエント アニメーション(主に空と鳥)を多数使用したこの特定の Doodle に適応させました。理想的には、タイムアウトはゲーム内インタラクションで制限されるべきです(たとえば、着陸直後に、鳥が今すぐ寝られるとドゥードルに報告できる)。しかし、最終的には実装しませんでした。

空は常に動いているため、スリープ状態と復帰状態の切り替え時に、落書きが停止または開始されるわけではありません。停止する前に速度が低下し、再開するときには逆に速度が上がります。必要に応じて、物理的なティックあたりの論理的なティック数を増減します。

遷移、変換、イベント

HTML の強みの 1 つは、自分で改善できることです。HTML と CSS の通常のポートフォリオで不十分な点があれば、JavaScript を使って拡張できます。残念ながら、多くの場合、ゼロからやり直す必要があります。CSS3 遷移は優れていますが、新しい遷移タイプを追加したり、遷移を使用して要素のスタイル設定以外のことを行ったりすることはできません。別の例: CSS3 変換は DOM には適していますが、キャンバスに移行すると、突然独力で作業することになります。

このような問題があるため、Lem の Doodle には独自の遷移エンジンと変換エンジンが用意されています。2000 年代から電話があったとか、そういう話は承知しています。私が組み込んだ機能は CSS3 ほど強力ではありませんが、エンジンが何をしても一貫性があり、より細かく制御できます。

まず、シンプルなアクション(イベント)システムから始めました。これは、setTimeout を使用せずに将来のイベントを発生させるタイムラインです。ドゥードルの時間は、任意の時点で速くなる(早送り)、遅くなる(低いフレームレート、CPU を節約するためにスリープ状態になる)、または完全に停止する(画像の読み込みが完了するのを待つ)ため、物理時間から切り離される可能性があります。

遷移は、他の種類のアクションと同じです。基本的な動きと回転に加えて、相対的な動き(オブジェクトを右に 10 ピクセル移動するなど)、震えなどのカスタム設定、キーフレーム画像アニメーションもサポートしています。

回転についても触れましたが、これも手動で行うことができます。回転が必要なオブジェクトには、さまざまな角度のスプライトがあります。主な理由は、CSS3 とキャンバスの両方の回転で、許容できない視覚的なアーティファクトが発生し、さらに、それらのアーティファクトがプラットフォームごとに異なることです。

回転するオブジェクトが他の回転するオブジェクトに接続されている場合(たとえば、回転する上腕に接続されている下腕に接続されているロボットの手など)は、ピボットの形で簡易な transform-origin も作成する必要があります。

これらはすべて、HTML5 ですでにカバーされている領域を最終的にカバーする、かなりの量の作業です。ただし、ネイティブ サポートが十分でない場合は、自転車の再発明が必要になることもあります。

画像とスプライトの処理

エンジンは、ドゥードルを実行するだけでなく、ドゥードルに取り組むためにも使用します。上記でいくつかのデバッグ パラメータを紹介しましたが、残りは engine.readDebugParams で確認できます。

スプライト化は、Google でもよく使用するよく知られた手法です。これにより、バイト数を節約し、読み込み時間を短縮できます。また、プリロードも容易になります。ただし、開発が難しくなるというデメリットもあります。画像を変更するたびに、再スプリティングが必要になります(大部分は自動化されていますが、それでも手間がかかります)。そのため、このエンジンは、開発用の元画像での実行と、engine.useSprites を介した本番環境用のスプライトの実行をサポートしています。どちらもソースコードに含まれています。

パックマンの Doodle
パックマンの Doodle で使用されるスプライト。

また、画像を事前に読み込んで、画像が間に合わなかった場合はその時点でドゥードルを停止することもできます。進行状況バーも表示されます。(「疑似」というのは、残念ながら HTML5 でさえ、画像ファイルのどの程度が読み込まれたかを把握できないためです)。

リグされた進行状況バーが表示された読み込みグラフィックのスクリーンショット。
リグされた進行状況バーが表示された読み込みグラフィックのスクリーンショット。

一部のシーンの複数のスプライトは、並列接続を使用して読み込みを高速化するためではなく、iOS の画像の 350 万ピクセルの制限が原因で使用されています。

HTML5 は、このすべてにどのように適合するのでしょうか。上記にはほとんどありませんが、スプリット/切り抜き用に作成したツールは、キャンバス、bloba[download] など、すべて新しいウェブ技術でした。HTML の魅力の 1 つは、以前はブラウザの外部で行う必要があった処理が、徐々に HTML に統合されていることです。PNG ファイルの最適化以外は、ブラウザ外で行う必要はありません。

ゲーム間の状態の保存

レムの世界は常に大きく、生き生きとしていて、リアルでした。彼の物語は通常、あまり説明せずに始まり、最初のページはメディアス レスから始まり、読者は自分で道を見つけなければなりませんでした。

「サイバーヤード」も例外ではなく、その雰囲気を Doodle で再現したいと考えました。まず、ストーリーを過度に説明しないようにします。もう 1 つの大きな部分は、本の世界観のメカニカルな性質に合っていると感じたランダム化です。ランダム化を扱う多くのヘルパー関数があり、さまざまな場所で使用しています。

また、他の方法でもリプレイアビリティを高めたいと考えています。そのためには、これまでに何回ドゥードルが完成したかを把握する必要がありました。歴史的に正しい技術的な解決策は Cookie ですが、Google のホームページでは機能しません。Cookie を使用すると、すべてのページのペイロードが増加します。Google は速度とレイテンシを重視しています。

幸い、HTML5 には Web Storage が用意されています。これは使いやすく、一般的な再生回数やユーザーが最後に再生したシーンを保存して呼び出すことができます。Cookie よりもはるかに優れた方法です。

この情報の用途

  • 早送りボタンを表示し、ユーザーがすでに見たカットシーンをすばやくスキップできるようにします。
  • フィナーレ中に異なる N 個のアイテムを表示する
  • シューティング レベルの難易度を若干引き上げました
  • 3 回目以降の再生では、別のストーリーのイースター エッグ ドラゴンが表示されます

これを制御するデバッグ パラメータはいくつかあります。

  • ?doodle-debug&doodle-first-run - 初回実行と見なす
  • ?doodle-debug&doodle-second-run - 2 回目の実行として扱う
  • ?doodle-debug&doodle-old-run - 古い実行として扱う

タッチデバイス

タッチデバイスでこのゲームを快適にプレイできるようにしました。最新のタッチデバイスは十分な性能を備えているため、このゲームを快適にプレイできます。また、タップ操作でゲームをプレイする方が、クリック操作でプレイするよりもはるかに楽しいです。

ユーザー エクスペリエンスの一部を事前に変更する必要がありました。当初、カットシーンやインタラクティブではない部分が進行中であることを示すのは、マウス ポインタのみでした。後で右下に小さなインジケーターを追加し、タッチデバイスにはマウス ポインタがないため、マウス ポインタにのみ依存しなくて済むようにしました。

標準 ビジー状態 クリック可能 クリック済み
進行中の作業
進行中の通常のポインタ
進行中のビジー ポインタ
処理中のクリック可能なポインタ
処理中のクリックされたポインタ
決勝
最終的な通常ポインタv
最終ビジー ポインタ
最終的なクリック可能なポインタ
最後にクリックされたポインタ
開発中のマウスポインタと最終的な同等のマウスポインタ。

ほとんどの機能はすぐに使用できました。しかし、タッチ操作の簡単な即席のユーザビリティ テストで、2 つの問題が見つかりました。一部のターゲットが押せすぎ、マウスクリック イベントをオーバーライドしただけなので、すばやいタップが無視されたのです。

クリック可能な透明な DOM 要素を別途用意することで、ビジュアルとは別にサイズを変更できたので、非常に役立ちました。タッチデバイス用に 15 ピクセルの追加パディングを導入し、クリック可能な要素を作成するたびに使用しました。(Fitts 氏を喜ばせるために、マウス環境にも 5 ピクセルのパッディングを追加しました)。

他の問題については、マウスクリックに依存せず、適切なタップ開始ハンドラとタップ終了ハンドラをアタッチしてテストしました。

また、よりモダンなスタイル プロパティを使用して、WebKit ブラウザがデフォルトで追加するタップ機能(タップ ハイライト、タップ コールアウト)を削除しています。

ドゥードルを実行しているデバイスがタッチに対応しているかどうかを検出するにはどうすればよいですか?遅延読み込み。事前に判断するのではなく、最初のタップ開始イベントを受け取った後に、IQ を組み合わせてデバイスがタップをサポートしていることを推測しました。

マウスポインタのカスタマイズ

ただし、すべてがタップベースというわけではありません。ガイドラインの 1 つは、できるだけ多くのものを Google の検索結果ページのデザインに盛り込むことでした。小さなサイドバー UI(早送り、疑問符)、ツールチップ、さらにはマウス ポインタも含まれます。

マウスポインタをカスタマイズする方法一部のブラウザでは、カスタム イメージ ファイルにリンクすることでマウスカーソルを変更できます。ただし、これは十分にサポートされておらず、制限も多少あります。

そうでない場合は、マウスポインタを、他の要素と同じようにドゥードルの一部にするのはどうでしょうか?これは機能しますが、いくつかの注意点があります。主なものは次のとおりです。

  • ネイティブ マウスポインタを削除できる必要がある
  • マウスカーソルを「実際の」カーソルと同期させ続ける必要がある

前者は難しい問題です。CSS3 では cursor: none を使用できますが、一部のブラウザではサポートされていません。そのため、いくつかの演技に頼る必要がありました。空の .cur ファイルをフォールバックとして使用したり、一部のブラウザに具体的な動作を指定したりするだけでなく、他のブラウザをハードコードしてエクスペリエンスから完全に除外したりしました。

もう 1 つは、一見すると比較的簡単ですが、マウス ポインタは落書きの世界の一部であるため、すべての問題を引き継ぐことになります。最大の課題は、ドゥードルのフレームレートが低いと、マウス ポインタのフレームレートも低くなります。マウス ポインタは手の延長であるため、どんな状況でもレスポンシブである必要があります。フレームレートが低いと、その結果は深刻です。(過去に Commodore Amiga を使用したことがある人は、激しくうなずいています)。

この問題の解決策として、マウス ポインタを通常の更新ループから切り離すという、やや複雑な方法があります。私たちは、睡眠を必要としない別の宇宙で、まさにそうしました。もっと簡単な解決方法はありますか?ローリング フレームレートが 20 fps を下回った場合に、ネイティブ マウス ポインタに戻すだけです。(ここで、ローリング フレームレートが役立ちます。現在のフレームレートに反応し、20 fps 付近で振動した場合、カスタム マウス ポインタが常に非表示と表示を繰り返すことになります)。次に進みます。

フレームレートの範囲 動作
10 fps 以上 フレームの欠落を防ぐため、ゲームの速度を落とします。
10 ~ 20 fps カスタム マウスポインタではなく、ネイティブ マウスポインタを使用する。
20 ~ 60 fps 通常の動作。
60 fps 超 フレームレートがこの値を超えないようにスロットルします。
フレームレートに依存する動作の概要。

なお、マウスポインタは Mac では黒ですが、PC では白です。その理由は、架空の世界でもプラットフォーム戦争には燃料が必要です。

まとめ

これは完璧なエンジンではありませんが、完璧を目指しているわけではありません。これは Lem の Doodle とともに開発されたもので、Doodle に固有のものです。問題ありません。ドン ノット氏の有名な言葉に「早計な最適化は諸悪の根源である」というものがあります。エンジンを最初に単独で作成し、後で適用するだけが理にかなっているとは思いません。理論が実践に役立つのと同じように、実践が理論に役立ちます。私の場合は、コードが破棄され、いくつかの部分が何度も書き直され、多くの共通部分が事後ではなく事前に認識されました。しかし、最終的には、私たちが望んでいたことを実現できました。それは、Stanisław Lem の業績と Daniel Mróz の絵を、考えられる最善の方法で称えることです。

以上で、Google が行わなければならなかった設計上の選択とトレードオフ、そして特定の実際のシナリオで HTML5 を使用した方法について、ご理解いただければ幸いです。ソースコードを試して、ご意見をお寄せください。

私もそうしました。以下のカウントダウンは、2011 年 11 月 23 日午前 0 時(ロシア時間)に公開された Lem の Google ドゥードルの最初のタイムゾーンで、最後の数日間公開されていました。ちょっとおバカな話かもしれませんが、落書きのように、重要ではないように見えるものにも深い意味があることがあります。このカウンタは、エンジンの優れた「ストレス テスト」でした。

宇宙船レムのカウントダウン クロックのスクリーンショット。
宇宙内のカウントダウン クロックを示す Lem の Doodle のスクリーンショット。

これが Google の検索結果ページのデザインの寿命を捉える 1 つの方法です。数か月もの作業、数週間のテスト、48 時間のベーキング。すべては、ユーザーが 5 分間楽しむために行われます。数千もの JavaScript 行は、その 5 分が有効に活用されることを願っています。ご活用ください。