BepInEx 项目深度解析:Unity / XNA 游戏模组框架是怎么把外挂代码塞进游戏进程里的
BepInEx 项目深度解析:Unity / XNA 游戏模组框架是怎么把外挂代码塞进游戏进程里的
BepInEx 名字全称是 Bepis Injector Extensible,在 GitHub 上是 Unity 与 XNA / FNA / MonoGame 一类 .NET 游戏最主流的插件 / 模组加载框架。到 2026 年 10 月,它已经有 8.7k stars、912 forks、386 open issues,主体 C# 实现,约 54 万行代码,LGPL-2.1 开源。
它解决的问题很直接:游戏发行方只给你 .exe(Unity Mono)或更糟的 IL2CPP 编译产物,几乎没有任何”插件”入口。你想给游戏加汉化、加 MOD、加 debug 工具,传统做法是反编译改 IL、重打包。BepInEx 让你不改游戏本体,直接把外部 DLL 注入到游戏进程里启动,再通过运行时补丁改写游戏行为。
这一篇就扒一遍它源码、文档和插件生态,看看一个”游戏外挂框架”在工程上是怎么搭起来的。
一、三层启动模型:Doorstop → Preloader → Core
BepInEx 最值得讲的,是它的进程启动时机。普通 .NET 程序只有一个”运行时托管”的入口,而 BepInEx 强行把 .NET 启动前的一段空白抢了过来:
1 | ┌──────────────────────────┐ |
1.1 第一层:UnityDoorstop 抢入口
BepInEx 不自己做注入,它依赖另一个项目 NeighTools/UnityDoorstop。Doorstop 的核心做法只有一行:
1 | # doorstop_config.ini(游戏根目录) |
Doorstop 给 Unity / XNA 的可执行文件挂了一个原生 main() 钩子(Windows 用 LoadLibrary+IAT hook,Linux 用 LD_PRELOAD),让程序在进入 .NET 运行时之前先加载 BepInEx.Preloader.dll。这一步的时机极早——.NET CLR 还没起来,你对 CLR 的任何运行时修补都还来得及。
1.2 第二层:Preloader 做”运行时修补”
进入 BepInEx.Preloader.Core 后,BepInEx 干了几件脏活:
Patching/:用 Mono.Cecil 直接改写Assembly-CSharp.dll的 MSIL 字节码——给游戏入口方法塞一个静态构造调用、把AssemblyResolve重定向到 BepInEx 的 DLL 解析器。RuntimeFixes/:对 Unity Mono / IL2CPP 的已知坑打补丁,比如 HarmonyX 需要的 trampoline、IL2CPP 跨域通信所需的前置 hook。EnvVars.cs/PlatformUtils.cs:探测当前是 Windows / Linux / OSX,是 Mono 还是 IL2CPP 运行时,决定后续启动分支。
注意这一步结束后游戏主程序还没运行,所以你可以安全地改写任何方法体。
1.3 第三层:Core 把插件链拉起来
接着 BepInEx.Core 启动,读 BepInEx/config/BepInEx.cfg,实例化 Logger、Console、ConfigManager,最后跑 Chainloader:
1 | // 简化自 Chainloader.cs |
Chainloader 会扫描 BepInPlugins/<guid>/ 目录下所有 DLL,加载标了 [BepInPlugin] 特性、且依赖关系(DLL/CLB 字段)能成图的对象,按拓扑序实例化。
二、源码结构一览
仓库根目录:
| 目录 | 作用 |
|---|---|
BepInEx.Core/ |
框架本体:配置、Console、Contract、Logging、Bootstrap |
BepInEx.Preloader.Core/ |
启动修补:Cecil 改 MSIL、RuntimeFixes、平台探测 |
Runtimes/Unity/ |
Unity Mono / IL2CPP 各自的预编译 DLL(含 Il2CppInterop 桥接层) |
Runtimes/NET/ |
传统 .NET Framework / .NET Core 运行时支持(XNA、FNA、MonoGame) |
build/ |
Cake-based 构建脚本:自动下载 MonoMod、Cecil、HarmonyX 等依赖 |
docs/ |
文档源(docs.bepinex.dev) |
下面这张图把”启动链 + 关键命名空间”贴出来,你对照源码读会更顺:
1 | BepInEx.Preloader.Core/ |
三、IL2CPP:硬骨头是怎么啃的
Unity IL2CPP 把原本托管的 C# 代码编译成 C++,再编译成原生机器码。结果就是:游戏里几乎没有可被 .NET 反射的元数据,传统 Mono 下 Type.GetType("Player") 直接失效。
BepInEx 处理 IL2CPP 的链路:
- Cpp2IL(SamboyCoding/Cpp2IL):解析 IL2CPP 生成的
global-metadata.dat+ 二进制,把原生方法恢复成 MSIL 表达。 - Il2CppInterop(BepInEx/Il2CppInterop):自己 fork 了一份 .NET 6 runtime,用
__INTERNAL__call等 stub 把 IL2CPP 原生函数”假装成” .NET 方法。 - dotnet-runtime 6:BepInEx 自己 fork 了一份 .NET 6(BepInEx/dotnet-runtime),打了若干补丁,让 IL2CPP interop 跑得稳。
最终效果:你写插件的时候,依然可以 var player = Il2CppType.Get<Player>(); player.DoSomething();——和写 Mono 插件几乎一模一样。
平台兼容矩阵来自仓库 README:
| Windows | OSX | Linux | ARM | |
|---|---|---|---|---|
| Unity Mono | ✔️ | ✔️ | ✔️ | N/A |
| Unity IL2CPP | ✔️ | ❌ | ✔️ | ❌ |
| .NET / XNA | ✔️ | Mono | Mono | N/A |
注意 IL2CPP 的 OSX / ARM 还是空的——Apple Silicon 玩家想跑 IL2CPP MOD 得自己编译。
四、补丁双子星:HarmonyX 与 MonoMod
光”把 DLL 塞进进程”还不够,MOD 通常要改游戏的某段逻辑。BepInEx 不自己造补丁轮子,而是把两套现成工具嵌进来:
4.1 HarmonyX:源码级别的 PREFIX / POSTFIX / TRANSPILER
HarmonyX 是 Harmony 的 fork,针对 Mono / IL2CPP 做了 trampoline 优化。用法长这样:
1 | using HarmonyLib; |
Prefix 改参、Postfix 改返回、Transpiler 改 MSIL——这套范式已经成为 C# 模组开发的事实标准。
4.2 MonoMod:更底层的 IL 重写
MonoMod 提供 MMHook 和 ILManipulator,能直接改写方法体的指令流。Harmony 解决不了的时候(比如改 struct 字段、改枚举底层值),MonoMod 是后备方案。
五、插件加载器生态:兼容是 BepInEx 的护城河
游戏圈在 BepInEx 之前就有 BSIPA、IPA、MelonLoader、uMod 等十几个加载器,每个都有自己的插件格式。BepInEx 不替代它们,而是给每个老加载器做了一个”shim”:
| 老加载器 | 适配项目 | 用途 |
|---|---|---|
| BSIPA | BepInEx.BSIPA.Loader | Beat Saber 系 |
| IPA | IPALoaderX | 老一代 Unity Mono |
| MelonLoader | BepInEx.MelonLoader.Loader | Melon 插件 |
| MonoMod | BepInEx.MonoMod.Loader | 原生 MonoMod 包 |
| uMod | BepInEx.uMod.Loader | Oxide/uMod 系 |
这套兼容设计是 BepInEx 真正黏住生态的地方——很多老 MOD 直接套上 shim 就能跑,新 MOD 作者也能挑熟悉的范式写。
六、50 行写一个最简插件
跟着 官方教程,你只需要一个 C# 类库项目加一个 dll 引用:
1 | using BepInEx; |
构建出 MyFirstPlugin.dll,扔到 BepInEx/plugins/com.example.myfirstplugin/ 下,启动游戏——Debug.Log 的内容会同时打到 BepInEx 日志里。
七、怎么从零上手
- 去 Releases 页 选对应平台(Unity Mono、IL2CPP、.NET)的版本下载。IL2CPP 玩家看清楚是 x86 还是 x64。
- 解压到游戏根目录,Windows 下双击运行一次
BepInEx.Preloader.dll,会生成BepInEx/目录。 - 编辑
BepInEx/config/BepInEx.cfg,按需开启[Logging]、[Console]、[Harmony]。 - 编译你的插件 DLL,丢进
BepInEx/plugins/<guid>/。 - 启动游戏,控制台应弹出(默认按
F1),日志写到BepInEx/LogOutput.log。
如果想用 Bleeding Edge(6.x),文档在 docs.bepinex.dev/master。
八、局限与生态位
把 BepInEx 抬到一个合理位置看,它的优缺点也很清楚:
强项
- 唯一同时覆盖 Unity Mono / IL2CPP / .NET XNA 的开源加载器
- Doorstop + Preloader 的注入时机比 MelonLoader 更早,能改 MSIL
- HarmonyX + MonoMod 双重补丁工具链,方法体级别的修改几乎是上限
- 兼容 shim 生态让老插件零成本迁移
短板
- OSX IL2CPP / ARM 还没支持,Apple Silicon 是空白
- 每个 Unity 版本升级时 IL2CPP metadata 格式可能变,需要 Il2CppInterop 适配,社区偶尔会滞后
- 不像 MelonLoader 有内置的 IL2CPP 汇编器,要写复杂 MOD 的人得自己结合 Cpp2IL
- 文档对”如何在不开 AntiCheat 的商用游戏里跑”的指导偏少(这也是为了规避滥用)
它在模组圈的生态位可以总结成一句话:当你需要的是”一个能跨平台、改游戏底层行为、有完整补丁生态的模组运行时”,BepInEx 是目前唯一的开源选项。
九、写在最后
BepInEx 这个项目最值得学习的,不是某个单独的 API,而是它对进程入口的争夺——用 Doorstop 抢在 .NET 之前、用 Preloader 在 CLR 启动瞬间打补丁、用 Chainloader 把外部 DLL 拓扑化成插件链。这个套路在 Unity mod、嵌入式脚本宿主、应用热补丁这些场景里都通用。
如果你正打算给某个 Unity / XNA 游戏做工具链,或者你想深入 .NET CLR 的运行机制,从 BepInEx 源码入手会比直接看 CoreCLR 友好很多——它把 Cecil / MonoMod / Harmony 的用法都演示在了真实工程里。
相关阅读可以延伸到我之前写的 AirLLM 分层流式加载拆解,里面也讲到了”模型分片按需加载”的同类工程思路;想看更多开源项目深度玩法可以翻 GitHub 9 月第一周热榜 Top 5。
你平时用什么给 Unity 游戏做 MOD?HarmonyX 顺手吗?欢迎评论区聊聊。