接触Unity引擎mod制作的朋友,应该都绕不开BepInEx这个名字。简单说,BepInEx是一个专门为Unity游戏打造的mod运行框架,它负责在游戏启动时提前加载插件,让我们能用C#写mod去修改游戏行为,而不需要改动游戏本身的二进制文件。项目标题里这几个关键词——Unity、BepInEx、mod制作,正好就是这条技术路线的三个核心。这篇文章我会从环境准备、框架安装、第一个mod编写、常见问题排查四个块,把整个过程完整捋一遍,适合刚接触Unity mod、想自己动手做一个小插件的朋友参考。
老玩家可能知道,Unity游戏改mod的路径不止一条,有直接改游戏资源的,有做内存修改的,也有靠注入DLL的。但BepInEx是目前社区里最稳定、生态最完整的一套方案,特别是配上Harmony补丁库,很多主流Unity游戏的mod都是在它上面跑的。我自己从BepInEx 4时代就开始用,一路跟着升到5.x再到6.x预览版,踩过的坑也不少,这篇就当是帮你把地图铺开,少走点弯路。
1. 为什么是BepInEx
1.1 它到底做了什么
很多人第一次看到BepInEx的压缩包,不太理解为什么解压到游戏根目录后,第一次启动游戏会在BepInEx文件夹里生成一堆配置和日志。这其实是它的核心机制:BepInEx本质上是一个预加载器,它在游戏主程序启动时抢先运行,把Unity引擎的初始化过程拦一道,然后加载BepInEx/plugins目录下的所有插件DLL。
这套机制对用户来说最大的好处是:你不用改游戏本体文件。游戏原始程序集保持原样,mod作为外部插件被挂载,更新游戏后大概率还能继续用。对开发者来说,BepInEx提供了统一的插件入口、日志系统、配置系统,甚至还有控制台,极大降低了mod开发门槛——你只需要写一个继承BaseUnityPlugin的类,就能在游戏启动时拿到执行权。
另外要分清楚一个概念:BepInEx本身不会修改游戏代码,它只是“宿主”。真正实现修改效果要靠两种手段,一种是直接调用游戏内部方法(通过反射或程序集引用),另一种是给游戏方法打Harmony补丁。BepInEx和Harmony是两个独立的库,但几乎成了黄金搭档,后面写mod时会重点讲。
1.2 和同类框架怎么选
Unity mod框架不止BepInEx一家。UnityModManager、MelonLoader、KingmakerModLoader这些都是社区里常见的选择,简单对比一下:
| 框架 | 定位 | 优点 | 缺点 |
|---|---|---|---|
| BepInEx | 通用Unity游戏mod框架 | 支持Mono和IL2CPP,插件生态庞大,配置灵活 | 手动安装步骤稍多 |
| UnityModManager | 偏GUIManagement的mod加载器 | 有界面,安装mod方便 | 对IL2CPP游戏支持较弱 |
| MelonLoader | 偏游戏内注入的mod框架 | 启动极早,适合需要抢在游戏初始化前执行的mod | 兼容性不如BepInEx稳定 |
个人经验是:成熟的老游戏,比如城市天际线、堕落之王,社区mod基本都围绕BepInEx做;新出的游戏如果是IL2CPP后端,BepInEx 6.x预览版也有对应支持。选BepInEx还有个理由:它背后的社区文档和示例工程相对多,遇到问题搜BepInEx <问题>基本都能找到答案。UnityModManager适合只装别人写好的mod,不适合自己做mod开发的场景。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开工前的准备:环境、工具与版本
2.1 先判断游戏后端
这一步是我见过最多的新手翻车点。Unity游戏的脚本运行环境分两种:Mono和IL2CPP。Mono是较早的解决方案,游戏目录下通常会有一个*_Data/Managed文件夹,里面放着能直接反编译的DLL;IL2CPP则是Unity把C#转成C++再编译成机器码,游戏根目录会出现一个大的GameAssembly.dll,原始DLL会被打包进global-metadata.dat。
怎么判断?打开游戏目录,看有没有GameAssembly.dll。有,就是IL2CPP;没有,大概率是Mono。再辅助看*_Data/Managed里有没有大量DLL,Mono游戏通常一眼可见。
这个判断直接影响后面选BepInEx版本。Mono游戏用BepInEx 5.x可以玩得很顺,直接引用游戏程序集写mod;IL2CPP游戏需要用到BepInEx 6.x配合Il2CppInterop,而程序集引用方式也完全不同,得用Il2CppDumper跑出DummyDll才能编译。
顺带提一个细节:很多游戏自带Steam启动项的-force-vulkan之类的参数,和BepInEx无关,不用管它。真正相关的是你要确认游戏是x64还是x86,这个能看游戏主进程的位数,BepInEx的包名里也会标明(win_x64、win_x86),别装错。
2.2 工具链选型
做Unity mod,工具链比编程语言本身还重要。我的固定组合是:
- BepInEx:核心框架,去GitHub Releases下载对应版本。
- dnSpy:主要反编译工具,用来看Mono游戏的程序集,顺带可以调试。
- Il2CppDumper:处理IL2CPP游戏的工具,能帮你还原出
DummyDll和global-metadata.dat中的结构。 - AssetStudio:解包Unity资源时用,比如要想提取游戏贴图、音效、动画,它能直接预览。
- Visual Studio 2022:写mod的IDE,C#项目类型选“.NET Framework 类库”或者“.NET Standard 2.0”都行。
注意一点:解包工具拿出来的东西,理论上涉及游戏版权,自己研究没问题,发布到公开平台时就要掂量一下。我个人的建议是,参考游戏内部逻辑来写mod完全OK,但不要直接搬运游戏资源或未加密的原始代码,这是行业的默认规矩。
2.3 BepInEx 5和6怎么选
这是搜索词里出现很多的一类问题。BepInEx 5.x是长期稳定版,目前最新版本大概在5.4.23.x,只支持Mono游戏。BepInEx 6.x是预览版,主要变化就是支持IL2CPP游戏,同时把Il2CppInterop整合进来了,但API有一些调整,安装方式也略微不同。
我的建议很简单:
- 游戏是Mono后端:直接选BepInEx 5.4.23.3(LTS)或5.4.23.x,生态最稳。
- 游戏是IL2CPP后端:用BepInEx 6.0.0-pre.x。
- 不确定后端:先查游戏目录再决定,别瞎猜。
另外还需要区分BepInEx 5与插件之间的兼容性。很多老插件是用BepInEx 5 API写的,直接丢进BepInEx 6的plugins目录可能不会加载,因为6.x移除了BaseUnityPlugin类?并没有,实际上6.x仍有BaseUnityPlugin,但Il2CppInterop的初始化发生了变化。真要迁移,得看插件作者有没有出6.x版本。
3. 安装BepInEx并跑通第一个插件
3.1 安装步骤与目录结构
安装BepInEx本身不复杂,但有一个几乎必备的第一步:你要确认游戏的写权限。如果游戏装在Program Files下,Windows的UAC可能会拦掉BepInEx创建目录和写日志的动作,导致安装后看起来什么都没发生。最好把游戏放在一个纯英文路径、无系统保护的位置。
具体步骤:
- 从GitHub下载对应版本的BepInEx压缩包。
- 解压,把
BepInEx文件夹和winhttp.dll(或者是doorstop_config.ini等文件)一起拷贝到游戏根目录,和游戏的可执行文件在同一层。 - 直接启动游戏一次,然后退出。
- 这时
BepInEx文件夹下会生成config、plugins、logs等子目录,说明预加载器已经生效。
第一次启动时严格来说,BepInEx会通过winhttp.dll注入到游戏进程。如果没生成这些目录,多半是注入失败,常见原因是游戏有反作弊保护(比如EAC、BattlEye会阻止DLL注入),或者Windows Defender把winhttp.dll隔离了。后者很气人,记得在Defender里加白名单。
安装完成后,目录结构大概是:
text复制GameRoot/
├── BepInEx/
│ ├── config/
│ ├── core/
│ ├── logs/
│ ├── plugins/
│ └── patchers/
├── Game.exe
└── winhttp.dll
plugins放落地插件,patchers放的是在游戏程序集加载前就要执行的预补丁组件,一般很少用到。logs里有一个LogOutput.log,这是排查问题的第一入口。
3.2 配置文件和日志
BepInEx/config/BepInEx.cfg里可以调BepInEx本身的行为,最值得关注的是两个区域:
[Logging]区域:UnityLogListening可以决定是否把Unity引擎的Debug.Log也抓到BepInEx日志里;LogLevels可以控制日志级别,排查问题的时候我喜欢把Info也打开。[Chainloader]区域:一般不用动,但如果游戏有多个exe入口,可能需要指定主程序。
BepInEx带一个控制台窗口,方便实时看日志。默认情况下控制台是否显示由配置里的console相关项控制。有一点需要注意:部分游戏在发布模式下调不出控制台,或者控制台和游戏分辨率冲突,这时候把[Logging]下的LogLevels打开同时看LogOutput.log也能达到目的。
3.3 乱码问题的处理
搜索词里有“bepinex乱码”,这确实是个高发问题。根源在于BepInEx的控制台和配置文件默认用UTF-8,但很多老游戏的控制台代码页是GBK,或者反过来,log里中文就会变成一团乱码。
解决思路有三层:
- 在
BepInEx.cfg里找[Logging]相关配置,看有没有LogLevels或WriteUnityLog之类的项,先把日志输出的编码统一成UTF-8。 - Windows系统区域设置里勾选“Beta: 使用Unicode UTF-8提供全球语言支持”,重启后乱码会缓解,但这个方法影响全局,不到万不得已我不用。
- 如果只是控制台乱码而log文件正常,优先在mod代码里避免输出中文,用英文日志。
实操经验是,我用BepInEx写mod时,日志信息一律用英文。不是歧视中文,而是跨平台、跨系统环境下英文日志永远是最稳的,等具体功能稳定了再决定要不要多语言。
4. 编写第一个mod:从Hello World到真实补丁
4.1 插件项目骨架
在Visual Studio里新建一个C#类库项目,目标框架选.NET Framework 4.7.2或.NET Standard 2.0。别选太高,否则目标游戏环境可能加载不了;也别选太低,否则用不了新语法。
引用两个核心DLL:
BepInEx.dll:位于BepInEx安装目录的core下。Harmony.dll:如果是BepInEx 5,一般在游戏根目录的BepInEx/core或BepInEx/utils下;如果BepInEx 6,搜索词里看到的IL2CPP场景会涉及Il2CppInterop,但Harmony还是必需的。
最简单的插件代码:
csharp复制using BepInEx;
using BepInEx.Logging;
namespace MyFirstMod
{
[BepInPlugin("com.example.myfirstmod", "My First Mod", "1.0.0")]
public class Plugin : BaseUnityPlugin
{
private void Awake()
{
Logger.LogInfo("Hello from MyFirstMod!");
}
private void Update()
{
// 每帧调用,别在这里做重活
}
}
}
[BepInPlugin]参数分别是GUID、名称、版本。GUID是全局唯一标识,最好用域名倒写,避免和其他mod冲突。Awake在插件被加载时执行,Start、Update这些生命周期方法和Unity的MonoBehaviour一致,理解起来没有额外成本。
编译好生成的DLL直接丢进BepInEx/plugins,启动游戏,日志里出现Hello from MyFirstMod!,就说明整条链路通了。
4.2 Harmony补丁实战
等到会写Hello World,就该学Harmony了。Harmony能把游戏里的某个方法“打补丁”,分别在方法执行前(Prefix)、执行后(Postfix)、出错时(Exception)插入我们自己的逻辑。
具体场景举例:假设游戏内部有个Player.Heal(int amount)方法,想让每次加血都翻倍,可以在Postfix里修改返回值,或者直接在游戏方法调用前改变参数。
Harmony调用方式有两种:注解式和手动式。注解式写起来清爽:
csharp复制using HarmonyLib;
[HarmonyPatch(typeof(Player))]
[HarmonyPatch("Heal")]
public class Patch_HealDouble
{
static void Postfix(ref int __result)
{
__result *= 2;
}
}
再配合在Awake里执行:
csharp复制private void Awake()
{
var harmony = new Harmony("com.example.myfirstmod");
harmony.PatchAll();
}
这里的"com.example.myfirstmod"必须和[BepInPlugin]的GUID保持一致,否则Harmony内部可能产生冲突。
很多新手卡在“不知道游戏内部方法叫什么、长什么样”这一步。这就得靠dnSpy或Il2CppDumper把游戏程序集翻出来看。Mono游戏直接用dnSpy打开Assembly-CSharp.dll,搜索关键词;IL2CPP游戏先跑Il2CppDumper得到DummyDll,再用dnSpy打开DummyDll里的同名DLL。
4.3 编译、部署与调试
写mod有一个很典型的循环:写代码 -> 编译 -> 复制DLL到plugins -> 启动游戏 -> 看日志 -> 改代码。
天天手动操作会累死。我个人的小技巧是在Visual Studio的“生成事件”里加一条命令行,把输出DLL自动复制到游戏plugins目录。这样每次F6编译完,直接启动游戏就能测。大致如下:
bat复制copy "$(TargetDir)MyFirstMod.dll" "D:\Games\MyGame\BepInEx\plugins\" /Y
调试时,如果日志不够直观,可以在代码里加File.WriteAllText之类的临时输出,但别写成常态,会影响性能。更专业的方式是配置Unity引擎的调试符号,但BepInEx场景下对Mono游戏还能附加进程调试,对IL2CPP就很难了,日志反而更实用。
5. 常见问题排查实录
5.1 插件加载不上
症状是启动游戏后,日志里没有你的插件信息。排查顺序按我经验排序:
- 确认DLL是不是真的在
BepInEx/plugins下,而不是在子文件夹里被漏扫了。BepInEx默认会扫描plugins下所有层级的DLL,但有些版本对顶层子文件夹的支持有变化。 - 确认插件程序集有没有引用BepInEx的公共强名称签名。如果DLL引用出错,BepInEx会在日志中报
FileNotFoundException或BadImageFormatException。 - 确认是否输出了构建结果但文件被游戏目录的同名DLL覆盖。有的游戏本身有
winhttp.dll,BepInEx需要覆盖它,如果覆盖不成功,整个框架不会加载。 - 检查日志文件里的
[Chainloader]行,有没有提示哪个插件被跳过。
最坑的是反作弊。BepInEx本身不是作弊工具,但很多反作弊系统会拦截所有DLL注入。如果你玩的游戏带反作弊,请不要在联机模式下用mod,那样违反游戏规则,还有封号风险。建议只在单机离线场景下玩mod。
5.2 补丁不生效
Harmony补丁没有生效,最常见的原因是类型或方法名不匹配。比如游戏经过混淆,方法名可能是a、b这种,dnSpy里看到的名字不代表运行时真实签名,尤其是IL2CPP转出来的DummyDll里那些字段偏移量。
我自己的排查习惯是:
- 先用dnSpy对照游戏当前版本的程序集,确认方法名和参数类型没写错。
- 在
Postfix或Prefix里加一个日志输出,看补丁到底有没有被执行。没执行就去掉[HarmonyPatch]改用手动Patch,有时候是注解解析有问题。 - 考虑游戏可能内嵌了多个程序集,同名类在不同程序集里,Harmony在PatchAll时可能解析到了错误的那一个。可以用ASM手动指定程序集来Patching。
还有一个容易忽略的点:如果你的mod想修改的类在Mono游戏里是internal或private,Harmony完全可以处理,但你需要保证引用的是正确的程序集。IL2CPP游戏里,普通方法补丁一般没问题,但属性、泛型、协程方法有时候不能直接补,得用Il2CppInterop转一层。
5.3 IL2CPP游戏的特殊情况
IL2CPP游戏的mod开发比Mono要绕一大圈。BepInEx 6 + Il2CppInterop是主流方案,但有些细节:
- Il2CppDumper跑出的DummyDll并不包含方法实现,只有签名,所以不能直接用dnSpy看逻辑,还得结合IDA之类逆Go分析
GameAssembly.dll,这个门槛比较高。 - 很多游戏把
global-metadata.dat加密了,Il2CppDumper会失败。这时候先搜一下有没有对应版本的解密工具,没有的话基本就劝退,别硬来。 - 用BepInEx 6时,插件里不能直接用
UnityEngine.Debug.Log,要走BepInEx的Logger,因为Il2CppInterop环境下Unity的静态方法访问需要经过interop层。
老实说,IL2CPP游戏的mod开发更适合有逆向基础的人,纯C#玩家想上手会痛苦一些。我的建议是先从Mono游戏练手,把BepInEx + Harmony这套流程吃透,再挑战IL2CPP。
5.4 性能与稳定性
mod写多了,游戏容易崩或者帧数骤降。排查下来很多问题不是逻辑错,而是资源泄漏。
常见几种:
- 在
Update里频繁创建对象。Unity每帧60次,累加很可观。应该把可复用的对象提升为字段。 - 用反射调用游戏内部方法时,每次都
GetMethod,性能很差。应该缓存MethodBase,或者干脆用Harmony的Patch。 - 打补丁后没有正确释放,
Harmony.UnpatchAll只在插件卸载时调用,一般不用管,但如果你在运行时动态Patching,一定要对应Unpatch。
我这里多说一句:mod本质上是在别人的程序里跑自己的代码,稳定性优先级比功能优先级高。宁可多加try-catch,也别让一个小mod把整个游戏带崩。
6. 一点进阶方向与个人体会
写完第一个能用的mod之后,下一步可以往这些方向摸索:给mod加配置界面(BepInEx自带的ConfigEntry就能做,不需要单独写UI)、监听游戏事件而不是每帧轮询、用AssetStudio配合做资源替换型mod、写patcher在游戏程序集加载前改静态方法等等。
我自己踩过几次坑之后,最大的体会是:做Unity mod开发,不要指望一次性看懂所有机制。BepInEx的源码在GitHub上完全开源,遇到问题去翻源码往往比搜教程更快;Harmony的官方文档也写得很清楚了,但版本差异一定要留意,BepInEx 5里内置的Harmony 2.x和BepInEx 6里用的Harmony 2.2.2在API上有些改动。
最后再分享一个小技巧:在BepInEx/config里把日志级别调到Debug,然后把游戏跑一遍,遇到bug时不要只给作者贴一句“不生效”,要把LogOutput.log里和你mod相关的这些行一起贴出来,这样排查效率能提高五倍不止。希望这篇能帮准备入坑Unity mod的朋友省下几天摸索时间。
