- 『Caves of Qud』は長年のUnity依存を取り除き、Godot上でゲームコアをビルド・起動する技術検証を進めており、当面の目標はモダンなVFXやUIなしでASCII+タイルモードで動作させること
- 作業はタイルアセットの取り込み、コアC#アセンブリの移植、レンダリング・入力リグの構築に分かれており、古いBMPタイルファイルはPNGに変換してGodotへ読み込ませた
- 初期ビルドの5,641件のエラーは、GeneratedCode、ConsoleLib、Genkit、Language、HistoryKit、Newtonsoft JSON、CodeDomなどを移しながら減っていき、UnityEngine依存の表面はスタブで狭めていった
- UnityのColor、GameObject、AudioSource、Debug.Log、Screenの使用箇所はUnityEngineReplacerと代替クラスで処理し、PlayFabやUnity UIに近いchargen・presentation層は一時的に外すか、glueモジュールとして分離候補に残した
- 結果として約50万行のC#ゲームコアがGodot上で起動し、フレームを供給して入力待ちになる段階に到達したが、レンダラー・入力ハーネスやVFX・サウンド・UIのリギング作業はまだ残っている
UnityからGodotへ移す作業範囲
- 移植作業は3つに分かれる
- アセット取り込み: タイルアセットをGodotプロジェクトに入れる作業
- コアアセンブリ移植: Unityではないメインゲームエンジンである
XRL Applicationと、エンジン側のGameManagerを移す作業
- レンダリングリグ構築: コア起動後に画面出力と入力を接続する作業
- Godotの“mobile”プロジェクトを作成し、Qudのテクスチャコンテンツをプロジェクトのルートフォルダにコピーした
- Godotが古い**
.bmpファイル**を扱えないため、ASCIIタイルを含むファイル群を一括でPNGへ変換した
- 変換後はアセットを読み込めたが、インポート進行表示が出るまで約30秒かかった
ビルドエラーを減らしながらコアアセンブリを移す
XRL Applicationをコピーした後、GodotでC#プロジェクトとソリューションを生成し、初期状態では5,641件のエラーが発生した
ConsoleLibやGenkitのような汎用ライブラリは丸ごと移し、古いスプライト・アトラシング用ソリューションのKoboldは、ファイルごとに必要性を確認しながら取り込むことにした
KoboldJSONは単純なJSONシリアライズライブラリなので、そのまま移した
Colorの使用箇所85件を全体置換したことで、エラーは4,700件まで減った
- 抜けていた
GeneratedCodeフォルダを追加すると、イベント別クラスのコード生成結果が含まれ、エラーは1,557件まで減少した
- Caves of Qudは、ボイラープレートの多いイベント別クラスをコード生成し、性能と強い型付けのイベント表面を得る構造になっている
Unity依存とglue層の整理
Embark Builderとキャラクター作成chargenにはUnity UIコードが多く、初期のASCII技術検証では削除した
- コンソール版はまだないが、初期検証はセーブデータのロードやランダム開始でテストできる
Gameフォルダは大部分がUnityとゲームの間のglueだが、CodeGenerationのようにglueではないフォルダもあるため、別ルートやPlatformフォルダへ移す方針を取った
LanguageとHistoryKitライブラリを移し、エラーは454件まで減った。完全ではなくてもgame/glue分離ができていたことが助けになった
- 残る大きなエラー分類は、
CodeDom/Roslyn、PlayFab、Harmony、一部のコンパイラ関連問題、そしてゲーム層へ漏れ出したUnityEngine表面だった
PlayFab関連コードは一時的にコメントアウトし、glueモジュールへ上げる候補として残した
Unity API代替スタブとGodot C#の違い
Color32はbyteベースの色型なので、すぐに代替実装を書いた
UnityEngineReplacerフォルダを作り、Unityのドキュメントやコード参照なしに、コンパイルエラーを見ながら必要な表面だけを実装した
GameObjectスタブを作ると、「GameObjectが分からない」というエラーが実際に必要なフィールドやメソッドのエラーへ変わり、ゲームが使っているインターフェース表面を把握できた
AudioSourceもエラー一覧を見ながらアクセスされるメンバーを1つずつ追加し、ゲームが実際に使う完全なAudioSourceポートインターフェースを完成させた
- そのほかに処理した表面は以下の通り
- **
Debug.Log**の代替shim
Screenの代替実装
IsNullOrEmpty拡張メソッドライブラリの移植
Math参照とPI/Piの違いへの対応
- Godotの
X、Yという大文字座標名の違いへの対応
System.Drawing.Color、System.Numerics.Vector3がIDEによって誤ってimportされる問題の整理
- Newtonsoft JSONはVisual StudioでNuGetパッケージを追加して解決し、CodeDomも
CodeDom NuGetパッケージで解決した
Godotで完全起動するまで
- エラーが11件まで減った時点で残っていた問題は、mod managementの動的C#コンパイル関連コードで、必須ではないが複雑なため最後まで残った
- その後、リンク段階とスタブフィールドのエラーを処理し、約**50万行のC#**コードがビルドできた
- 次の段階は小さなレンダラーと入力ハーネスを作ってコアアセンブリを起動することで、目標はASCII+タイルモードでゲームを実行可能にすることだった
- Godotで空のsceneを作り、基本nodeに
GameManager.csを付けた。GameManagerはNodeを継承し、partialコードが必要だった
- Godotの
_ReadyはUnityのAwake、_ProcessはUnityのUpdateに対応する形で使った
- 初期化過程ではload pathとmod managementを移し、型resolverが名前で型を見つけられない問題をprintf方式で追跡した
- 原因はdynamic assemblyの処理だった
- mod assemblyがdynamicのため、main assembly検査でdynamic assemblyを除外していた
- Godot editorではmain game assemblyもdynamicのため、型探索対象から外れていた
- 修正後、完全起動に成功し、50万行規模のゲームコアがフレームを供給しながら入力待ちする状態になった
- 残作業は“just work”と表現されたが、実際にはVFX、サウンド、UIリギングにかなりの作業量が残っている
1件のコメント
Hacker Newsのコメント
このゲームはほぼ全て独自エンジンを使っていて、Unityはハードウェア抽象化レイヤーと移植用の枠組みとしてだけ使っているようなので、移植の難易度という面では最良に近いケースに見える
こういうゲームは意外と多いけれど、もちろん大半のUnityタイトルを代表するものではない
UI刷新後もまだそうなのかは分からないが、もしそうなら、MonoGameに移植してコストを抑えなかったのがいつも不思議に思えた
Androidも似ていて、OSはカメラ検出や暗号化のようなものを置き換えるライブラリを呼び出す用途にしか使われなくなり、ある日、OSは実質的に薄い金属層とブートローダーにすぎないのだと気づくことになる
https://nitter.net/unormal/status/1703163364229161236
くそ、Elon。もうTwitterを扱うのが本当にうんざりだ
うわあ、本当に素晴らしい。この移植プロセスを段階ごとに見られるのも良いし、かかった時間もかなり妥当で驚いた
カスタムコードが非常に多く、エディタ機能をあまり使わない構造なら、エンジンの代わりにレンダリングライブラリを使ってもよさそう
土台にエンジンがあれば、こうしたものを無料で手に入れられる。Unityなら費用が発生するかもしれないが
Brianがどう考えているのか気になるなら、この動画を見るとよい: https://www.youtube.com/watch?v=U03XXzcThGU
以前ここで多くを学んだ
最近のDHHのTypeScript論争を思い出す
静的型付けなしでこういう移植をすると想像してみると、変更のたびにビルドして実行し、クラッシュを探さなければならない。こういう時には、リファクタリングしやすい言語と良いアーキテクチャで作っておいたことが本当にありがたくなる
Twitterアカウントを持っていないんだけど、何が起きているの?