Unity游戏Mod开发入门:BepInEx插件加载与Harmony补丁实战 很多人第一次接触Unity游戏的Mod开发都是被英灵神殿、雨中冒险2这类热门游戏带进来的。打开NexusMods看到别人那些花里胡哨的功能第一反应往往是“这到底是怎么做到的”然后一搜教程铺天盖地都是“下载BepInEx放到游戏目录”这种前置安装说明再往后想自己动手写Mod能用的中文资料就很少了。这篇博文就把这条路完整走一遍。从BepInEx的工作原理讲起到用Visual Studio 2022新建工程再写一个能真正修改游戏逻辑的Harmony补丁每一步都给出能直接照做的代码和配置。不管你之前有没有写过C#只要对着文章一步步来大概率能把第一个插件跑起来。这篇文章适合所有想在Unity游戏上做Mod的玩家和初学者也适合那些已经在用现成Mod、但想自己改点什么的朋友。1. 开干之前先弄明白BepInEx和Unity Mod的关系1.1 BepInEx到底是干什么的BepInEx全称是“Bep In Ex”一个专门为Unity游戏设计的插件加载框架。你可以把它理解成游戏的“外挂总管”——它本身不修改游戏文件而是提供一套标准机制让你写的DLL插件能在游戏启动时被加载进去然后通过钩子(Hook)的方式修改游戏行为。很多热门Unity游戏都有对应的BepInEx版本包括英灵神殿(Valheim)、雨中冒险2(Risk of Rain 2)、博德之门3也有对应的BepInEx生态。这些游戏的Mod绝大部分都是BepInEx插件你写的每一个Mod本质上就是一个符合BepInEx规范的.NET程序集(DLL)放在指定目录下BepInEx会在游戏运行时自动加载它。没有BepInEx你就得直接改游戏的程序集文件改一个备份一个游戏一更新全部白干。有了BepInEx所有修改都隔离在plugins目录里删掉DLL就等于没装过任何Mod游戏本体毫发无损。这套隔离思路是所有Mod开发工具链的核心价值。1.2 它是怎么“侵入”游戏的BepInEx的加载机制可以拆成三层来看。第一层是代理DLL。BepInEx安装时会往游戏根目录放一个winhttp.dll在部分版本里是doorstop相关的文件。Windows系统加载程序集时有个特性如果exe同目录下存在winhttp.dll系统会优先加载它而不是系统目录里的版本。BepInEx就是利用这个机制让游戏启动时先加载这个代理DLL。第二层是Mono运行时注入。代理DLL启动后会定位游戏使用的Unity Mono运行时通过它把BepInEx的核心程序集注入到游戏进程中。注意这里有个重要的分支Unity游戏有两种代码方式一种是把C#编译成中间语言(IL)交给Mono虚拟机运行称为Mono模式另一种是使用IL2CPP工具链把代码编译成C再转成原生机器码游戏目录里会有个很大的GameAssembly.dll。Mono模式加载的是原汁原味的.NET程序集BepInEx可以直接操作IL2CPP模式下游戏逻辑被编译进GameAssembly.dll需要用专门针对Il2Cpp的BepInEx版本并且配合Il2CppInterceptor这类工具来截获方法调用。大多数PC上的Unity独立游戏用的是Mono模式但用Il2Cpp的也越来越多了。判断方法很简单看游戏目录下有没有Assembly-CSharp.dll这个文件。有就是Mono模式只有GameAssembly.dll但找不到Assembly-CSharp.dll就是IL2CPP模式。第三层是Harmony补丁。BepInEx本身只能让插件在游戏里“跑起来”真正要修改游戏行为靠的是Harmony这个库。Harmony能在运行时修改方法的IL指令在方法执行前、执行后甚至完全替换方法体里插入你自己的逻辑。这就是为什么所有BepInEx插件的核心逻辑几乎都离不开Harmony。比如英灵神殿的加速奔跑、无限负重本质上都是往对应的游戏方法上打了一个补丁。1.3 我的开发环境与工具清单先说明一下2024年这个时间点BepInEx 6.0已经进入了预览阶段面向.NET 6.0新项目建议直接用6.x。如果遇到兼容性问题可以退回使用稳定的5.4.23版本那个对应.NET Framework 4.x。本文以下内容以BepInEx 6.0为主线讲解但会在关键位置标注和5.x的差异。开发环境清单项目推荐方案说明开发工具Visual Studio 2022 Community免费安装时勾选“.NET 桌面开发”工作负载BepInExBepInEx 6.0.0-pre.2从GitHub Releases下载win_x64版本反编译工具dnSpy 或 ILSpy用来查看游戏程序集内部结构找方法名和签名游戏本体任意Mono模式的Unity游戏我演示用的英灵神殿ValheimSteam版即可.NET SDKVisual Studio自带6.0项目需要.NET 6 SDK支持装上Visual Studio 2022之后有两点容易踩坑第一安装的时候务必勾选“.NET 桌面开发”这个工作负载否则创建项目时找不到C#类库模板第二注意区分Visual Studio和Visual Studio Code这次我们用Visual Studio IDE不是VS CodeVS Code做Mod开发不是不行但配置调试环境做起来更累新手直接上IDE省很多事。2. 搭建插件工程从建目录到第一行代码2.1 Visual Studio里新建类库项目打开Visual Studio 2022选择“创建新项目”筛选框里输入“类库”选C#语言项目模板选“类库”Class Library。项目名称我建议直接叫插件名比如ExampleMod解决方案和项目同名就行。建好之后最重要的第一步是修改目标框架。默认新建的类库是net8.0或者net6.0这取决于你Visual Studio的版本。BepInEx 6.x要求net6.0所以右键项目选择“属性”在“目标框架”里选择.NET 6.0如果列表里没有需要先在Visual Studio Installer里安装.NET 6的SDK。这里有个硬性约束BepInEx加载插件时会参考程序集的目标框架装错版本会直接加载失败。接下来需要引入两个NuGet包。右键项目选择“管理NuGet程序包”搜索BepInEx.Core安装6.0.0-preview版本再搜索BepInEx.Harmony安装最新的2.x版本。如果你用的是BepInEx 5.4.x的老教程做法完全不同是直接手动引用BepInEx安装目录里的BepInEx.dll和0Harmony.dll。用NuGet的好处是自动处理依赖关系而且装的是.NET 6对应的构建版本不会出现引用错库的情况。先别急着写代码。BepInEx加载插件时默认会扫描plugins目录下的子文件夹。比较好的目录组织方式是“作者名-插件名”比如server-ExampleMod/dll。在项目属性页里把“输出路径”直接设置成游戏目录下的BepInEx/plugins/YourName-ExampleMod/。这样一来每次编译生成的DLL就会自动部署到正确的插件位置省去手动复制的步骤。注意输出路径如果用绝对路径记得在团队协作或者换电脑时重新配置相对路径有时候不够灵活。2.2 引用游戏程序集这一步最容易被忽略写Mod必然会访问游戏的类型和方法。比如你想修改游戏中人物的血量逻辑就要引用包含人物类定义的程序集。在Mono模式下这些C#类通常集中在游戏目录/GameName_Data/Managed/Assembly-CSharp.dll这个文件里。这里有个极易踩坑的地方新手容易去Unity引擎安装目录里找Assemble-CSharp.dll来引用。千万不要必须引用游戏自己目录里带的那份因为同一个类在两个版本里方法签名可能完全不一样引错了不用说编译出来必定运行错误。用dnSpy打开这个Assembly-CSharp.dll你就能看到游戏内部所有C#类型的结构。这里顺便解释一下GameAssembly.dll的问题。很多新手在翻游戏目录时会困惑到底是Assembly-CSharp.dll还是GameAssembly.dll其实GameAssembly.dll是IL2CPP模式的产物里面是转成C之后编译出来的原生代码没法用dnSpy直接反编译成C#。如果你的游戏是IL2CPP模式那Mod开发的复杂度要高好几个级别需要配合Il2CppInspector等工具来还原类型信息新手不建议从这种游戏入门。在Visual Studio里添加引用右键“依赖项”→“添加项目引用”→“浏览”找到游戏目录下Managed文件夹里的程序集。核心通常就是Assembly-CSharp.dll另外可能需要UnityEngine.CoreModule.dll、UnityEngine.dll等。选择时要留意属性面板里“复制本地”Copy Local这个选项务必设置为False。如果设成True编译后会把这些游戏自带的大DLL复制到你插件输出目录不仅脏BepInEx加载时还可能因为程序集版本冲突导致各种诡异错误。2.3 一个Hello World插件完整代码逐段解析现在开始写代码把项目里的Class1.cs删掉新建一个Plugin.cs内容如下using BepInEx; using BepInEx.Logging; namespace ExampleMod; [BepInPlugin(com.yourname.examplemod, ExampleMod, 1.0.0)] public class Plugin : BaseUnityPlugin { private void Awake() { Logger.LogInfo(插件ExampleMod已成功加载); } private void Update() { if (UnityEngine.Input.GetKeyDown(UnityEngine.KeyCode.F5)) { Logger.LogInfo(你按下了F5); } } }这段代码有几处需要逐个理解别照着抄完就完了。[BepInPlugin]特性是BepInEx识别一个类为插件的入口点。括号里三个参数分别代表GUID全局唯一标识符建议用“组织名.模块名”的格式、插件显示名称、版本号。GUID不能乱写后面Harmony补丁的标识也会依赖这个值多个插件如果GUID撞了BepInEx直接拒绝加载。类继承BaseUnityPlugin这是BepInEx提供的一个Unity MonoBehaviour子类意味着你的插件在游戏里承担着一个组件的角色。Awake方法在插件被加载时执行相当于插件的构造函数通常用来做初始化工作比如读取配置、注册Harmony补丁。Update方法每帧都执行Unity游戏每秒大概运行几十帧到上百帧想监听玩家按键行为的话写在这个方法里就行。Logger是BaseUnityPlugin内置的日志对象LogInfo方法会把字符串写入BepInEx的日志系统。这里有个2024年的重要细节BepInEx 6.0默认会复制一份日志到LogOutput.log文件同时在游戏窗口里显示控制台。如果你想把中文日志正常显示出来后面会专门讲到编码问题的处理。编译一下如果输出路径配置正确你就能看到BepInEx/plugins/ExampleMod/ExampleMod.dll这个文件生成。运行游戏游戏窗口里应该能刷出“插件ExampleMod已成功加载”的日志。3. 用Harmony给游戏“做手术”第一个真实Mod功能3.1 Harmony是什么看懂Prefix/Postfix/TranspilerBepInEx解决了“插件如何跑起来”的问题而Harmony解决的是“插件如何改变游戏行为”的问题。Harmony的原理朴实但强大游戏运行期间它会在内存中修改某个方法的IL指令插一段额外的跳转逻辑让方法“执行前先看看插件想不想说什么执行后再看看插件想不想修改结果”。打个比方游戏的某个方法就像一条固定的流水线原来流程是“进料→加工→出货”。Harmony让你能在“进料”前加一道检查工序Prefix在“出货”前加一道返工工序Postfix。你不需要动整条流水线只需要在新的工序里做自己的事。这就是Mod开发最核心的思想不是重写游戏而是通过钩子函数修改关键环节。Harmony的三种主要补丁类型补丁类型执行时机典型用途Prefix原方法执行之前拦截调用、修改参数、阻止原方法执行Postfix原方法执行之后修改返回值、读取方法执行后的状态Transpiler编译IL层面直接修改原方法内部指令非常高阶新手先不用学写一个Harmony补丁本身非常简单就是在普通方法上打一个[HarmonyPatch]特性。真正的难点在于确定你到底要补丁哪个类、哪个方法、方法签名长什么样。这就需要dnSpy出马打开Assembly-CSharp.dll搜索目标类的目标方法查看它的访问级别、参数类型、返回值类型然后照着写入你的补丁方法。3.2 实战修改英灵神殿的玩家移动速度下面演示一个真实的功能修改英灵神殿中角色的移动速度。在英灵神殿里人物的移动速度由Character类的GetSpeed方法计算得出。我先用dnSpy定位到这个方法观察它的签名。不同版本游戏这个方法名字和签名可能完全不同所以一定要以你自己的游戏版本为准我可以展示大致思路但方法名必须现场确认。假定找到的方法签名是public float GetSpeed()注意这个方法是实例方法不是静态方法。为了获取调用对象Harmony提供了特定参数名约定第一个参数可以叫__instance它代表调用这个方法的实例对象。如果你想读私有字段就按“___字段名”的格式定义参数例如“___m_moveSpeed”。这是Harmony魔法般的参数注入机制参数名就是约定本身写错名字参数会被置空。写一个Prefix补丁在方法执行前强行覆盖返回值using BepInEx; using BepInEx.Logging; using HarmonyLib; namespace ExampleMod; [BepInPlugin(com.yourname.examplemod, ExampleMod, 1.0.0)] public class Plugin : BaseUnityPlugin { private void Awake() { Logger.LogInfo(ExampleMod正在加载...); Harmony.CreateAndPatchAll(typeof(Plugin).Assembly, Info.Metadata.GUID); Logger.LogInfo(ExampleMod加载完成); } [HarmonyPatch(typeof(Character), nameof(Character.GetSpeed))] [HarmonyPrefix] private static bool GetSpeed_Prefix(Character __instance, ref float __result) { // 只在玩家角色上生效 if (__instance.IsPlayer()) { float originalSpeed __instance.GetSpeed(); __result originalSpeed * 2f; return false; // 返回false表示不再执行原方法 } return true; // 返回true继续执行原方法 } }这里有两个关键点。第一方法名GetSpeed_Prefix是随意的Harmony不靠方法名识别靠的是[HarmonyPrefix]和[HarmonyPatch]特性组合但命名尽量规范易读。第二Prefix补丁返回bool值返回false代表跳过原方法返回true代表继续执行原方法。我在这里用了return false ref __result的形式相当于完全接管了速度计算逻辑。有两点要注意我在补丁里又调用了__instance.GetSpeed()如果去掉return false或者在这里不设置__result就会造成无限递归因为你的Prefix把原方法拦下来后又调用了原方法本身。这种“自己调自己”的写法必须配合return false使用逻辑上等价于“读一次原始值返回修改后的值”。另一种更常见的做法是不调用原方法而是利用___m_moveSpeed私有字段或者维护一个自定义字典来记录原始速度避免递归风险。另外强制返回false后游戏内其他逻辑如果也依赖GetSpeed的返回值可能产生连锁反应这类补丁要谨慎最好能区分玩家和非玩家NPC的上下文。Awake方法里调用了Harmony.CreateAndPatchAll注意传入的是程序集对象和GUID。这个调用会扫描整个程序集找出所有带[HarmonyPatch]特性的类并应用补丁。第一个参数typeof(Plugin).Assembly代表当前插件程序集第二个参数是补丁的GUID标识一般直接用plugin里的GUID这样日志里能看到每个补丁的来源方便排查问题。如果忘了调用CreateAndPatchAll特性写得再完美也不会生效。3.3 配置系统给Mod一个开关真实Mod不可能把所有逻辑都写死在代码里玩家要能自己调整才算完整。BepInEx提供了极其简洁的配置API不需要自己解析配置文件几行代码就搞定。在Plugin类里添加一个配置字段private static ConfigEntryfloat SpeedMultiplier; private void Awake() { SpeedMultiplier Config.Bind( 移动, // 配置节对应cfg文件里的[移动] 速度倍率, // 配置项名称 2.0f, // 默认值 玩家移动速度倍数1.0为原始速度); // 描述文本 Harmony.CreateAndPatchAll(typeof(Plugin).Assembly, Info.Metadata.GUID); }然后在Prefix补丁里改用配置值private static bool GetSpeed_Prefix(Character __instance, ref float __result) { if (__instance.IsPlayer()) { __result __instance.GetSpeed() * SpeedMultiplier.Value; return false; } return true; }运行一次游戏后BepInEx会自动在BepInEx/config目录下生成一个以GUID命名的cfg文件。玩家用记事本打开就能修改速度倍率值改完保存下次启动游戏生效。这里的Config.Bind用法很固定三个常用参数分别是section、key、defaultValue。描述文本虽然可写可不写但写了之后cfg文件里会生成注释对玩家来说友好很多。进阶用法是在Awake里用Config.SettingChanged事件动态响应配置变化——比如玩家在游戏里用快捷键切换开关Mod不需要重启游戏就能生效。4. 编译、部署与调试让流程顺畅起来4.1 让VS自动把DLL复制到游戏插件目录手工复制DLL一次两次还行一旦开发进入高频迭代阶段每次改几行代码编译后还要手动复制很快就会烦。推荐把目录组织做成两个层次的方案。项目属性里“构建事件”→“后期生成事件命令行”可以写xcopy /y /d $(TargetPath) E:\Games\Valheim\BepInEx\plugins\ExampleMod\这个命令在每次编译成功后将当前生成的DLL复制到指定插件目录。/y参数表示不询问直接覆盖/d表示只复制比目标新的文件。路径里的E:\Games\Valheim要替换成你自己的游戏实际目录。注意复制之后还要对插件DLL进行“二次部署”因为BepInEx加载的是DLL旁边的依赖文件。如果你在项目里引用了额外的DLL库也需要一并复制过去否则插件加载时会因缺少依赖报错。可以在后期事件里加一行xcopy /y /d $(TargetDir)*.dll E:\Games\Valheim\BepInEx\plugins\ExampleMod\不过这种做法会把所有DLL都复制过去包括游戏自带的重引用。所以更推荐在“依赖项”→“引用”里把CopyLocal设置正确只让真正需要随插件分发的DLL被复制然后后期事件里只复制特定文件。4.2 日志排查三板斧Mod开发中一大半时间是靠日志排错的。BepInEx的日志系统有几个输出目的地游戏窗口内的控制台、BepInEx/LogOutput.log文件以及如果你用Visual Studio调试器的话还可以输出到“输出”窗口。最常用的三种日志方法方法级别使用场景Logger.LogInfo信息插件加载成功、配置读取到某值、补丁已应用Logger.LogWarning警告参数异常、非致命错误、兼容性提示Logger.LogError错误初始化失败、异常捕获后上报一个典型的排查流程是这样先看LogOutput.log里有没有你的插件加载记录没有说明文件位置或命名有问题有但随后跟了一堆异常堆栈异常信息里通常能精确告诉你错在哪个文件的哪一行。我最常做的事是在关键分支处加Logger.LogInfo输出变量当前值比如“SpeedMultiplier.Value2.0”“IsPlayer()True”这样能快速定位是逻辑没进去还是进去后算错了。4.3 几种“看起来没效果”的情况处理第一种情况日志显示插件加载成功但Harmony补丁没有生效。这个排查方向主要是方法签名对不上。游戏更新后原方法可能从GetSpeed()变成了GetSpeed(float deltaTime)你按旧签名打补丁Harmony匹配不到方法自然不会触发。判断方法是看启动日志里是否包含“Patching”相关的提示BepInEx在应用补丁时通常会打日志如果对应方法名没出现在日志里就是签名没对。第二种情况补丁生效了但游戏直接崩溃通常是Prefix里引发了异常。Harmony默认会捕获补丁里的异常并记录但有时候异常发生在构造参数期间会导致整个游戏闪退。遇到崩溃第一时间查LogOutput.log末尾的异常栈不要急着重装游戏。第三种情况快捷键不起作用。先确认Update方法有没有被调用——在Update第一行加一条Logger.LogWarning如果日志疯狂刷屏说明在运行按F5没反应那就是按键判定代码写错了。5. 常见问题与排查技巧实录速查表5.1 中文乱码从代码到日志一网打尽“BepInEx乱码”是很多中文Mod作者第一个遇到的问题。乱码出现的位置不同处理方式也不同。第一种是在代码文件里写中文编译后插件日志显示乱码。这是因为Visual Studio默认会把C#文件保存为UTF-8 with BOM。但有些情况下如果你的系统区域设置是中文环境Visual Studio可能以GB2312编码保存文件。解决办法在Visual Studio里打开出现乱码的.cs文件选择“文件”→“另存为”→“保存按钮右边的下箭头”→“编码保存”在编码列表里选择“Unicode (UTF-8 带签名) - 代码页 65001”保存覆盖即可。BepInEx 5.x对UTF-8支持比较差有时必须保存为UTF-8 with BOM才不会乱码BepInEx 6.x用的.NET 6原生支持UTF-8基本没有这个困扰。第二种是游戏窗口里的BepInEx控制台本身显示中文日志乱码。这是控制台代码页的问题Windows的命令行窗口默认代码页可能是936GBK而日志在写入时用的是系统默认编码字符集对不上就显示乱码。解决办法是在游戏启动前把Windows控制台代码页切换为UTF-8。在游戏目录下创建一个bat文件里面写chcp 65001 start Valheim.exe然后以后都用这个bat启动游戏。如果不想用这个方法或者游戏是Steam启动的那建议以文件日志为准直接看BepInEx/LogOutput.log文件用支持UTF-8的编辑器比如VS Code打开就不会乱码。第三种是cfg配置文件里的中文说明乱码。这种情况一般是直接改cfg文件时用了记事本记事本默认保存为ANSI编码而BepInEx读取时按UTF-8解析来回一折腾就花了。记住一条cfg配置文件统一用UTF-8编码保存或者干脆避免在配置项里写复杂中文描述。5.2 插件没加载或报错官方日志的正确打开姿势BepInEx日志的加载失败最常见原因是版本不匹配。提示信息往往长这样[Error] Cannot load [ExampleMod 1.0.0] because it has a dependency on BepInEx.Core 6.0.0 which is not loaded.这说明插件编译时参考的BepInEx版本和你安装到游戏里的BepInEx版本不一致。处理方法就一句话打开Visual Studio查看NuGet包里装的BepInEx.Core版本号和游戏目录BepInEx/core文件夹下的BepInEx.Core.dll版本号做对比两边保持一致。还有一类“加载失败”是逻辑层面的插件加载了但报NullReferenceException通常意味着你访问了某个游戏对象上的组件但这个组件在当前场景里不存在。比如在Update里直接写GameObject.Find(Player).GetComponent ()主菜单场景没有这个对象就会抛异常。经验之谈凡是要访问场景对象的代码一律先判空再走逻辑var player GameObject.Find(Player); if (player null) return;5.3 游戏更新后Mod一夜失效临时修复思路Unity游戏更新尤其是大型更新往往伴随代码结构大改。你的Mod昨天还好好的今天Steam自动更新完就进不了游戏这是所有Mod作者都会经历的事。第一种可能是游戏更新把BepInEx本身搞失效了因为游戏主程序可能做了防御性改动。先验证一下把BepInEx整个目录删掉或临时改名用纯净模式启动游戏游戏正常说明BepInEx是问题根源。去BepInEx的官方发布页看看有没有针对新版本游戏的新版BepInEx有就更新。第二种可能是BepInEx正常但你的插件加载失败大概率是方法签名变了。用dnSpy打开更新后的Assembly-CSharp.dll重新核对你要补丁的方法名、参数类型、返回值类型改代码重新编译。游戏大版本更新后原方法被移除的情况也经常发生此时唯一的办法是查找替代方法——比如原方法被一分为二或者逻辑移到了另一个类里。dnSpy的“搜索方法引用”功能在此时特别好用右键原方法选择“分析”能看到所有调用它的地方顺着这些线索找新逻辑所在通常代码更新后游戏的结构变化也不是天翻地覆的花点时间能追上。很多新人在Mod失效后会直接在评论区和论坛催Mod作者更新。说实话游戏更新后Mod失效属于生态常态作者也是义务维护。真着急用建议自己先按上面的思路尝试排查实在搞不定再给作者提供LogOutput.log文件和游戏版本号这些信息对作者定位问题帮助巨大。有能力的Mod作者往往会同时维护多款游戏Mod逐个提示“Y键已添加、N键未关联”鼠标悬停显示说明能解决Mod作者不会忘记按键绑定到哪个操作的问题。6. 写Mod五个实战心得关于Mod的安全性最后提醒一点所有放在BepInEx/plugins目录下的DLL在游戏启动时都拥有和游戏进程一样的权限。这意味着如果你从杂七杂八的网站下载Mod不排除里面有恶意代码的可能。自己写Mod无所谓但如果下载第三方插件尽量去官方发布渠道不要图方便在来路不明的网站乱下压缩包。开发Mod时也建议不要直接用Steam库文件夹里的游戏做测试可以复制一份出来或者用Steam的“设置→→下载→Steam库文件夹”路径管理避免反复重启游戏影响主库存档。从第一次对着教程复制Hello World到能改一个具体数值再到写出一个有配置界面的完整功能每个阶段都会遇到新问题。但这个过程中培养出来的排查思路不只适用于Unity Mod对理解游戏引擎工作方式、程序集加载机制甚至日常软件开发都有帮助。我第一次写Mod踩得最深的坑就是搞错了Harmony补丁的参数注入规则觉得怎么写都编译不过去后来静下心把官方文档读了一遍才恍然大悟参数名就是约定写对了参数自动赋值写错了就是浪费时间。这个经历后来帮了我大忙因为我意识到对任何框架而言最可靠的学习路径永远是官方文档加真实日志而不是各种二手教程的复制粘贴。BepInEx插件开发这条路门槛其实没有想象中那么高。只要你会最基本的C#语法懂一点二分查找、面向对象剩下的全靠动手试错。试试看第一个能用的Mod带来的成就感比玩通关任何大作都要强。