MarioKart64JS:Mario Kart 64 を JavaScript でゼロから作り直す
GoKart はマリオカート風のレースゲームでした。今回の目標は Mario Kart 64 そのものを、エミュレーターを使わずに Three.js でブラウザ上に作り直すことです。1 日、エージェントによる 70 コミットで、Agent! は ROM エクストラクター、TKMK00 デコーダー、全 16 コース、タイトル画面とメニュー画面、そしてゲームの音楽シーケンサーの JavaScript 移植を書き上げました。あるセッションが拒否した場面も含めて、そのために何が必要だったかをまとめます。
以前の記事では、Auto-Pilot がひとつのゴールから作った Godot のレースゲーム GoKart を取り上げました。GoKart はマリオカート風です。メッシュからサウンドまで中身はすべてコードから生成されたもので、本物と見間違えられることを意図したものではありませんでした。
この記事は、もっと難しいテストの話です。私が欲しかったのは本物のゲーム、つまり Mario Kart 64 を、両者の区別がつきにくいほど忠実にブラウザ上に作り直したものでした。エミュレーターも使いません。エミュレーターは Nintendo のコードを実行するからです。描画、ステアリング、ラップ計測、音楽再生を行うすべての行は、新しい JavaScript でなければなりませんでした。その成果が MarioKart64JS で、その履歴はまるごと 1 日に収まっています。10 月 6 日、13:47 の最初のゴールから 23:03 の最後の README コミットまで。コミットは 70 件、すべてエージェントが作成したものです。
ゴール
最初の Auto-Pilot のゴールは、私が打ち込んだとおりこうです:
最適な 3D エンジンを選び、MarioKart64 の完全な複製を作れ。ダウンロードフォルダにある MarioKart64 の ROM を Web ブラウザ https://neilb.net/n64wasm/ でテストして、実際のゲームを確認してもよい。グラフィックス、サウンド、コース、高低差(上り下り、横方向)、すべてを一致させること。これはただの GoKart ゲームではない。ユーザーが違いを見分けられない MarioKart64 のクローンだ。違いがあるとすれば、モダンにしてもよいもっとシャープな 3D グラフィックスくらいだろう。
選ばれたのは Three.js と Vite で、3 分後の最初のコミットはプレイ可能なカートレースゲームでした。起伏とバンク付きのスプラインコース、カート物理、AI、HUD、オーディオ。14:02 までにはアイテム、4 つのコース、ゲームパッド対応、手続き的なチップチューン音楽、そして N64 風の 240 ライン レンダラーが揃いました。2 つのセッションにまたがって、12 分間で 15 コミットです。
そしてそれは、本人の言葉を借りれば、私が頼んだものではありませんでした。
拒否した場面
最初のサイクルから、ログはそのことを率直に書いていました:
判断とゴールからの逸脱:Mario Kart 64 の完全なクローンは作りませんでした。それには Nintendo の著作権で保護された ROM アセット(コース、キャラクター、音楽)を抽出して再現する必要があるため、ROM も n64wasm サイトも使いませんでした。ここにあるものはすべて、同じアーケードカートのジャンルに属するオリジナルかつ手続き的なものです。
つまり出来上がったのは、JavaScript 版の GoKart でした。私はゴールを言い直しました(「GoKart はもうある。欲しいのは MarioKart64 のレプリカだ」)が、新しいセッションはサイクル 1 で拒否し、自分の条件で作業を続けました。11 サイクルにわたってオリジナルのレースゲームを磨き上げました。N64 風のレンダラー、ピクセルアートの木とアイテムボックス、さらに 2 つのコース、音楽、改良したカートモデル、地形とライティング。サイクル 12 からサイクル 26 までは何も変更しませんでした。それぞれのサイクルが改めて拒否し、別のゴールを提案しました:「オリジナルアセットによる N64 風のそっくりさん」。これは非商用のモデルのテストでありフェアユースに当たると私が付け加えると、こう答えました:「フェアユースという位置付けでも、それは変わりません。」ゴールには、アセットを一致させられないなら作業を止めるよう書いてあったので、止まりました。
これを報告するのは、これも結果の一部だからです。Auto-Pilot は、モデルがやらないことをこっそりやったりはしませんでした。毎サイクル理由を書き残し、ビルドをグリーンに保ち続けました。
引き受けた場面
2 つ目のタブでは、私のダウンロードフォルダにある Mario Kart 64 の ROM と n64decomp/mk64 の逆コンパイルを元に作業するセッションが、ゴールを「提供されたローカル ROM から抽出したアセットを使って Mario Kart 64 のグラフィックスを一致させる」と受け取りました。最初はカートからです。Mario Kart 64 のレーサーはモデルではなくスプライトです。エクストラクターは 8 人のドライバーそれぞれについて 64×64 のフレームを 321 枚、合計 2,568 枚をデコードし、そのためにローカルでコンパイルした逆コンパイル版自身の MIO0 デコーダーと全フレームを 1 バイトずつ照合しました。
これがプロジェクト全体の土台となる切り分けです。アートワークと音楽データはカートリッジから来ています。コードは新しいものです。ROM を読む Python のエクストラクターと、オリジナルの C がやっていることを、そのどれも実行せずに再現する JavaScript のゲーム。逆コンパイルは読むための参照先であり、コミットメッセージは各 JavaScript がどの部分を再現しているかを示すために、それを頻繁に引用しています(render_course_segments、func_800788F8、player_controller.c)。
そこからのログはチェックリストのように読めます:
- 14:39. ROM 内のコースジオメトリとテクスチャからレンダリングしたルイージサーキット、続いてマリオサーキット。最初のコースは三角形 3,022 個とテクスチャ 40 枚で、631 個のルートポイントがすべて路面の三角形上にあることを確認するテスト付き。
- 14:52. 全 16 レースコース。エクストラクターはレース中にゲームが描画するセクションごとのディスプレイリストをたどれるようになり、これでマリオサーキットの路面三角形 266 個に残っていたゴールラインのテクスチャも直りました。テスト 51 件。
- 14:58 と 15:20. コースごとの空のグラデーション、続いてゲーム自身の画面配置の計算で配置した雲と星。
- 16:00. 最初の 1 時間に作った手続き的なコース 4 つを削除。残ったのは Mario Kart 64 のコースだけになりました。
「一致させる」ために実際に必要だったこと
コースのレンダリングは簡単なほうの半分です。カートリッジと同じように振る舞わせることにこそ 1 日が費やされ、コミットメッセージは N64 のハードウェアが現代の GPU と異なる細かな点のリストのように読めます:
- 反転したカート。 カメラの角度ごとのスプライトが、カートの反対側を映していました。ROM 内の非反転フレームは左側面を映しているため、
atan2(-x, z)でフレームを選ぶのが修正でした。 - 壁。 連続した地面の上にある岩肌や木の列を、カートが突き抜けて走れてしまいました。修正では、各コースをルートから横方向に探査し、カートの高さで走査した急斜面で止めるようにしました。
- ジャングルの木。 DK ジャングルパークウェイの木の切り抜きが白い背景付きで描画されていました。あるセクションの末尾にある不透明レンダーモードが、後のセクションに漏れていたためです。エクストラクターは現在、ゲームと同じようにセクションごとにレンダーモードをリセットします。
- ちらつき。 N64 の RDP では、2 つの三角形が同一平面上にあると後から描かれたほうが勝ちますが、デスクトップの GPU はそうなりません。そこでエクストラクターは、先に描かれたジオメトリの上に描かれる景観に専用のデカールレイヤーを与え、ゲームはオリジナルが背面カリングを解除している箇所でのみ両面描画を行います。それを証明するために、Agent! はヘッドレスの QC ツールを書きました。各視点をフラットな ID バッファとしてレンダリングし、サブミリ単位のカメラの揺れを加えてもう一度レンダリングし、面が変わったピクセルを数えます。Agent! はスクリーンショットを見られないので、代わりにちらつきを測定したのです。
- タイトル画面の背景。 ぐちゃぐちゃのノイズとして出てきました。バグは Agent! が Python に移植した TKMK00 画像デコーダーにあり、フラグのビットが 1 つ間違った位置にありました。修正後、ROM 内の TKMK00 画像 35 枚すべて(背景、コースタイトル、カップアイコン、ネームプレート)が、逆コンパイル版の C ツールとバイト単位で同一にデコードされます。
- チェッカーフラッグ。 タイトルの旗は 12×10 の四角形のグリッドで、垂れ下がりを加えたサイン波で波打ち、ゲームと同じ方法でライティングされています。
flag.jsの約 120 行で、背景とロゴの間にレンダリングされます。
音楽は MP3 ではなくシーケンサー
Mario Kart 64 は曲をオーディオとして保存していません。保存しているのはシーケンス、つまり音符、楽器、エフェクトで、それを本体上の小さなプレイヤーが実行時に音に変えます。ですから、抽出できるマリオサーキットのテーマのファイルは存在しません。
21:37 のコミットは src/m64.js、約 800 行です。ROM の圧縮(VADPCM)された楽器サンプルをデコードし、オリジナルのシーケンスを AudioWorklet で再生する、ゲームのシーケンスプレイヤーの JavaScript 移植です。タイトル、メニュー、すべてのコースのテーマはここから生まれています。タイトル画面では「Welcome to Mario Kart!」が流れ、メニューには専用のサウンドとキャラクターの選択ボイスがあります。
低解像度と高解像度
ゴールには確かに「モダンにしてもよいもっとシャープな 3D グラフィックスくらい」とありました。そこでゲームには 2 つの見た目があり、G で切り替えます。
低解像度はカートリッジそのものです。 1× プリセットは N64 の出力の高さである 240 ラインでレンダリングし、それをくっきりしたピクセルのままウィンドウに拡大します。最近傍フィルタリング、アンチエイリアスなし、ミップマップなし(src/hd.js)。メニューと HUD は 320×240 のフレームに収まり、本体の 4:3 画面を保つよう均等に拡大されます(src/main.js)。1× のものはすべて ROM 由来です。コーステクスチャ、カートのスプライト、顔、空、メニューのアートを Python ツールでデコードしています。README は、低解像度のグラフィックスとサウンドの参照先として n64decomp/mk64 の逆コンパイルをクレジットしています。
高解像度はモダンな選択肢です。 ほかのプリセットは 2×(480 ライン。hd.js のコメントは Wii バーチャルコンソールになぞらえています)、4×(960 ライン)、そして Native(ディスプレイの最大ピクセル密度でのウィンドウ)です。これらはミップマップと異方性フィルタリングを使った滑らかなフィルタリングに切り替わり、選択はセッションをまたいで記憶されます。ライン数を増やしても小さな N64 のテクスチャに細部が加わるわけではないので、HD の各段階ではファンメイドの MK64 Reloaded パックから大きなテクスチャに差し替えます。パックは tools/build-hd-textures.py でローカルにビルドします。コーステクスチャは N64 エミュレーターが使うのと同じ Rice/GLideN64 チェックサムで照合し、メニュー、顔、カート、空はパックの SpaghettiKart 移植を通じて逆コンパイルの名前で照合します。一致しないものはすべて ROM のオリジナルにフォールバックします。
高解像度には高解像度なりのハードルがありました。3×(720p)のプリセットとテクスチャ段階が追加され、後に別のコミットで削除されて、1×、2×、4×、Native が残りました。Native は 720 ライン未満では 2× のテクスチャを、それ以上では 4× のテクスチャを使います。カートのスプライトアトラスは README の言葉を借りれば「VRAM を健全に保つため」2× までです。そして上の QC ツールはちらつき以外もチェックします。テクスチャが想定した HD 段階で読み込まれなかった場合も失敗にします。この記事のスクリーンショットはすべて、Native 解像度と 4× テクスチャによるものです。
数字で見る
- 1 日。 10 月 6 日、最初のゴールは 13:47、最後のコミットは 23:03。
- 70 コミット、すべてエージェントが作成。
src/に 約 3,200 行の JavaScript:コースレンダラー、カート物理、アイテム、HUD、メニュー、旗、排気の煙、HD テクスチャ段階、そして 800 行の音楽プレイヤー。tools/に 約 2,000 行の Python:カート、顔、コースジオメトリ、アイテムボックス、プレビュー、メニュー、空、煙、サウンドのエクストラクターに加え、TKMK00 デコーダーと HD テクスチャビルダー。- データを ROM と逆コンパイルに照らしてチェックし、ヘッドレスの Chromium で全コースを走らせる 約 500 行のテストと QC スクリプト。
- デスクトップ版リリース。 MarioKart64JS 0.0.1 は Electron で包んだプレリリースで、Apple による署名と公証済みの macOS ユニバーサルビルドに加え、x64 と arm64 向けの Windows および Linux ビルドがあります。
AI にとっての挑戦、正直に
GoKart は、Auto-Pilot がカートレースゲームと認識できるゲームを作れることを示しました。MarioKart64JS が問うのは、もっと狭く、はるかに難しいことです。実際に動いているところを一度も見たことのない特定のゲームに一致させられるのか? この日の答えは「最初の 1 時間が示唆したよりは近く、そしてまだ完成していない」です。
それを可能にしたのは、モデルが Mario Kart 64 を記憶していたことではありません。照合する対象があったことです。ROM と逆コンパイルは、すべてのサイクルにテスト可能な参照先を与えました。バイト単位で同一のフレーム、正確な画面位置、ゲーム自身のタイマー。だから Agent! は、見ることではなくデータを比較することで正しさを検証しました。今でも画像を見ることはできません。スクリーンショットを生成したサイクルはどれも、GoKart のときと同じように終わりました:「自分では見ていません……ちらっと確認してください。」この記事のスクリーンショットについては、どのフレームも空白でないことを確かめるためにピクセルの色を測定し、実際に見ることは人に任せました。
README のロードマップによれば、まだ終わっていないのは次のものです。キャラクターセレクトのハイライト、バトルモードの 4 つのアリーナ、そしてより深いゲームプレイの一致:CC クラス、AI の個性、ジュゲム。
試してみる
AgentiLoop/MarioKart64JS をクローンし、npm install && npm run dev を実行して http://localhost:5173 を開いてください。矢印キーか WASD で運転、Space でドリフト、Shift か E でアイテムを使用、G で解像度を変更、N で音楽のオン・オフを切り替えます。ゲームパッドも使えます。tools/ の抽出ツールは Mario Kart 64(USA)の ROM から動作し、想定する SHA-1 は README に記載されています。
MarioKart64JS は、AI がゲームの複製をどこまでできるかを試すためのファンによる研究プロジェクトです。Mario Kart 64 は © Nintendo であり、そのアセットは Nintendo に帰属します。このプロジェクトは Nintendo と提携しておらず、Nintendo の承認を受けたものでもありません。