Unity游戏Mod开发入门:基于BepInEx与Harmony的插件开发实战
1. 项目概述为什么选择BepInEx作为Unity Mod开发的起点如果你是一个Unity游戏的爱好者看着《星露谷物语》、《太吾绘卷》或者《大侠立志传》里那些由社区大神制作的、让游戏体验翻倍的Mod心里痒痒的也想自己动手试试那你来对地方了。这个项目就是带你从零开始用BepInEx框架敲开Unity游戏Mod开发的大门。别被“开发”两个字吓到这和你想象中的“从零写一个游戏”完全不同。Mod开发更像是一种“外科手术”我们是在一个已经成熟运行的“病人”游戏本体身上进行精准的“器官移植”或“功能增强”而BepInEx就是我们手中那套最趁手、最安全的手术工具。为什么是BepInEx在Unity Mod开发社区里它几乎是事实上的标准。简单来说它是一个插件加载器和运行时补丁框架。你可以把它理解为一个“中间人”或者“接线板”。游戏启动时BepInEx会先于游戏代码加载然后它负责把我们写的Mod插件一个个.dll文件挂载到游戏进程里并允许我们修改游戏原有的代码逻辑。相比于直接去反编译、修改游戏原生的Assembly-CSharp.dll文件BepInEx的方案是非侵入式的。你的Mod以独立插件的形式存在不会破坏游戏本体文件安装和卸载都像复制粘贴一样简单极大地降低了风险也方便管理。很多新手会困惑于“Mono”和“IL2CPP”这两个词。这里简单解释一下Unity游戏最终发布的脚本代码有两种后端。老一些的Unity游戏多用Mono这种模式下我们的C#代码逻辑相对容易理解和修改。而新出的、尤其是追求高性能和安全的游戏越来越多地使用IL2CPP它会将C#代码转换成C再进行编译相当于给代码加了一层“壳”分析和修改的难度会大一些。但好消息是BepInEx对两者都提供了支持只是针对IL2CPP需要额外的一个组件BepInEx.IL2CPP。本指南会以更常见的Mono后端游戏作为主要示例因为其原理更直观适合入门。掌握了Mono下的开发再理解IL2CPP的差异就会容易得多。所以无论你是想为自己喜欢的游戏添加一个新功能修复一个你觉得不爽的设定还是纯粹想学习逆向工程和C#实战这篇指南都试图为你铺平最初的那段路。我们不需要从零搭建Unity开发环境而是直接从一个“黑客”或“改造者”的视角切入目标明确让游戏按我们想要的方式运行。2. 环境准备与工具链搭建磨刀不误砍柴工在开始写第一行Mod代码之前把环境搭建好是至关重要的一步。一个清晰、正确的开发环境能避免后续无数诡异的错误。这里我会详细列出每一个环节并解释其作用。2.1 核心工具Visual Studio与.NET SDK我们的Mod本质上是使用C#编写的类库.dll。因此你需要一个C#开发环境。安装Visual Studio 2022去微软官网下载Community版本免费。安装时在“工作负载”选择界面务必勾选“.NET桌面开发”。这个工作负载包含了我们需要的C#编译器、项目模板和基础类库。不需要勾选“Unity游戏开发”或“通用Windows平台开发”等无关项。确认.NET Framework版本这是最容易出错的地方。绝大多数使用Mono后端的Unity游戏其运行时环境是**.NET Framework 3.5、4.x**而不是更新的.NET Core或.NET 5/6/7/8。你需要在Visual Studio Installer中为“.NET桌面开发”工作负载勾选上相应的**.NET Framework 4.x 开发工具**例如4.7.2或4.8。同时为了兼容性建议也安装**.NET Framework 3.5 SP1 开发工具**。提示如何知道目标游戏用的.NET版本一个简单的方法是查看游戏目录。如果游戏使用Mono通常在其游戏根目录/游戏名_Data/Managed/文件夹下会有一个mscorlib.dll文件。用记事本打开这个dll开头几行可能会显示版本信息或者你可以用ildasm.NET Framework SDK自带工具查看。更保险的做法是直接参考该游戏其他成熟Mod的编译目标框架。2.2 BepInEx框架获取与游戏部署我们不从零编译BepInEx而是直接使用其官方发布的打包版本。下载BepInEx前往BepInEx的GitHub发布页搜索“BepInEx Releases GitHub”。根据你的目标游戏选择版本。对于大多数Mono游戏下载BepInEx_x64_版本号.zip64位游戏或BepInEx_x86_版本号.zip32位游戏。如果不确定就下x64版本。部署到游戏目录找到你的游戏安装根目录例如Steam\steamapps\common\你的游戏。将下载的ZIP包里的所有文件主要是BepInEx文件夹和doorstop_config.ini、winhttp.dll等几个根目录文件解压到游戏根目录。务必确保BepInEx文件夹与游戏的.exe启动文件在同一级目录。首次运行与目录生成运行一次游戏。如果部署成功游戏应该能正常启动可能会比平时慢一点。然后关闭游戏。此时游戏根目录下的BepInEx文件夹里会自动生成几个子文件夹core/: BepInEx核心库。plugins/:这是我们放置自己开发的Mod插件.dll文件的地方。patchers/: 放置补丁器高级用法初期可忽略。config/: BepInEx及各个插件的配置文件会生成在这里。LogOutput.log: 最重要的日志文件任何插件加载错误、运行时异常都会记录在这里是排查问题的第一手资料。2.3 创建你的第一个Mod项目现在我们在Visual Studio中创建Mod项目。打开Visual Studio选择“创建新项目”。在搜索框输入“类库”选择“类库(.NET Framework)”模板注意不是“.NET Standard”或“.NET Core”类库。点击下一步。给你的项目起个名字比如MyFirstGameMod。选择位置。在“框架”下拉菜单中选择你之前确认的游戏所使用的.NET Framework版本比如.NET Framework 4.7.2。点击“创建”。项目创建后在“解决方案资源管理器”中右键点击项目名 - “管理NuGet程序包”。在浏览选项卡中搜索“BepInEx.Core”。选择它并安装。这会自动为你的项目添加对BepInEx核心API的引用。这是与BepInEx框架交互的基础。至此你的开发环境就准备好了。接下来我们需要理解Mod如何与游戏“对话”。3. BepInEx插件核心结构与生命周期一个最基本的BepInEx插件就是一个包含了特定类和方法的C#类库。理解它的结构就理解了Mod的加载和运行机制。3.1 插件主类继承BaseUnityPlugin在你的项目中默认会有一个Class1.cs文件。把它重命名为有意义的名称比如MyAwesomeMod.cs。然后修改其内容如下using BepInEx; using BepInEx.Logging; using UnityEngine; namespace MyFirstGameMod { // 最重要的特性BepInPlugin // GUID必须是全局唯一的建议使用“作者名.插件名”的格式 // 名称和版本号会显示在BepInEx的插件管理界面 [BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)] public class MyAwesomeMod : BaseUnityPlugin // 必须继承BaseUnityPlugin { // 内部日志记录器用于向BepInEx控制台和日志文件输出信息 internal static ManualLogSource Log; // Awake方法是插件的入口点在插件被加载后立即调用一次 private void Awake() { // 将基类的Logger赋值给我们的静态Log变量方便其他地方使用 Log Logger; // 输出一条日志证明插件已加载 Log.LogInfo($插件 {PluginInfo.PLUGIN_NAME} v{PluginInfo.PLUGIN_VERSION} 已加载); // 在这里进行你的插件初始化工作例如 // 1. 读取配置 // 2. 注册Harmony补丁修改游戏代码 // 3. 添加游戏对象或组件 // 4. 绑定键盘快捷键等 } // Update方法会在游戏的每一帧被调用如果游戏对象处于激活状态 // 对于不依赖每帧逻辑的插件可以删除此方法 private void Update() { // 例如检测按键输入 if (Input.GetKeyDown(KeyCode.F8)) { Log.LogWarning(你按下了F8键); // 触发自定义功能 } } } }同时我们创建一个PluginInfo.cs文件来集中管理元信息namespace MyFirstGameMod { // 这是一个静态类用于定义插件的元数据 public static class PluginInfo { public const string PLUGIN_GUID com.yourname.mycoolmod; public const string PLUGIN_NAME 我的超级Mod; public const string PLUGIN_VERSION 1.0.0; } }关键点解析[BepInPlugin]特性这是BepInEx识别一个类为插件主类的标记。没有它你的dll不会被加载。继承BaseUnityPlugin这赋予了你的类访问BepInEx框架和游戏Unity环境的能力。Awake()方法这是插件的“构造函数”只执行一次。所有的一次性初始化操作都应放在这里。Update()方法因为继承了MonoBehaviour通过BaseUnityPlugin你可以使用所有Unity的生命周期方法如Update,Start,OnDestroy等用于需要持续运行或响应的逻辑。3.2 编译与部署测试在Visual Studio中按CtrlShiftB编译项目。编译成功后在项目文件夹的bin\Debug\或bin\Release\下找到生成的MyFirstGameMod.dll文件。将这个dll文件复制到游戏的BepInEx\plugins\文件夹下。你可以创建一个以你插件命名的子文件夹如BepInEx\plugins\MyAwesomeMod\来管理这样更清晰。启动游戏。观察游戏启动时是否有命令行窗口弹出BepInEx控制台或者直接查看游戏根目录下的BepInEx\LogOutput.log文件。如果你在Awake中写了日志输出你应该能在日志文件中看到类似这样的行[Info :MyAwesomeMod] 插件 我的超级Mod v1.0.0 已加载恭喜你你的第一个“Hello World”级别的Mod已经成功运行了它现在除了打印日志什么也不做但已经证明了从代码到游戏内执行的完整链路是通的。4. 与游戏交互的核心技术Harmony补丁打印日志只是第一步Mod的灵魂在于修改游戏原有的行为。我们不可能拿到游戏的源代码直接改这就需要用到补丁Patching技术。在BepInEx生态中最强大的补丁库是Harmony。它允许你在运行时动态地修改其他程序集即游戏代码中方法的执行逻辑。4.1 Harmony基础概念与安装Harmony的工作原理可以理解为“劫持”。它找到游戏里你想改的那个方法比如一个计算伤害的方法CalculateDamage然后让你有机会在这个方法执行前Prefix、执行后Postfix甚至完全替换它Transpiler更高级插入你自己的代码。安装Harmony在项目的NuGet包管理器中搜索并安装**HarmonyX**或Lib.Harmony。HarmonyX是Harmony的一个活跃分支与BepInEx集成更好推荐使用。引用命名空间在需要使用Harmony的类文件中添加using HarmonyLib;。4.2 实战修改玩家金币数量假设我们想修改一个游戏功能当玩家获取金币时获得的双倍。我们首先需要知道游戏里处理获取金币的方法是什么。这需要用到反编译工具。工具准备下载dnSpy或ILSpy。这些都是.NET程序集的反编译工具。我们将用它来查看游戏代码。定位目标打开反编译工具拖入游戏目录游戏名_Data\Managed\下的Assembly-CSharp.dll文件。这个文件包含了游戏的大部分C#逻辑代码。使用搜索功能如搜索“Gold”、“AddGold”、“Money”等关键词浏览类和方法找到疑似负责增加金币的方法。例如你可能会找到一个名为PlayerInventory的类里面有一个AddGold(int amount)方法。假设我们找到了这个方法// 这是你通过dnSpy看到的游戏原始代码推测 public class PlayerInventory { public void AddGold(int amount) { this.gold amount; UpdateGoldUI(); // 更新UI显示 } }我们的目标是让amount参数翻倍。4.3 创建并应用Harmony补丁在你的Mod项目中新建一个类文件GoldMultiplierPatch.cs。using HarmonyLib; using BepInEx.Logging; namespace MyFirstGameMod { // HarmonyPatch特性用于声明这是一个补丁类 // 第一个参数指定要修补的目标类型类 // 第二个参数指定要修补的目标方法名 [HarmonyPatch(typeof(PlayerInventory), nameof(PlayerInventory.AddGold))] internal class GoldMultiplierPatch { // Prefix补丁在目标方法执行前运行 // 返回值为false可以跳过原始方法执行true则继续执行 // 使用ref关键字可以修改传入的参数 static bool Prefix(ref int amount) { // 获取我们插件主类的日志器 var log MyAwesomeMod.Log; // 记录原始金额 int originalAmount amount; // 将金额翻倍 amount * 2; // 输出日志便于调试 log.LogInfo($金币获取翻倍生效原始值{originalAmount}, 修改后{amount}); // 返回true让原始方法继续执行但此时amount已经被我们修改了 return true; } // Postfix补丁在目标方法执行后运行 // 可以通过__instance访问调用该方法的对象实例即PlayerInventory // 可以通过__result访问方法的返回值如果方法有返回值 // static void Postfix(PlayerInventory __instance, int amount) { ... } } }4.4 在插件启动时注册Harmony补丁修改MyAwesomeMod.cs中的Awake方法在插件加载时创建并应用所有补丁。private void Awake() { Log Logger; Log.LogInfo($插件 {PluginInfo.PLUGIN_NAME} v{PluginInfo.PLUGIN_VERSION} 已加载); // Harmony相关初始化 // 创建一个唯一的Harmony实例ID通常使用插件的GUID var harmony new Harmony(PluginInfo.PLUGIN_GUID); try { // 搜索当前程序集即我们的Mod dll中所有带有[HarmonyPatch]特性的类并应用补丁 harmony.PatchAll(); Log.LogInfo(Harmony补丁应用成功); } catch (System.Exception ex) { // 如果补丁应用失败记录错误但不要让插件崩溃 Log.LogError($应用Harmony补丁时发生错误: {ex}); } }现在重新编译你的Mod将新的dll覆盖到BepInEx\plugins\目录启动游戏。当游戏内触发PlayerInventory.AddGold方法时比如完成任务、出售物品你的Prefix补丁就会生效传入的amount参数会被翻倍然后游戏代码再用翻倍后的值去增加金币。你可以在游戏内尝试获得金币并观察BepInEx\LogOutput.log中是否出现了你写的日志信息。注意Harmony补丁是全局的、持久的。一旦应用它会影响所有调用该方法的代码。务必确保你的补丁逻辑严谨避免造成游戏崩溃或逻辑错误。在补丁方法内做好异常处理和日志记录至关重要。5. 配置管理与用户自定义一个好的Mod应该允许用户自定义其行为而不是把参数硬编码在代码里。BepInEx提供了内置的配置文件系统。5.1 创建与绑定配置BepInEx的配置基于ConfigEntryT类。我们在插件主类的Awake方法中初始化配置。using BepInEx.Configuration; public class MyAwesomeMod : BaseUnityPlugin { internal static ManualLogSource Log; // 声明配置项 internal static ConfigEntryfloat GoldMultiplier; internal static ConfigEntrybool EnableInstantMining; internal static ConfigEntryKeyboardShortcut ToggleModKey; private void Awake() { Log Logger; // ... Harmony初始化 ... // 1. 初始化配置 SetupConfig(); } private void SetupConfig() { // 第一个参数配置所在的分组Section在配置文件中会显示为[分组名] // 第二个参数配置项的键Key // 第三个参数配置项的默认值 // 第四个参数配置项的描述会显示在配置管理工具中 GoldMultiplier Config.Bind( 通用设置, // 分组 GoldMultiplier, // 键 2.0f, // 默认值2.0倍 金币获取倍率。设置为1.0则为原版效果。 // 描述 ); EnableInstantMining Config.Bind( 功能开关, EnableInstantMining, false, 是否开启瞬间采集测试用。 ); // 特殊类型键盘快捷键 ToggleModKey Config.Bind( 快捷键, ToggleModKey, new KeyboardShortcut(KeyCode.F7), // 默认快捷键F7 用于开关Mod功能的快捷键。 ); } }5.2 在代码中使用配置现在我们可以修改之前的金币翻倍补丁使其使用配置项[HarmonyPatch(typeof(PlayerInventory), nameof(PlayerInventory.AddGold))] internal class GoldMultiplierPatch { static bool Prefix(ref int amount) { // 从静态配置项中读取倍率 float multiplier MyAwesomeMod.GoldMultiplier.Value; // 如果倍率不等于1即不是原版才进行修改和记录日志 if (multiplier ! 1.0f) { int originalAmount amount; // 使用浮点数计算再转回整数避免精度问题 amount (int)(amount * multiplier); MyAwesomeMod.Log.LogInfo($金币倍率生效倍率{multiplier}, 原始{originalAmount}, 修改后{amount}); } // 即使倍率为1.0也返回true让原方法执行 return true; } }5.3 用户如何修改配置用户不需要手动编辑文本文件。BepInEx通常与一个名为BepInEx.ConfigurationManager的插件配合使用。用户安装ConfigManager让用户从BepInEx的插件发布站下载BepInEx.ConfigurationManager的dll放入BepInEx\plugins\目录。游戏内图形化配置安装后在游戏中按F1键默认会弹出一个配置窗口。左侧列表会显示所有加载的、支持配置的插件。找到你的插件名我的超级Mod点击后右侧就会显示你定义的所有配置项GoldMultiplier,EnableInstantMining等并提供滑块、输入框、复选框等控件供用户直接修改。修改后即时生效或重启游戏生效取决于你的代码实现。这种方式极大地提升了Mod的易用性和专业性。6. 常见问题排查与调试技巧实录Mod开发过程中99%的时间都在和错误作斗争。以下是我踩过无数坑后总结的排查清单。6.1 插件根本未加载症状游戏日志LogOutput.log里完全没有你插件的任何信息包括错误信息。排查步骤检查dll位置确认你的插件dll确实放在了BepInEx\plugins\或其子目录下。检查BepInEx版本确保你下载的BepInEx版本与游戏架构x86/x64匹配并且游戏本身支持有些游戏有反作弊可能会阻止。检查依赖项你的插件dll可能依赖其他库如0Harmony.dll,BepInEx.Core.dll。确保这些库存在于游戏的BepInEx\core\目录下。使用Visual Studio发布时选择“依赖项随程序集复制”或手动确保这些dll在插件目录或游戏能找到的位置。查看启动日志仔细阅读LogOutput.log文件的开头部分看BepInEx是否成功加载以及它扫描了哪些插件目录。6.2 插件加载但报错症状日志中有你的插件名但伴随Exception错误信息。排查步骤阅读完整错误堆栈错误信息会明确指出哪一行代码出了问题。例如“MissingMethodException”可能意味着你引用的某个游戏类或方法不存在。这通常是因为游戏版本更新类名或方法签名变了。检查.NET框架版本这是最常见的问题之一。错误如“Could not load file or assembly... System.Runtime, Version4.x...” 几乎可以肯定是你项目编译的目标框架与游戏运行时框架不匹配。确保你的项目目标框架与游戏使用的.NET Framework版本一致。检查Harmony补丁目标HarmonyPatch特性中指定的类名和方法名必须完全正确包括命名空间、参数类型。使用dnSpy仔细核对。方法是否是public是否是static参数类型是否匹配使用Try-Catch包裹关键代码在Awake和补丁方法中用try-catch块包裹你的代码并将异常信息用Log.LogError输出这样你至少能在日志中看到错误原因而不是一个沉默的崩溃。6.3 补丁未生效症状插件加载日志正常但游戏内功能没有任何变化。排查步骤确认补丁已注册在Awake方法中在harmony.PatchAll()后加一行日志Log.LogInfo($成功修补了 {harmony.GetPatchedMethods().Count()} 个方法。);。如果数量为0说明Harmony没找到任何[HarmonyPatch]类。检查补丁逻辑在Prefix/Postfix方法开始处加日志看它是否被调用。如果没被调用说明目标方法可能不是你想象的那个或者签名不匹配。游戏内验证用你加的日志来验证。例如在金币补丁里确保日志能打印出来。如果日志出来了但金币没变可能是你修改的参数ref int amount并不是实际影响金币的那个变量或者游戏在别的地方还有另一套逻辑。6.4 高级调试技巧使用BepInEx控制台在BepInEx\config\BepInEx.cfg中可以启用[Logging.Console]下的Enabled true。这样游戏启动时会同时打开一个控制台窗口所有日志实时显示比看文件方便得多。使用Debug.Log在补丁或插件代码中你可以直接使用Unity引擎的Debug.Log(“消息”)。这些消息会出现在游戏的输出日志中如果游戏本身有控制台或你用第三方工具捕获也能看到。这比BepInEx的日志有时更直接。版本兼容性在插件的Awake方法最开始用Log.LogInfo输出当前游戏场景名、版本号等信息。这能帮你快速确认Mod是否运行在正确的游戏版本上。最小化测试当你增加新功能时最好创建一个全新的、只包含该功能的最小化测试插件。这能隔离问题避免现有复杂代码的干扰。7. 从修改到创造进阶交互思路掌握了基础的补丁和配置后你的Mod可以做得更多。这里提供几个进阶方向7.1 添加游戏内用户界面GUI许多实用的Mod需要提供一个界面让用户交互。你可以使用Unity的即时模式GUIIMGUI来快速绘制简单的界面。using UnityEngine; public class MyAwesomeMod : BaseUnityPlugin { private bool showConfigWindow false; private Rect windowRect new Rect(20, 20, 300, 200); private void Update() { // 使用你在配置中定义的快捷键来开关窗口 if (ToggleModKey.Value.IsDown()) { showConfigWindow !showConfigWindow; } } // OnGUI在每一帧被调用以渲染GUI private void OnGUI() { if (!showConfigWindow) return; // 绘制一个可拖拽的窗口 windowRect GUI.Window(0, windowRect, DrawConfigWindow, 我的Mod设置); } private void DrawConfigWindow(int windowID) { // 在窗口内绘制配置控件 GUILayout.Label($当前金币倍率: {GoldMultiplier.Value}); float newMultiplier GUILayout.HorizontalSlider(GoldMultiplier.Value, 0.5f, 10.0f); if (newMultiplier ! GoldMultiplier.Value) { GoldMultiplier.Value newMultiplier; // 可以在这里立即应用配置更改 } if (GUILayout.Button(关闭窗口)) { showConfigWindow false; } // 允许窗口拖拽 GUI.DragWindow(); } }7.2 监听与响应游戏事件有时你不需要修改具体方法而是需要在某个游戏事件发生时执行操作。你可以查找游戏中的事件Event或委托Delegate并通过Harmony补丁或者反射来订阅它们。例如假设游戏有一个OnGameSave事件// 通过Harmony补丁一个调用事件的方法或者直接反射获取事件并添加监听器 [HarmonyPatch(typeof(GameManager), nameof(GameManager.SaveGame))] internal class SaveGameHook { static void Postfix() { MyAwesomeMod.Log.LogInfo(游戏已保存执行自定义操作...); // 例如备份存档文件 } }7.3 创建新的游戏对象与组件你可以像在常规Unity开发中一样在运行时创建GameObject并添加自定义的MonoBehaviour组件。private void Awake() { // ... 其他初始化 ... CreateModGameObject(); } private void CreateModGameObject() { // 创建一个名为“MyModController”的隐藏游戏对象 GameObject modObj new GameObject(MyModController); // 防止场景切换时被销毁 GameObject.DontDestroyOnLoad(modObj); // 隐藏它在层级视图中的显示可选 modObj.hideFlags HideFlags.HideAndDontSave; // 将我们自己的脚本组件添加上去 modObj.AddComponentMyModBehaviour(); } // 自定义组件 public class MyModBehaviour : MonoBehaviour { private float timer 0f; private void Update() { timer Time.deltaTime; if (timer 5f) { Debug.Log(自定义组件运行中...); timer 0f; } } }这种方式让你可以拥有完全独立于游戏原有逻辑的、持续运行的后台服务或管理器。开发Unity Mod是一个不断探索、逆向和创造的过程。从简单的数值修改到复杂的系统重写再到全新的功能添加BepInEx和Harmony为你提供了无限的可能性。最关键的是保持耐心善用日志多阅读其他优秀开源Mod的代码社区是你最好的老师。当你第一次看到自己写的代码在喜欢的游戏里真正发挥作用时那种成就感是无与伦比的。