広告枠を募集中 ご相談ください

UnityゲームModding構造解説:BepInExにおけるMonoとIL2CPPの動作差、Harmonyフックとアセンブリ書き換えの仕組み

365Finds Editorial
Wiki 記事 11,430 本収録

あなたの AI で 365 Wiki を使う

下のプロンプトをコピーして、普段使っている AI に貼り付け、そのまま質問してください。

完全版 Skill をダウンロード

まずはプロンプトでお試しください。よく使う場合は完全版 365Wiki Skill をインストールすると、毎回貼り付ける必要がありません。普段使う AI がなければ、Gemini や Microsoft Copilot のウェブ版でも使えます。

はじめに:UnityエンジンにおけるModdingの動作原理

Unityゲームエンジンは、マルチプレイヤーサバイバルゲームの『Valheim』や『Lethal Company』から、高度なシミュレーションゲームである『Cities: Skylines II』や『Dyson Sphere Program』に至るまで、現代のインディーおよびミドルクラスPCタイトルの膨大なシェアを占めています。独自のバイナリやBethesda製エンジンのプラグインレコード(esp/esm)に依存する従来のModdingとは異なり、Unity作品のModdingは.NETおよびC#のソフトウェアエコシステムと深く結びついています。

Unityゲームを改変する際、ディスク上のオリジナルリソースファイルを直接上書きすることは稀です。現代の主流な手法は、実行時におけるインメモリのアセンブリ動的書き換えとメソッドフック(In-Memory Method Hooking)です。このエコシステムの中核を成しているのが、オープンソースのプラグインブートストラップフレームワークであるBepInEx(Bepis Injector Extensible)と、Andreas Pardeike氏によって開発された動的パッチライブラリHarmonyです。

しかし、Mod製作者やプレイヤーは、異なるゲーム間でModを導入しようとする際に、起動直後のクラッシュやフックの不発といった互換性トラブルに直面しがちです。その根本原因は、Unityが採用するコンパイル方式、すなわち従来のMonoバックエンドと先行コンパイルされたIL2CPP(Intermediate Language to C++)バックエンドという決定的なアーキテクチャの断絶にあります。

スクリプティングバックエンドの分岐:MonoとIL2CPPの内部構造

UnityプロジェクトをビルドしてPC向け実行ファイルを出力する際、開発者は2つの根本的に異なるスクリプティングバックエンドから一方を選択します:

【C#ソースコード:ゲームのメインロジック】
             │
             ▼
【Roslyn / Unity C#コンパイラ】 ---> 共通中間言語 (CIL バイトコード)
             │
             ├─────────────────────────────────────────┐
             ▼                                         ▼
     【Mono バックエンドの経路】              【IL2CPP バックエンドの経路】
- CILをマネージドアセンブリ(DLL)として保持   - IL2CPPがCILをC++コードへと全量トランスパイル
- Assembly-CSharp.dll 等をそのまま出力    - ネイティブコンパイラが GameAssembly.dll を出力
- 実行時にMono JIT仮想マシンがバイトコード実行 - メタデータを別個の global-metadata.dat に分離
- 完全なリフレクション情報、逆コンパイルが容易 - 純粋な機械語ネイティブバイナリ、CLR仮想マシン不在

Monoスクリプティングバックエンド

Monoモードでビルドされたゲームでは、ゲームプレイロジックやAIスクリプトなどのすべてが、標準的な.NET中間言語(CIL)としてコンパイルされ、Managed/Assembly-CSharp.dllなどのDLLに格納されます。

IL2CPPスクリプティングバックエンド

一方、IL2CPPでビルドされたタイトルでは、ビルドパイプラインが根本から異なります:

BepInExブートストラップの仕組みと動的プロキシ注入

保護されたネイティブなUnityプロセス内に外部Modコードを読み込ませるため、BepInExはWindowsの**DLLプロキシハイジャック(DLL Proxy Hijacking)**を活用しています。

Doorstopによるプロキシ読み込みチェーン

Windows OSは実行ファイルが必要とする動的リンクライブラリ(DLL)を検索する際、システムディレクトリ(C:\Windows\System32)よりもアプリケーション自身の直下ディレクトリを最優先で探索します:

  1. 偽装プロキシDLLの配置:BepInExはゲームの実行ファイルと同じフォルダに、OS標準ライブラリと同名の偽装DLL(winhttp.dll、version.dll、あるいはdoorstop.dll)を配置します。
  2. メインスレッドの乗っ取り:OSローダーがゲームを起動する際、正規のシステムライブラリではなく、ローカルの偽装winhttp.dllが優先的にメモリへロードされます。
  3. ランタイムの起動:偽装DLLはUnity Doorstopを呼び出し、Unityエンジンのメイン初期化処理が開始される前に、BepInEx.Preloader.dllおよびBepInEx.Core.dllをプロセスメモリ空間へと割り込ませます。
  4. プラグインの探索と実行:ランタイム環境の構築後、BepInExはデバッグコンソールを起動し、ログ収集パイプラインを初期化して、BepInEx/plugins/フォルダ内のコンパイル済みModアセンブリを一括ロードします。

BepInEx 5とBepInEx 6:IL2CPP Interopの架け橋

Harmony動的パッチ:非破壊的インメモリフックの技術

BepInExによってメモリ上に展開されたModは、ゲーム内部の関数を改変するためにHarmonyライブラリを使用します。Harmonyはディスク上のバイナリを一切書き換えず、JITコンパイル時にメモリ上のメソッドプロローグ(先頭命令)を直接書き換えることで処理をインターセプトします。

4つの主要パッチタイプ

【ゲーム本来のメソッド呼び出し:例 PlayerController.TakeDamage(float amount)】
           │
           ▼
【Harmony Prefix パッチ】 ---> true(本来の処理を続行)または false(本来の処理をスキップ)
           │
           ▼
【ゲーム本来のメソッド実行:HP減算処理】
           │
           ▼
【Harmony Postfix パッチ】 ---> 戻り値 (__result) の取得・改変やステータス更新
           │
           ▼
【Harmony Finalizer パッチ】 ---> 未処理の例外を捕捉しゲームクラッシュを防止
  1. Prefixパッチ([HarmonyPrefix]):
    • 本来のメソッドが実行される直前に割り込みます。
    • 引数の検証や書き換えが可能です(例:ダメージ適用前に引数amountを半減させる)。
    • 実行の短絡(Short-circuit):Prefixがfalseを返した場合、本来のメソッド処理を完全にスキップして即座に関数を終了させることができます。
  2. Postfixパッチ([HarmonyPostfix]):
    • 本来のメソッドが完了した直後に実行されます。
    • ref __resultパラメータを通じて、本来のメソッドの戻り値を読み取ったり、別の値に書き換えたりできます。
    • 副作用イベントのトリガー(例:アイテム作成完了時に実績を付与する)に最適です。
  3. Transpilerパッチ([HarmonyTranspiler]):
    • 最も高度なパッチタイプです。対象メソッドの生のCILバイトコードをIEnumerableのCodeInstructionシーケンスとして直接操作します。
    • JITコンパイル前のバイトコードレベルで、特定の命令(OpCode)をピンポイントで挿入・削除・置換できます。
  4. Finalizerパッチ([HarmonyFinalizer]):
    • メソッド全体を包む動的なtry/finallyブロックとして機能します。予期せぬ例外エラーをトラップして抑制し、Modのエラーによるゲームの強制終了(デスクトップクラッシュ)を防止します。

トラブルシューティングマトリクス:Unity Modの不具合診断

Modが正常に読み込まれない、あるいはクラッシュが発生した場合は、以下の診断基準に従って原因を特定してください:

不具合の症状アーキテクチャ上の根本原因診断指標対処手順
コンソールが出ない/Modが無視されるプロキシDLLがOSローダーによって読み込まれていないBepInEx/LogOutput.logが生成されないフォルダ直下にwinhttp.dllがあるか確認;アクセス権限を点検
起動直後に即時クラッシュ (IL2CPP)IL2CPP製ゲームに誤ってMono用のBepInEx 5を導入したフォルダ直下にGameAssembly.dllが存在するBepInEx/を全削除し、BepInEx 6 x64 IL2CPP専用ビルドを導入
ゲームのアプデ後に全Modがエラーゲーム本体の更新によりInteropプロキシキャッシュが不一致ログにTypeLoadExceptionやメソッド不在エラーが多発BepInEx/interop/フォルダを削除してプロキシを再生成させる
特定Modの機能が無効化される複数のModが同一メソッドにPrefixを当ててfalseを返したMod Aは動くがMod Bが沈黙する;ログ上でパッチ競合Modの読み込み順序を変更するか、非破壊的なTranspiler版へ移行
アンチチートによる起動遮断EACやBattlEyeがプロキシDLLインジェクションを不正検知「Untrusted system file」エラーやプロセスの即時キル公式オフライン起動オプションを利用するかMod用起動フラグを付与

Mod環境を構築したり、アセンブリ競合を解決する際、365Findsが提供する詳細なアーキテクチャ解説を参照することで、表面的な試行錯誤を避け、内部ランタイムの仕組みに基づいた確実なトラブルシューティングが可能になります。MonoとIL2CPPの実行モデルの違いを正しく理解し、安全なメモリパッチ手法を適用することが、UnityゲームのMod環境を長期にわたり安定稼働させるための不可欠な基礎知識です。

参考文献