xkcdの「Machine」開発ノート
初期構想
- 3月末までアイデアを練り、4月初めに決めたアイデア
- 「Something Awfulのユーザーたちが作ったブルーボールGIFのように、タイル型の巨大装置を作れるだろうか? みんなが小さな四角形を1つずつ担当するんだ」
- 最初はアイデアが完全に固まっているように感じていたが、実際に話し合ってみると、まだ多くのことを決めなければならないと気づいた
- ボールがどこから出てくるのか、全員が同じ機械を見ているのか、目的は何か、プレイヤーはどう相互作用するのかなど、核心部分についてそれぞれ異なる考えを持っていた
以前の試みから学んだこと
- ユーザー制作コンテンツ中心のインタラクティブ漫画を作った経験があった
- Lorenz: 読者がパネルのテキストを書いて、ジョークとストーリーを発展させるエクスクイジット・コーパス(とても楽しかった)
- Collector's Edition: 読者がxkcdアーカイブに隠されたステッカーを探し、共有キャンバスに永久的に貼り付けるゲーム(意図した結果にはならなかった)
- 初期の空の中央マップから始めると混乱に陥る
- ステッカー配置へのインセンティブが不足していて、個人の行動でプロットを進めにくく、単純なパターンしか生まれなかった
- 全体のストーリーや目標がなく、ステッカー同士の関係も不明確だった
- 集団キャンバスが成功するには、何を作れるのかを例で示し、共有された文脈と目的が必要だ
制約条件の設計
- 大型の玉落としマシンを作ると決めた後、選択肢が多すぎるという問題に直面した
- 100x100サイズのグリッドで構成することに決めた
- クライアント側で1万個のタイルをリアルタイムにシミュレーションするのは危険に思えた
- プレイヤーが直接コミュニケーションせずに複雑な機械の下位区画を作る方法や、分離されたタイルが統合されたときに動作するか確信が持てなかった
- いくつもの思考実験の末に、3つの中核原則を定めた
1. 正確性を犠牲にしてでも、プレイヤーの表現力を最大化する
- 機械はどれほど予測可能であるべきか?
- 全体をサーバーで実行する案や、個々のタイルを検証する案も検討したが、プロトタイプエディタで混沌とした玉の衝突パターンを簡単に作れることがわかった
- 玉が妨げなく直線移動しない限り、予測不能な機械は簡単に作れてしまった
- 機械の予測可能性を高めることは、プレイヤーの自由度と相反する
- 開発期間が逼迫していたことも、予測やシミュレーションを減らすアプローチを後押しした
- 極度に非決定論的な機械や壊れた機械まで含めて、プレイヤーに非常に柔軟な制作の自由を与えることにした
- そのため、制約条件を満たしているかどうかの能動的な審査と、不適切なコンテンツの除去が必要になった
2. 互換性があり交換可能な機械を促す、厳格な制約条件を与える
- 審査の受け入れと予測不能なプレイヤー製マシンの存在によって、むしろさらに多くの秩序が必要になった
- 当初は入出力を完全に自由形式にすることも考えたが、審査過程で初期タイルの差し替えが必要になった場合、大規模な障害を引き起こしかねないと認識した
- 同じタイル空間で複数のプレイヤーが互換性のある設計を作れるよう、十分に強力な制約条件を設計した
- Robustness原則を適用: 「送るデータには保守的に、受け取るデータには寛容に」
- 入出力制約を与えるため、開始時点から機械全体のマップが必要だった
- マップ生成によって機械の難易度を調整した(単純な1入力1出力から、複雑な4入力4出力の合流まで)
- リアルタイムのフィードバックを与えるため、タイルが受け取ったのと近い速度で玉を排出するよう制限した
- 玉を飲み込んだり遅延させたりする機械を制限
- ランダムな入力速度でタイルをカオステストした
- 「機械をしばらく動かし、平均的に制約条件を満たしているか確認する」という原則を確立した
3. 機械は最初の30秒以内に安定状態へ到達しなければならない
- 審査者はどれだけ長く見守る必要があるのか、という問いが生じた
- 機械全体の審査にかかる時間を計算した(1万タイル基準で83.3時間)
- 30秒以内に安定状態へ入るように任意に決定した
- 玉が30秒後に消えるよう設定した
- 初期には有効期限がなく、プレイヤーがゲームを学んでいる間に玉が蓄積して画面を埋め尽くしていた
- アクティブな剛体が増えるにつれ、物理シミュレーションの速度が低下した
- 楽しさよりも、玉が邪魔になる状況だった
- 玉の有効期限によって、機械が時間経過とともにエラーを蓄積しないようになった
- 審査者は30秒見れば、ほとんどの玉がどこへ行きうるか把握できる
シミュレーションとシュールレアリスム
- Machineのアーキテクチャには2つの大きな課題があった
- 上記の設計制約で異質なタイル同士をつなぎ、全体の機械にすることは本当に機能するのか?
- 巨大な機械をサーバーでもクライアントでもリアルタイム実行できないなら、どう表示するのか?
スクロールしながら単一の玉を追跡できるようにすることが目標
- 機械全体がシミュレーションされていなくても、プレイヤーが見ている領域の周辺はシミュレーションされている必要があった
- 初期には、無限マップ上で表示領域だけをシミュレーションすることを試した
- かなりうまく動いたが、スクロール時にタイルが空の初期状態のままシミュレーションに入ってきて、流れに空白が生じた
- 空のタイルではなく、すでに活動しているように見える必要があった
2つ目の課題: タイルのスナップショットを安定状態に達した後でのみ撮り、スクロールで見える直前にだけ存在させること
- 最終漫画における、表示クリッピングを無効化したビュー(CSS
overflow:hidden, contain:paint 無効化):
- スナップショットに気づいただろうか? 特別に注意して見ない限り、気づきにくい
- レンダリングされたタイルだけが物理シミュレーション上に存在する
- 表示最適化: ビュー領域内の玉だけが見えるが、シミュレーションはタイル全体の範囲で行われる
- 機械の上部を装うため、シミュレーション最上段の行で玉を生成・供給した(入力制約の想定速度に基づく)
- 審査UIにスナップショット生成を組み込んだ
- 審査者はタイル承認前に最低30秒待たなければならない
- 承認ボタンをクリックするとスナップショットを生成
- 審査者の裁量で、機械が見栄えのよい状態になるまで少し長く待つこともできる
- スナップショット方式は期待以上にうまく機能した
- 機械に蓄積したエラーをリセットするという好ましい効果があった
- スクロールして見えるタイルの第一印象は、審査者が気に入ったクリーンで良い状態になる
- 実際には長く見ていると多くの機械が壊れたり破綻したりする可能性があるが、探索を続けると新しいスナップショットに入るため、目にすることはない
- 漫画の中でスクロールする機械は実在しない。シュールなのだ
- 全体が一度にシミュレーションされていないが、そのことがむしろより良い結果を生んでいる
ReactとDOMで数千個の玉をレンダリングする
- Rapier物理エンジンをベースに構築
- 優れたドキュメント、有用な基本要素のクリーンなAPI、Rust実装(ブラウザではWASMとして実行)のおかげで、印象的な性能が出た
- 初期にはRapierの決定論保証に惹かれたが、サーバー側シミュレーションは行っていない
- Rapierの上にカスタムReactコンテキスト
<PhysicsContext> を作成
- Rapier物理オブジェクトを生成し、Reactコンポーネントのライフサイクル内で管理する
- 物理や衝突面を持つ、配置可能な各オブジェクト向けの「ウィジェット」コンポーネントを作りやすくした
- Reactがシンプルで雑なscene graphの役割を果たした
- ビューのスクロール時にタイルのロード/アンロードを単純化: タイルがアンマウントされると、すべての物理とDOMがクリーンアップされる
- おまけに、hot reloadingをfast refreshと結び付けやすくなった(衝突形状の調整に本当に便利)
- Reactコンテキスト方式のもう1つの利点:
- 物理フックは
<PhysicsContext> の内部にないとnoopになる
- 審査UIで静的なタイルプレビューをレンダリングするのに使えた
- Rapierオブジェクトの生成には、フックではなくコンポーネントを使えばよかった(react-three-rapierが採っている方式)
- Reactのdiffingにより適している(依存関係が変わると
useEffect は以前のインスタンスを削除して再生成する)
- Machineは完全にDOMを使ってレンダリングされている
- 開発初期には、性能面でDOMレンダリングの限界に達するのではないかと心配していた
- 遅すぎるならPixiJSやcanvasへ切り替えるつもりだったが、DOMをどこまで活用できるか試してみたかった
- レンダリング性能の最適化:
- フレームループが物理シミュレーションを持つウィジェットに直接スタイルを適用
- Reactのdiffはscene graphに構造的な変更があるときだけ実行される
- 当初は玉をReactで
1件のコメント
Hacker Newsの意見
複数の意見を総合すると、次のように要約できる:
Rapier物理エンジンを使用していたが、再帰的なエラーによってクラッシュが発生することもあった