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

ハロー、(変な)世界

Google ホームページは魅力的なコーディング環境です。特にスピードとレイテンシを重視すること、あらゆる種類のブラウザに対応し、さまざまな状況下で機能しなければならないことなど、多くの難しい制限が伴います。驚きと喜びです。

Google Doodle は、Google のロゴと入れ替わる特別なイラストです。筆者とペンやブラシとの関係には長きにわたって制約の独特な特徴がありましたが、インタラクティブなコンテンツにも貢献しています。

私がコーディングしたすべてのインタラクティブな Doodle(パックマンジュール ヴェルヌワールド フェア)と、私が手伝った多くの Doodle は、未来的でアナクロニスティックなものが等しくありました。最先端のウェブ機能を活用したり、ブラウザ間で互換性を持たせた実用性を存分に活かしたりする絶好の機会です。

インタラクティブな Doodle から多くのことを学べます。最近の Stanisław Lem ミニゲームも例外ではなく、17,000 行の JavaScript コードが Doodle 史上初めてさまざまなことを試しています。本日は、そのコードを皆様と共有したいと思います。面白いものが見つかるかもしれません。また、私の間違いを指摘するかもしれません。それについて少しお話ししたいと思います。

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

なお、Google のホームページは技術デモのための場ではありません。Doodle では、特定の人物やイベントを祝いたいと考えています。そのためには、優れたアートと、呼び出すことができる優れたテクノロジーを使用しますが、テクノロジーのためにテクノロジーを称賛することは決してありません。つまり、広く知られている HTML5 のどの部分が使用可能であるかを注意深く検討し、それによって Doodle の質を高めるために、その要素が気が散ったり見づらくなったりすることなく、改善できるかどうかを判断する必要があります。

それでは、スタニスワフ レムの Doodle に登場した最新のウェブ テクノロジーと、そうでないものを見てみましょう。

DOM と canvas を使用したグラフィック

Canvas は強力で、この Doodle で実現したいと考えていたことを実現しました。しかし、私たちが検討していた古いブラウザの中には、この機能をサポートしていないものもあります。私は、本来は優れたエクスキャンバスを作成した人とオフィスを共有していますが、別の方法を選ぶことにしました。

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

このアプローチには興味深い課題が伴います。たとえば、DOM 内のオブジェクトを移動または変更するとすぐに影響が生じますが、キャンバスの場合、すべてが同時に描画される特定の瞬間があります。(キャンバスを 1 つだけ作成し、それをクリアして、フレームごとにゼロから描画することにしました。多すぎる、つまり文字どおり可動する部分が多く、複雑すぎるため、重複する複数のキャンバスに分割して選択的に更新するほどの複雑さ。

残念ながら、キャンバスへの切り替えは単に drawImage() で CSS 背景をミラーリングするだけの単純な作業ではありません。DOM でさまざまなものをまとめると、無料で得られる多くのものが失われます。最も重要なのは、Z-Index とマウスイベントを重ねることです。

私はすでに「平面」というコンセプトで Z-Index を抽象化していました。Doodle は、遠くの空からすべての前のマウスポインタまで、多数の平面を定義しました。Doodle 内の各行為者は、どの平面が属しているのかを判断する必要がありました(平面内の小さなプラス/マイナスの修正は、planeCorrection を使用して可能でした)。

DOM を使用してレンダリングすると、平面は単純に Z-Index に変換されます。 しかし、キャンバスを使用してレンダリングする場合は、長方形を描画する前に平面に基づいて並べ替える必要があります。毎回これを行うには費用がかかるため、実行者が追加されたとき、または別のプレーンに移動したときにのみ、順序が再計算されます。

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

この Doodle で試してみたかったことの 1 つは、 第四の壁を破ることでした。上記のエンジンにより、キャンバスベースのアクターと DOM ベースのアクターを組み合わせることができました。たとえば、フィナーレの爆発音は、ユニバース オブジェクト用のキャンバスと、Google ホームページのその他の領域にある DOM の両方に表示されています。この鳥は通常、他の俳優と同様に飛び回り、ギザギザのマスクでクリップされていますが、撮影レベルではトラブルから離れることを決心し、[I’m Feeling Lucky] ボタンに座っています。つまり、鳥がキャンバスを離れて DOM 要素になり(後でその逆も同様)、訪問者に対して完全に透過的であることが望まれました。

フレームレート

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

まず、requestAnimationFrame を使用することにしましたが、setTimeout を使用できない場合は以前の setTimeout にフォールバックしました。requestAnimationFrame は、以下で説明するように、いくつかの状況で CPU を節約しますが、単に setTimeout よりもフレームレートを高くすることもできます。

現在のフレームレートの計算は簡単ですが、大幅に変化する可能性があります。たとえば、別のアプリがしばらくコンピュータを占有すると、フレームレートが急速に低下する可能性があります。したがって、100 物理ティックごとにのみ「ローリング」(平均)フレームレートを計算し、それに基づいて決定を行います。

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

  • フレームレートが 60 fps を超えると、スロットリングが行われます。現在、Firefox の一部のバージョンでは、requestAnimationFrame にはフレームレートの上限がなく、CPU を浪費することはありません。実際には 65 fps に制限しています。これは、丸め誤差により、他のブラウザの 60 fps より少し高くなるだけです。誤ってスロットリングを開始するのは望ましくありません。

  • フレームレートが 10 fps 未満の場合は、フレームをドロップするのではなく、単にエンジンの速度を下げます。これは負け負けですが、フレームを過度にスキップするのは、単純に低速な(かつまとまりのある)ゲームに比べると混乱を招くと考えました。そのもう一つの副作用として、システムが一時的に遅くなっても、エンジンが追いついていないため、ユーザーが不思議な動きを感じることはありません。(Pac-Man では若干異なる方法で処理しましたが、最小フレームレートの方が適切なアプローチです)。

  • 最後に、フレームレートが著しく低下した場合にグラフィックを簡素化する方法を検討します。マウスポインタを除いて、レムの Doodle については対応していません(詳細は後述します)。仮に、低速のコンピュータでも Doodle が滑らかに見えるようにするために、無関係なアニメーションの一部が失われてしまう可能性があります。

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

ベンチマーク

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

理想としては、Doodle はさまざまなグラフィック テクニックを暗黙的にベンチマークするものです。たとえば、DOM 要素の移動(style.leftstyle.top を使った移動、キャンバスでの描画、CSS3 変換を使った DOM 要素の移動など)です。

最もフレームレートが高い方に切り替えますそのためのコードを記述し始めましたが、少なくとも私のベンチマークの方法は信頼性が低く、多くの時間を要することがわかりました。私たちはスピードを重視しており、Doodle がすぐに表示され、クリックまたはタップしたらすぐにゲームプレイが開始できるようにしたいと考えています。

ウェブ開発は結局のところ、本来の作業をこなさなければならないことになりかねません。私は肩の後ろを見て、誰も見ていないことを確認してから、Opera 10 と Firefox をキャンバスからハードコードしました。次の人生では、<marquee> タグとして戻ってきます。

CPU の節約

友人が家にやってきて『ブレイキング・バッド』のシーズンの最終回を見て、それを台無しにし、DVR から削除するとします。そんな男にしたくないでしょ?

これは最悪のたとえ話です。しかし、Google は Doodle もそのような人にはしません。だれかのブラウザタブにアクセスを許可されているという事実は特権であり、CPU サイクルをためらせたり、ユーザーの注意をそらしたりすると、不快なゲストになってしまいます。そのため、Doodle で遊ぶ人がいない場合(タップ、マウスのクリック、マウス操作、キーの押下がない)、最終的には Doodle はスリープ状態になるようにします。

時期

  • ホームページで 18 秒が経過した後(このアーケード ゲームは「アトラクト モード」と呼ばれます)
  • タブにフォーカスがある場合は 180 秒後
  • タブにフォーカスがない場合(例: ユーザーが別のウィンドウに切り替えたが、アクティブでないタブで Doodle を見ている、など)
  • タブが表示されなくなった場合(たとえば、ユーザーが同じウィンドウ内の別のタブに切り替えた場合。見えなくても、サイクルが無駄になりません)

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

上記のタイムアウトは、通常よりも寛容です。私はこの Doodle に適応させました。Doodle には多くのアンビエント アニメーション(主に空と鳥)が含まれています。理想的には、タイムアウトはゲーム内のインタラクションで制限します。たとえば、着地した直後に鳥が Doodle に「今すぐ眠れる」と報告しますが、私はそれを最終的には実装しませんでした。

空は常に動いているため、眠りにつくときや目を覚ますときに、Doodle は停止または開始するだけではありません。一時停止する前に速度が低下し、必要に応じて物理ティックあたりの論理ティックの数を再開、増減します。

遷移、変換、イベント

HTML の強みの一つは、自分自身で改善できることです。通常の HTML と CSS のポートフォリオに不十分な点があれば、JavaScript をラングリングして拡張できます。残念なことに、ゼロから始める必要が生じることもよくあります。CSS3 遷移は便利ですが、新しい遷移タイプを追加したり、遷移を使用して要素のスタイル設定以外の動作を行ったりすることはできません。もう一つの例は DOM には CSS3 変換が適していますが キャンバスに移行すると

このような問題などから、Lem Doodle には独自の遷移エンジンと変形エンジンが搭載されています。はい、2000 年代の年代のようなものです。私が組み込んだ機能は CSS3 ほど強力ではありませんが、どのようなエンジンでも一貫して動作し、制御が大幅に向上します。

まず、シンプルなアクション(イベント)システムから始めました。これは、setTimeout を使用せずに将来のイベントを送信するタイムラインです。これは、落書き時刻が速くなる(早送り)、遅くなる(フレームレートが低い、CPU を節約するためにスリープ状態になる)、完全に停止する(画像の読み込みが完了するのを待つ)など、実際の時刻から離れる可能性があるためです。

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

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

回転するオブジェクトの一部が他の回転するオブジェクトに取り付けられていることを考えると、たとえば、下腕に接続されたロボットの手、回転する上腕にそれ自体が取り付けられていることを考えると、私は貧しい人の変形原点をピボット形式で作成する必要もありました。

これらはすべて、最終的には HTML5 によって対処されている面をカバーする、かなりの量の作業です。しかし、ネイティブ サポートが十分とは言えず、ホイールの刷新が必要になったときです。

画像とスプライトの取り扱い

エンジンは、Doodle を実行するためだけのものではなく、Doodle に取り組むためのものでもあります。上記のデバッグ パラメータをいくつか共有しました。残りは engine.readDebugParams にあります。

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

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

また、画像のプリロードもサポートしており、画像の読み込みが間に合わなかった場合は、Doodle を中止できます。その際、疑似進行状況バーが表示されます。(あいにく、HTML5 でも、読み込み済みの画像ファイルの量は判別できないからです)。

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

一部のシーンでは、並列接続で読み込み速度をそれほど高めるためではなく、複数のスプライトを使用します。これは単に、iOS の画像では 500 万ピクセル中 300 万ピクセルの制限によるものです。

HTML5 の位置付けスプライト/切り抜き用に作成したツールは、キャンバス、bloba[download] など、まったく新しいウェブテクノロジーでした。HTML の優れた点の 1 つは、以前はブラウザの外部で実行しなければならなかった処理を少しずつ取り入れることができる点です。必要な作業は、PNG ファイルの最適化だけでした。

ゲーム間で状態を保存する

レムの世界は常に広大で生き生きとしたリアルさを感じていました。彼のストーリーは通常、説明の点ではあまり説明されずに始まります。最初のページはメディア解像度で始まり、読者は彼女または彼の道を見つけなければなりません。

「サイバリアド」も例外ではなく、Doodle でもその雰囲気を再現したいと考えました。まず、ストーリーを過度に説明しないようにしましょう。もう 1 つの大きな部分はランダム化です。これは本書の世界が持つ力学的な性質に合っていると考えました。ランダム性を扱う多くのヘルパー関数を、多くの場所で使用できます。

また、他の方法でリプレイ可能性を高めたいと考えていました。そのためには、これまでに Doodle が何回完成したかを知る必要がありました。歴史的に正しい技術的解決策は Cookie ですが、Google のホームページでは機能しません。Cookie のたびにページのペイロードが増加します。Google では、スピードとレイテンシを非常に重視しています。

幸いなことに、HTML5 ではウェブ ストレージを簡単に使用できるため、一般的な再生回数とユーザーが最後に再生したシーンを保存して呼び出すことができます。Cookie では不可能でした。

収集した情報の用途

  • ユーザーがすでに見たカットシーンを早送りできるボタンが表示されます。
  • フィナーレで N 個の項目が表示されます
  • 射撃の難易度が少し上がります
  • 3 回目以降のプレイでは、別のストーリーのドラゴンのイースター エッグの確率が表示されます

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

  • ?doodle-debug&doodle-first-run – 初めて実行したとき
  • ?doodle-debug&doodle-second-run – 2 回目の走りをする
  • ?doodle-debug&doodle-old-run – 古い実行を想定する

タッチデバイス

タッチデバイスでも Doodle が違和感なく感じられるものにしました。最新のものは Doodle がきれいに動き、タップでゲームを体験できるのがクリックよりもずっと楽しいです。

ユーザー エクスペリエンスにいくつかの事前の変更が必要でした。もともと、マウスポインタは、カットシーン/非インタラクティブな部分が発生していることを通知するための唯一の場所でした。その後、右下に小さなインジケーターを追加したため、マウスポインタだけに依存する必要はありません(タッチデバイスには存在しないことを前提)。

標準 混んでいます クリック可能 クリック済み
対応中の作業
進行中の通常のポインタ
進行中のビジー ポインタ
進行中のクリック可能ポインタ
進行中のクリック型ポインタ
最終版
ファイナル ノーマル ポインタv
最後のビジー状態のポインタ
クリック可能な最終ポインタ
最後にクリックされたポインタ
開発中のマウスポインタと、最終的な同等のもの。

ほとんどの機能は最初から機能しており、しかし、タップ エクスペリエンスの即座のユーザビリティ テストでは、2 つの問題が見つかりました。1 つは、一部のターゲットが押しにくいこと、もう 1 つはマウスクリック イベントをオーバーライドしただけなので、クイックタップが無視されたことです。

クリック可能な透明な DOM 要素を別々に用意しておくと、ビジュアルとは無関係にサイズを変更できるため、非常に便利です。タッチデバイス用に 15 ピクセルのパディングを導入し、クリック可能な要素が作成されるたびに使用しました。(Mr. Fitts にため、マウス環境用に 5 ピクセルのパディングも追加しました)。

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

また、WebKit ブラウザがデフォルトで追加する一部のタッチ機能(ハイライト表示のタップ、コールアウトのタップ)を削除するため、より最新のスタイル プロパティを使用します。

Doodle を実行している特定のデバイスがタップをサポートしているかどうかは、どのように検出するのでしょうか?ぐるぐる回る。まずは IQ を組み合わせることで、最初の接触開始イベントの発生後にデバイスがタッチに対応しているかどうかを差し引くようにしました。

マウスポインタをカスタマイズする

しかし、すべてがタップベースというわけではありません。その指針の 1 つは Doodle のストーリーに できるだけ多く盛り込むことでした。小さなサイドバー UI(早送り、疑問符)、ツールチップ、マウスポインタもあります。

マウスポインタをカスタマイズする方法一部のブラウザでは、独自の画像ファイルにリンクしてマウスカーソルを変更できます。ただし、これは適切にサポートされておらず、ある程度制限されます。

そうでない場合、どうしますか?マウスポインタを Doodle の別の俳優に 作ってみたらどうでしょうか?この方法は機能しますが、主に次のような注意点があります。

  • ネイティブのマウスポインタを
  • マウスポインタを本物のポインタと 同期させておく必要があります

これは難しい問題です。CSS3 では cursor: none を使用できますが、一部のブラウザではこれもサポートされていません。空の .cur ファイルをフォールバックとして使用し、一部のブラウザで具体的な動作を指定し、他のブラウザはエクスペリエンスからハードコードするなど、なんらかの技能を利用する必要がありました。

もう 1 つは表面上は比較的簡単ですが、マウスポインタは Doodle の世界の一部にすぎず、その問題もすべて継承します。一番大きな問題は、Doodle のフレームレートが低いと、マウスポインタのフレームレートも低くなります。マウスポインタは手の動きに合わせて自然に動作するため、どのような場合でもレスポンシブに感じられる必要があるため、悲惨な結果になります。(過去にコモドール アミガを使っていた人々は今や激しくうなずいています)。

この問題のやや複雑な解決策の 1 つは、マウスポインタを通常の更新ループから切り離すことです。寝る必要のない代替世界で 成し遂げたのよこの問題をよりシンプルに解決するには、ローリング フレームレートが 20 fps を下回った場合にネイティブ マウスポインタに戻すだけです。(ここで役立つのがローリングフレームレートです現在のフレームレートに反応して 20 fps 前後に振動すると、ユーザーにはカスタム マウスポインタが常に非表示と表示されてしまいます)。これにより、次のことが行われます。

フレームレートの範囲 動作
10 fps 超 より多くのフレームがドロップされないように、ゲームを遅くします。
10 ~ 20 fps カスタム マウスポインタではなくネイティブ マウスポインタを使用します。
20 ~ 60 fps 通常のオペレーション。
60 fps 超 フレームレートがこの値を超えないようにスロットリングします。
フレームレート依存の動作の概要

また、マウスポインタが Mac では黒く、PC では白くなっています。なぜでしょう。プラットフォーム戦争では、架空の世界であっても 燃料が必要だからです

おわりに

これは完璧なエンジンではありませんが、実現を目指しているわけではありません。レム ドゥードルと並んで開発されたもので、非常に特化しています。大丈夫です。Don Knuth が有名なように、「早期の最適化はすべての悪の根源です」。私は、最初にエンジンを単独で記述し、後でそれを適用することが理にかなっているとは思わない。私の場合、コードが破棄され、いくつかの部分が何度も書き換えられ、多くの一般的な部分が事実ではなく投稿に気づきました。スタニスワフ レムのキャリアとダニエル ムロスのスケッチを思いつく形で最も効果的に祝うことができました。

ここまでの説明が、私たちが行う必要があった設計上の選択とトレードオフのいくつか、そして実際の特定のシナリオでの HTML5 の使用法のお役に立てば幸いです。では、ソースコードをいろいろ試してみて、感想をお聞かせください。

私は自分で行いました。以下は最後の数日間に公開されており、ロシアでは 2011 年 11 月 23 日の早朝までカウントダウンしました。ロシアでは、レムの Doodle が最初に確認されたタイムゾーンでした。おそらくばかばかしいものですが、落書きと同様、重要でないものに見えても、より深い意味を持つことがあります。このカウンタは、エンジンにとって素晴らしい「ストレステスト」でした。

ユニバースのカウントダウン時計のレム Doodle のスクリーンショット。
レム ドゥードルのユニバース カウントダウン クロックのスクリーンショット。

Google Doodle は、制作に何か月もかけ、テストに数週間かけて、48 時間かけて作って、なんと 5 分間プレイする時間です。JavaScript の数千行が 1 行も、この 5 分の時間をうまく使えることを願っています。ご活用ください。