問い合わFastly 無料トライアル

Compute@Edge : 名作ビデオゲーム『DOOM』を移植する

id Software 社の『DOOM』、その移植性の高いコードベースとクリーンな抽象化のため、ゲーム史上最も多く移植されたゲームの1つとされています。Fastlyサーバーレスコンピューティング環境に構築された Compute@Edge機能を試すべく、プラットフォームに『DOOM』を移植する実験を行いました。 

『DOOM』 Compute@Edge 上でインタラクティブに動作することがわかれば、プラットフォームの新境地を開拓することができると考えました。具体的なデモを作成することで、Compute@Edge がもたらすエキサイティングな可能性を紹介することが今回の目的でした。では、移植の手順について説明します。

『DOOM』のこれまでの歴史

『DOOM』1993年に id Software 社が開発し、同年12月に発売したゲームです。高品質な 2D ゲームの開発を専門としていた id Software は、1992年の『Wolfenstein』、そして翌年の『DOOM』の公開で 3D ゲームへと画期的な飛躍を遂げ、PC ハードウェアの急速な進化の波に乗り、ゲーム業界の限界を押し広げました。

『DOOM』1997年にオープンソース化され、README ファイルには「お好きな OS移植してください」と書かれていました。以来『DOOM』は多くのファンによって、何百もの多種多様なプラットフォームに移植されてきました。『DOOM』のファンであると同時に Fastly 社員でもある私は、ぜひこのゲームを使って Compute@Edge可能性を試してみたいと思いました。今回は、この名作ビデオゲームを Compute@Edge移植した方法を紹介します。

このプロジェクトを行うにあたって、Fabien Sanglard 『Game Engine Black Book』という本が貴重な資料で、とても参考になりました。彼は『Wolfenstein』関する著書も出版していますが、両方ともゲーム開発の歴史における重要な出来事を深く研究された内容で、非常に参考になる面白い本です。

移植

『DOOM』移植は、次の2ステップで行いました。

  1. プラットフォーム非依存のコード (つまり、特定のアーキテクチャやプラットフォームのシステムコール、SDK依存しないコード)コンパイルして実行する。これが一般的な「ゲームプレイ」の大部分にあたります。

  2. 必要に応じ、プラットフォーム固有の API コールをターゲットプラットフォーム用に置き換える。これは、主にレンダリングやオーディオなどの入出力を扱うコードです。

C バインディングの公式な公開インターフェースはないので、自宅で試す場合は fastly-sys クレートから C API逆引きする必要があります。

共通コード

レンダリングやオーディオなしで『DOOM』を Compute@Edge動作させること自体は、比較的簡単でした。コードベースにはすべての関数名にプリフィックスがついており、実装に特化した関数には「I_」使われているため、コードベースを調べ、コンパイルからこれらを削除することは難しくありませんでした。それが終わったら、wasi-sdk使って Wasmバイナリをターゲットにしました。WebAssemblyネイティブコードを手間をかけずにコンパイルするように設計されているため、この変更は非常に簡単でした。

ゲームを WebAssembly バイナリとして動作させるために修正が必要だった背景には、『DOOM』32ビットコンピューティングの時代に開発されたことがあります。ポインタが4バイトであると仮定してコードが書かれている箇所がいくつかありますが、これは当時は完全に合理的な選択でした。『DOOM』のデータは、リリース時に開発チームが作成しまとめた、すべてのアセットを含むファイルから読み込まれます。このデータは直接メモリに読み込まれ、ゲーム内の C 構造体にキャストされます。これら構造体にポインタが含まれている場合、64ビット環境でデータをロードすると、構造体に正しオーバーレイされず、予期しない動作が発生します。これらの問題を突き止めることはごく簡単でしたが、最初は明らかなクラッシュが何度か発生しました。

ゲームループの変更

共通のコードを Compute@Edge 上で実行するには、『DOOM』採用していた従来のゲームループをリファクタリングする必要がありました。一般的なゲームは、初期化後にキーボードやマウス、コントローラなどのローカルなデバイスからの入力を受けて映像や音声を出力するという、入力シミュレーション出力を任意の頻度で繰り返し行う無限ループで動作します。しかし、Compute@Edge ではインスタンスが起動して作業を行った後、呼び出し元に戻ることが目的となっているため、このようなプロセスは最終的にプラットフォームによって削除されてしまいます。そこでループを完全に削除し、インスタンスがゲームの1フレームの実行するように変更しました。 

結果的に、以下のようにループで実行されるようになりました。

Screen Shot 2021-03-31 at 3.33.38 PM

次のセクションでは、それぞれのステップについてさらに詳し説明していきます。

出力

ビデオゲームではプレイヤーに表示される最終的な画像を保存するメモリをフレームバッファ呼びます。最近のゲームでは、フレームバッファは専用の GPU ハードウェアで構築されていること多く、最終的な画像は GPU 上でいくつものパイプラインステップを実行した結果として得られることが多くなっています。しかし1993年当時、レンダリングはソフトウェアで行われており、『DOOM』の最終的なバッファは基本的な C 言語の配列でプログラマーが利用できるようになっていました。このシンプルで分かりやすい設計のおかげで、開発者は比較的容易に『DOOM』を新しプラットフォームに移植することができたわけです。

Compute@Edge場合、フレームバッファをプレイヤーのブラウザに返して表示させたいと考えました。そのためには、C API使ってフレームバッファをレスポンスボディに書き込み、それをダウンストリームに送るだけの簡単な手順でした。

// gets a pointer to the framebuffer
byte* framebuffer = GetFramebuffer(&framebuffer_size);
BodyWrite(bodyhandle, framebuffer, framebuffer_size,...);
SendDownStream(handle, bodyhandle, 0);

ブラウザで動作しているクライアントが Compute@Edge からの http レスポンスを受信すると、フレームバッファが解析され、ブラウザでレンダリングされます。

ステート

この新しモデルでゲームループを再現するには、後続のフレームを Compute@Edge から取得する際にゲームのどの地点にいるかを新しインスタンスに伝えるために、どこかにステートを保存する必要がありました。これには『DOOM』のセーブロード機能を利用することができました。この機能は、プレイヤーがゲームのステートをディスクに保存し、後ほど中断した所からゲームプレイを続けることができるように、もともと存在していたものです。

ステートの保存には、フレームバッファと同じ仕組みを使用しました。ゲームフレームの終わりにセーブシステムを呼び出してゲームのステートを表すバッファを取得し、http レスポンスを呼び出し元に返す際に、フレームバッファにピギーバックさせました。

// gets a pointer to the framebuffer
byte* resp = GetFramebuffer(&framebuffer_size);
// gets the gamestate, appends it to the framebuffer
resp+fb_size = GetGameState(&state_size);
BodyWrite(bodyhandle, framebuffer, framebuffer_size + state_size,...);
SendDownStream(handle, bodyhandle, 0);

この変更に伴い、フレームバッファとステートを分離し、ステートはローカルに保存、フレームバッファはブラウザに表示するようにクライアントを変更しました。次回 Compute@Edgeリクエストを発行する際には、リクエストボディで渡されたステートを Compute@Edgeインスタンスが読み取り、以下のようにゲームに渡すことができます。

BodyRead(bodyhandle, buffer,...);
LoadGameFromBuffer(buffer);

この時点でゲームフレームを実行すると、まるでゲームのステートを保存した直後のように実行されます。

入力

次に必要なのは、実際にゲームをプレイするためのユーザー入力です。『DOOM』の入力システムは、入力イベントという概念で抽象化されています。例えば「プレイヤーが W キーを押した」とか「プレイヤーがマウスを X 方向に動かした」などです。標準的な Javascriptイベントリスナーを使って、『DOOM』対応できる入力イベントをブラウザ上で簡単に生成することが可能です。

document.addEventListener(‘keydown’, (event) => {
// save event.keyCode in a form we can send later
});

これらの入力イベントは、Compute@Edge への http リクエストの際に、ステートと共に送信されます。するとインスタンスは、フレームを実行する前に、入力イベントをゲームエンジンに渡すことができる形式に解析します。

最適化

このデモの最初のバージョンは、1往復あたり200ミリ秒で動作しました。これは、インタラクティブなゲームとしては許容範囲外です。一般的なゲームでは33ミリ (30 FPS) または16ミリ (60 FPS)動作します。レイテンシは更新頻度に大きな影響を及ぼすため、最初のバージョンより4倍早い50ミリ秒を目標にすることにしました。

今回ゲームを最適化する際に注目したポイントは、連続したゲームループの実行から1フレーム単位の実行へ変更することでした。ゲームシステムの多くは、ティックがフレームの差分であるという概念に基づいて設計されています。セーブゲームには取り込まれないものの、判断を下すためにフレームごとに使用されるステートが、ゲームによって保持されます。システムの機能性とパフォーマンスのため、これらのシステムではある程度の調整を行う必要がありました。またシステムの多くは、最初のフレームの状態 (つまり多くの変数やステートが初期化されている状態) ではないかのように動作している場合に、最も効果的に機能しました。  

またゲームでは起動時に多くの事前計算が行われるようになっていました。そのほとんどが、三角法を使ったビュー空間とワールド空間の計算でした。事前計算テーブルはゲームの画面解像度の情報が必要だったため、ランタイム時に計算が行われていた訳です。今回はレンダリング解像度を固定していたため、テーブルをコンパイル済みのバイナリに埋め込むことで、フレームごとの起動時の計算を回避することができました。

最終的には1ティックあたり50 - 75ミリ秒での動作が可能になりました。本来の『DOOM』に近づけるためにはまだ改善が必要ですが、Compute@Edge でこのようなプロジェクトを反復して行うことが可能であることを証明することができました。

今回のポイント

今回は私にとって初の Compute@Edge試みだったこともあり、手探りの状態でデバッグや反復作業に臨みました。Compute@Edge では断続的に、そして積極的に改善が進められています。私がこの作業に取り組んだ3週間の間でも、デプロイの信頼性やデバッグのしやすさなど、様々な面での改善が見られました。特に便利だったのは『DOOM』から出力されるプリントをほぼリアルタイムで見ることができる Log Tailing 機能でした。まだレンダリングがうまく動作していない時点での不透明な C プログラムでの反復作業において、この機能がデバッグ作業で非常に役立ちました。全体として Compute@Edge へのデプロイは、従来のビデオゲーム機などでの作業に似ていたと思います。

正直Compute@Edge頻繁なゲームアップデートを必要とするリアルタイムゲームの実行に理想的なソリューションだとは言えません。今回の方法で『DOOM』のようなゲームを実行することで得られるメリットは特にありませんでした。ですが、今回のプロジェクトの目的はプラットフォームの限界に挑むことでした。Compute@Edge可能性の限界を試し、興味深いデモを作成することで、プラットフォームに関心を持ってもらい、インスピレーションを与えることが今回の目標でした。ビデオゲームを Compute@Edge移植した利用事例は他にも存在すると思いますが、今後もより多くの企業に興味を持っていただけるよう、このプラットフォームが秘める可能性を紹介していきたいと思っています。

興味がある方は、ぜひ Developer Hubデモを試しくださ

Justin Liew
Senior Software Engineer
投稿日
興味がおありですか?
エキスパートへのお問い合わせ
この投稿を共有する
Justin Liew
Senior Software Engineer

Fastly Senior Software Engineer として Justin Fastly サーバーレスコンピューティング環境 Compute@Edge 中核となるキャッシュソフトウェアの開発に取り組んでいます。18年に及ぶゲーム業界での実績を持つ Justin は Fastly 入社前FIFAGears of War」、「Don't Starveをはじめとする人気ゲームの開発に従事しました。余暇に自宅のあるカナダの美しブリティッシュコロンビア周辺を家族と一緒に散策するのを楽しみにしています。