1. 这不是“又一个Unity插件教程”,而是一份能让你三天内跑通第一个BepInEx模组的实战手记
你是不是也经历过这样的时刻:在Steam上看到某款Unity游戏,明明只差一个“自动拾取”或“无限体力”的小功能,就能彻底改变游玩体验,可翻遍论坛、GitHub和ModDB,找到的全是零散的配置片段、过期的API引用,甚至还有人把BepInEx和UnityInjector混为一谈?我试过——去年帮朋友调试《Risk of Rain 2》的一个自定义技能模组,光是搞清 BaseUnityPlugin 和 BaseUnityPlugin<T> 的区别就花了整整一天;更别提那个在Unity 2019.4.38f1环境下正常、换到2021.3.30f1就直接崩溃的 HarmonyPatches 加载顺序问题。这不是玄学,是BepInEx生态里真实存在的“版本断层带”。它不讲道理,但有迹可循。
BepInEx模组开发全攻略:从入门到精通的Unity游戏定制指南,核心关键词就是 BepInEx、Unity模组、Harmony补丁、IL注入、游戏热重载 。它解决的不是“能不能改”的问题,而是“怎么改得稳、改得快、改得可持续”的问题。它适合三类人:刚接触C#但想动手改游戏的Unity新手;熟悉Unity编辑器但对运行时Hook毫无概念的美术/策划;以及已经写过几个模组,却总在更新游戏本体后被迫重写全部逻辑的资深Modder。这篇指南不堆砌理论,不复述官方文档,所有内容都来自我过去三年在《Valheim》《GTFO》《Lethal Company》等十余款Unity游戏上的真实开发记录——包括那些被删掉的、没进README的、只在Discord私聊里分享过的排错技巧。
我不会告诉你“先安装BepInEx再放DLL”,而是告诉你:为什么必须用 BepInExPack 而不是单独下载 BepInEx 主程序;为什么 0Harmony.dll 要放在 plugins 目录下而非 core ;为什么你写的第一个 [BepInPlugin] 属性里,第三个参数(GUID)一旦重复,游戏连启动日志都不会输出一行。这些细节不是刁难,是BepInEx设计哲学的具象化表达:它把“稳定压倒一切”刻进了每个文件夹命名和每行日志格式里。接下来的内容,就是带你亲手拆开这个黑盒,看清齿轮如何咬合。
2. BepInEx不是“插件管理器”,它是Unity游戏的第二套运行时基础设施
2.1 为什么Unity原生不支持热补丁?——从Mono运行时说起
要真正理解BepInEx的价值,得先放下“它是个Mod Loader”的预设,把它看作一套嵌入式操作系统。Unity游戏打包后,其核心逻辑通常编译为 .dll 文件(如 Assembly-CSharp.dll ),由Mono运行时(或IL2CPP生成的原生代码)加载执行。Mono本身是遵循ECMA-335标准的.NET实现,但它 默认不提供运行时方法替换(Runtime Method Replacement)能力 。你可以用反射读取类型、调用方法,但无法在不重启进程的前提下,把 PlayerController.TakeDamage() 的原始IL指令替换成你自己的逻辑——这正是模组最基础的需求。
举个生活化类比:Unity游戏像一台出厂封固的智能电视,它的遥控器(UI)和芯片(逻辑)是一体浇筑的。你想加个“按住音量键3秒自动静音”的功能,官方遥控器没这个按键,你也不能撬开电视换主板。BepInEx做的,不是给你多配一个遥控器,而是悄悄在电视内部加装了一块“信号中继板”:它拦截所有从遥控器发来的红外指令,在到达主芯片前做一次翻译和增强,再转发出去。这块板子不改动原厂硬件,却让整台电视具备了新能力。
技术上,BepInEx通过两个关键层实现这一目标:
- Loader层 :负责在Unity主进程启动前,劫持
mono或il2cpp的加载流程,注入自己的初始化代码。它会扫描BepInEx\plugins\目录,识别所有标记了[BepInPlugin]特性的程序集,并确保它们在游戏主逻辑加载前完成注册。 - Core层 :提供
BaseUnityPlugin基类、Chainloader插件调度器、Logger统一日志系统等基础设施。它不直接修改游戏代码,而是为上层模组提供一套稳定的“钩子接口”。
提示:很多新手误以为BepInEx = Harmony。这是严重误解。Harmony只是BepInEx生态中负责“方法替换”的一个库(由Andersson007开发),就像螺丝刀只是修车工具箱里的一件。BepInEx可以不用Harmony(比如只做配置文件注入),但Harmony几乎离不开BepInEx提供的运行时环境。
2.2 BepInEx的目录结构,每一层都是设计意图的显性表达
当你解压 BepInExPack 到游戏根目录,会看到这样的结构:
GameRoot/
├── BepInEx/
│ ├── core/ # BepInEx核心运行时(bepinex_core.dll, 0Harmony.dll等)
│ ├── plugins/ # 你的模组DLL存放处(必须是.NET Standard 2.0+)
│ ├── config/ # 模组配置文件(.cfg格式,BepInEx自动读取)
│ ├── patchers/ # (可选)用于修改游戏原始DLL的IL Patch工具
│ └── log/ # 所有日志输出(BepInEx.log, Unity.log等)
├── winhttp.dll # BepInEx注入的网络代理(用于后续在线更新)
└── game.exe # 原始游戏启动器
这个结构不是随意安排的。 core/ 目录下的 bepinex_core.dll 是真正的“心脏”,它通过 winhttp.dll 劫持Windows API调用,在 game.exe 入口点执行前完成自身初始化。 plugins/ 目录则采用“无依赖隔离”设计:每个模组DLL必须自行打包所有依赖(除BepInEx Core外),避免DLL Hell。这意味着你不能在模组项目里直接引用 UnityEngine.dll ,而必须通过BepInEx提供的 UnityAPI 桥接层访问——这看似麻烦,实则是为跨Unity版本兼容性埋下的伏笔。
我踩过最深的坑之一,就是在 plugins/ 里放了一个带 Newtonsoft.Json.dll 引用的模组。结果在《Valheim》里运行正常,到了《GTFO》却因游戏本体自带同名但不同版本的Json库而崩溃。解决方案?不是降级你的Json库,而是用 <PackageReference Include="Newtonsoft.Json" PrivateAssets="all" /> 在.csproj里声明“此依赖仅供编译,不打包进输出DLL”。BepInEx的目录哲学,本质是“强约定,弱耦合”。



被折叠的 条评论
为什么被折叠?



