BepInEx插件开发入门:从环境搭建到Harmony补丁实战

📅 2026/8/3 18:22:45 👁️ 阅读次数 📝 编程学习
BepInEx插件开发入门:从环境搭建到Harmony补丁实战

1. 项目概述:为什么选择BepInEx作为Unity游戏插件开发的起点?

如果你是一个Unity游戏玩家,尤其是对《雨中冒险2》、《英灵神殿》这类支持模组的游戏情有独钟,那你一定对“插件”或“模组”不陌生。它们能改变游戏规则,添加新内容,甚至修复官方未处理的Bug,极大地延长了游戏的生命周期。而BepInEx,正是连接玩家创意与游戏世界的那座最稳固、最通用的桥梁。它不是某个特定游戏的专属工具,而是一个为Unity引擎游戏量身打造的、强大的插件/模组加载与运行框架。

简单来说,BepInEx是一个“中间人”。它能在游戏启动时,将自己“注入”到游戏进程中,接管Unity引擎的某些核心加载流程。这样一来,它就能在游戏加载官方资源的同时,也加载我们开发者编写的、以DLL(动态链接库)形式存在的插件代码,并让这些代码在游戏运行时生效。这个过程,我们称之为“运行时补丁”或“Hook”(钩子)。与直接修改游戏原生DLL文件(俗称“破解”或“crack”)这种高风险、不兼容且易被反作弊系统检测的方式不同,BepInEx采用非侵入式的内存注入,安全、可逆,并且为插件之间提供了良好的隔离和协作机制。

为什么说它是“终极”选择?因为在Unity游戏模组开发社区,BepInEx已经成为了事实上的标准。它拥有庞大的社区支持、详尽的文档(尽管有些是社区贡献的)、以及经过无数模组验证的稳定性。从简单的界面修改到复杂的游戏机制重写,BepInEx都能提供相应的底层支持。对于开发者而言,它抽象了复杂的注入细节,让我们可以更专注于插件功能本身的逻辑实现,用熟悉的C#和.NET环境进行开发,这大大降低了入门门槛。

所以,无论你是想为自己喜爱的游戏制作一个显示更多信息的小插件,还是想开发一个功能全面的 overhaul(大修)模组,从BepInEx开始你的旅程,都是一个明智且高效的选择。它适合所有有一定C#和Unity基础,并渴望将自己的想法融入现有游戏的开发者。

2. 核心思路与工具链搭建:构建你的开发环境

在动手写代码之前,一个顺手且正确的开发环境是成功的一半。BepInEx插件开发本质上是在为某个特定的、已编译的Unity游戏制作“外挂”程序集,因此我们的环境需要同时兼顾.NET开发、Unity游戏资源分析以及最终的测试部署。

2.1 开发环境选型解析

集成开发环境(IDE):Visual Studio 2022 Community这是我们的主力代码编辑器。选择VS2022社区版,因为它免费、功能强大,对C#和.NET的支持最为完善。你可能会看到一些关于在Visual Studio中使用Qt插件或其他机制的讨论,但那与我们无关。我们只需要它的核心C#开发、NuGet包管理和调试功能。确保安装时勾选“.NET桌面开发”工作负载。

代码辅助工具:Cursor或VS Code(可选但推荐)如果你觉得VS2022过于庞大,或者喜欢更轻量、AI辅助更强的编辑器,Cursor是一个新兴的优秀选择。它基于VS Code,但深度整合了AI代码补全和对话功能。在开发过程中,当你需要快速查询某个BepInEx API的用法,或者让AI帮你生成一段常见的Hook代码模板时,Cursor能显著提升效率。当然,传统的VS Code配合C#扩展也能胜任。

目标游戏与分析工具

  1. 目标游戏:你需要一个确定的支持BepInEx的Unity游戏,并确保其已安装。例如,《Risk of Rain 2》就是一个绝佳的入门选择,其模组生态极其繁荣。
  2. Unity游戏分析利器:dnSpy / ILSpy这是插件开发者的“眼睛”。游戏本身的逻辑都编译在Assembly-CSharp.dll等程序集中。我们需要使用dnSpy或ILSpy这类.NET反编译工具来打开游戏的DLL文件,查看其中的类、方法、字段,以确定我们需要修改或交互的代码位置。这是进行“运行时补丁”的前提,因为你必须知道你要“钩”住哪里。

BepInEx开发包我们需要通过NuGet为项目引用BepInEx的核心库。通常需要以下两个包:

  • BepInEx.Core: 提供插件系统核心、日志、配置等基础功能。
  • BepInEx.Unity/BepInEx.Harmony: 提供与Unity引擎的集成以及对Harmony库的支持(用于方法级别的代码修补)。

注意: 不要从不明来源下载所谓的“BepInEx SDK”压缩包。始终通过Visual Studio的NuGet包管理器来获取官方发布的包,这是保证依赖版本正确和项目可维护性的关键。

2.2 创建与配置插件项目

  1. 新建项目: 在Visual Studio中,创建一个新的“类库(.NET Framework)”项目。框架版本的选择至关重要。你必须根据目标游戏所使用的.NET版本(通常是.NET Framework 4.x,如4.7.2)来选择。选错框架版本会导致插件无法被加载。项目名称可以定为类似MyFirstBepInExPlugin

  2. 引用BepInEx库: 右键点击项目“引用” -> “管理NuGet程序包”。在浏览选项卡中搜索BepInEx.Core并安装。通常,它会自动引入相关的依赖(如Harmony)。对于大多数Unity游戏,你还需要安装BepInEx.Harmony(或BepInEx.Unity,取决于游戏和BepInEx版本,以官方文档为准)。

  3. 关键项目配置

    • 右键项目 -> “属性”。
    • 在“应用程序”选项卡,确保“目标框架”与游戏匹配。
    • 在“生成”选项卡,将“输出路径”修改为一个方便的位置,比如bin\Debug\。这会让编译后的DLL生成在项目下的bin\Debug文件夹,便于我们后续手动复制到游戏目录进行测试。
  4. 分析游戏程序集: 找到你的游戏安装目录,进入游戏名_Data\Managed文件夹。将Assembly-CSharp.dll文件复制到一个安全位置(不要直接在原目录操作)。用dnSpy打开这个文件。现在,你可以像浏览源代码一样浏览游戏的所有逻辑了。例如,如果你想做一款修改玩家金币的插件,就可以搜索“Gold”、“Currency”、“Player”等关键词来定位相关类和方法。

3. 插件核心架构与基础代码实现

一个最基本的BepInEx插件由几个核心部分组成:插件元数据、启动入口、配置管理和日志系统。理解这些是编写任何功能的基础。

3.1 插件类与元数据定义

每个插件都必须有一个作为入口的主类,并使用BepInEx提供的特性(Attribute)进行标记。

using BepInEx; using BepInEx.Logging; using HarmonyLib; // 命名空间建议与插件名相关,避免冲突 namespace MyFirstBepInExPlugin { // 最重要的特性:标记这是一个BepInEx插件 // GUID必须是全球唯一的,通常使用“作者名.插件名”的格式 [BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)] public class Plugin : BaseUnityPlugin // 必须继承自BaseUnityPlugin { // 内部静态引用,方便其他类访问插件实例和日志 internal static Plugin Instance { get; private set; } // 日志记录器,用于输出信息到BepInEx的控制台和日志文件 internal static ManualLogSource Log => Instance.Logger; // 插件的配置项(如果需要) internal static BepInEx.Configuration.ConfigFile Config => Instance.Config; // Awake方法在插件被加载时由BepInEx自动调用,这是你的启动代码 private void Awake() { // 设置静态实例 Instance = this; // 输出一条日志,确认插件已加载 Log.LogInfo($"插件 {PluginInfo.PLUGIN_NAME} v{PluginInfo.PLUGIN_VERSION} 正在加载..."); // 应用Harmony补丁(这是实现代码修改的核心,稍后详解) Harmony.CreateAndPatchAll(typeof(Plugin).Assembly); Log.LogInfo($"插件 {PluginInfo.PLUGIN_NAME} 加载完成!"); } } // 一个静态类,用于集中定义插件的元信息,保持整洁 public static class PluginInfo { public const string PLUGIN_GUID = "com.yourname.myfirstbepinexplugin"; public const string PLUGIN_NAME = "我的第一个BepInEx插件"; public const string PLUGIN_VERSION = "1.0.0"; } }

代码解析与注意事项

  • [BepInPlugin]: 这个特性是插件的“身份证”。BepInEx通过它来识别和加载插件。GUID是唯一标识符,必须确保不与网络上其他插件重复,否则会导致冲突。NameVersion会显示在BepInEx的启动日志和某些模组管理器中。
  • BaseUnityPlugin: 继承这个类,你的插件就自动获得了日志记录器(Logger)、配置文件(Config)等基础服务。
  • Awake(): 这是Unity标准的生命周期方法之一。在BepInEx中,它等同于插件的“主函数”。所有初始化逻辑都应放在这里。
  • 日志的重要性: 在插件开发中,Log.LogInfo/Debug/Warning/Error()是你的最佳伙伴。通过日志,你可以在游戏运行时观察插件的行为,这是排查问题的首要手段。永远不要使用Console.WriteLine(),因为它的输出在打包的游戏里是看不到的。

3.2 使用Harmony进行方法级代码修补

Harmony库是BepInEx实现运行时修改的“魔法棒”。它允许你在不接触原始DLL文件的情况下,在目标方法执行前、后或完全替换它。这是插件实现游戏逻辑修改的核心技术。

假设通过dnSpy,我们发现在游戏中有个Player类,里面有一个AddGold(int amount)方法,我们想实现“双倍金币”的效果。

第一步:创建补丁类和方法我们不在主Plugin类里直接写Harmony代码,而是创建一个专门的补丁类,这样结构更清晰。

using HarmonyLib; namespace MyFirstBepInExPlugin.Patches { // HarmonyPatch特性用于指定要修补的目标类和方法 [HarmonyPatch(typeof(Player))] // 目标类:Player [HarmonyPatch(nameof(Player.AddGold))] // 目标方法:AddGold internal class PlayerAddGoldPatch { // Prefix补丁:在目标方法执行前运行 // 方法名可以是任意的,但必须为静态,返回类型为bool(可选),并接收与目标方法相同的参数 static bool Prefix(ref int amount) { // 修改传入的amount参数,实现双倍效果 Plugin.Log.LogInfo($"原金币增加量: {amount}"); amount *= 2; Plugin.Log.LogInfo($"修改后金币增加量: {amount}"); // 返回 true 表示继续执行原始方法;返回 false 则会跳过原始方法的执行 return true; } // Postfix补丁:在目标方法执行后运行 // 可以访问目标方法的返回值(通过 __result)和参数 // static void Postfix(int amount, ref int __result) // { // // 如果AddGold有返回值,可以在这里进一步修改__result // } } }

第二步:应用补丁回到主Plugin类的Awake方法中,我们需要让Harmony知道这个补丁类的存在。我们之前使用的Harmony.CreateAndPatchAll(typeof(Plugin).Assembly);这行代码,会自动扫描当前程序集(即你的插件DLL)中所有带有[HarmonyPatch]特性的类,并应用补丁。这是一种简便的批量注册方式。

Harmony补丁类型详解

  • Prefix(前缀): 在原方法执行之前运行。可以通过返回false来阻止原方法执行。常用于修改参数、进行条件检查或完全替代原方法逻辑。
  • Postfix(后缀): 在原方法执行之后运行。可以读取和修改原方法的返回值(通过__result),也可以访问参数。常用于处理结果、触发额外事件。
  • Transpiler(编译器): 高级功能,直接操作原方法的CIL(中间语言)指令。用于进行更底层、更复杂的修改,比如修改循环次数、插入新的指令等。新手初期很少用到。

实操心得: 在编写Prefix/Postfix时,参数的命名和类型必须与目标方法完全一致(可以使用ref来修改值类型参数)。Harmony使用了一种叫做“参数访问”的机制,你甚至可以通过定义名为__instance的参数来访问目标方法所属的实例对象(对于非静态方法)。多查阅Harmony的官方文档和示例,是掌握这门“魔法”的关键。

4. 配置系统与用户交互

一个成熟的插件应该允许用户进行自定义配置,而不是把参数硬编码在代码里。BepInEx内置了一个简单但强大的配置系统。

4.1 创建与绑定配置项

我们在Plugin类的Awake方法中,初始化完日志后,来创建配置。

private void Awake() { Instance = this; Log.LogInfo($"插件 {PluginInfo.PLUGIN_NAME} v{PluginInfo.PLUGIN_VERSION} 正在加载..."); // 1. 绑定配置文件 // Config属性来自BaseUnityPlugin,它已经关联了一个以插件GUID命名的.cfg文件 // 我们在此处定义配置项 BindConfigurations(); // 2. 应用Harmony补丁 Harmony.CreateAndPatchAll(typeof(Plugin).Assembly); Log.LogInfo($"插件 {PluginInfo.PLUGIN_NAME} 加载完成!"); } private void BindConfigurations() { // 创建一个“通用”配置部分 var section = Config.Bind( "Settings", // 配置部分(Section)的名称,用于在配置文件中分组 "GoldMultiplier", // 配置项的键(Key) 2.0f, // 默认值 "金币倍率。设置为1.0为原版,2.0为双倍,0.5为减半。" // 描述,会显示在配置文件中 ); // 将配置项的值存储在一个静态属性中,方便全局访问 // 注意:Config.Bind返回的ConfigEntry<T>对象,其Value属性才是当前值 PluginSettings.GoldMultiplier = section.Value; // 订阅配置值改变事件(可选) section.SettingChanged += (sender, args) => { PluginSettings.GoldMultiplier = section.Value; Log.LogInfo($"金币倍率已更新为: {PluginSettings.GoldMultiplier}"); }; } // 一个静态类来集中管理所有配置值 public static class PluginSettings { public static float GoldMultiplier { get; set; } = 2.0f; }

4.2 在代码中使用动态配置

现在,我们可以修改之前的Harmony补丁,使用动态的配置值,而不是硬编码的2。

[HarmonyPatch(typeof(Player))] [HarmonyPatch(nameof(Player.AddGold))] internal class PlayerAddGoldPatch { static bool Prefix(ref int amount) { // 使用配置值 float multiplier = PluginSettings.GoldMultiplier; if (Mathf.Approximately(multiplier, 1.0f)) // 如果倍率是1,则不做任何事,直接跳过补丁逻辑以提升性能 { return true; } Plugin.Log.LogDebug($"应用金币倍率: {multiplier}"); // 注意:amount是int,multiplier是float,需要处理类型转换和舍入 int originalAmount = amount; amount = Mathf.RoundToInt(originalAmount * multiplier); Plugin.Log.LogInfo($"金币变化: {originalAmount} -> {amount} (倍率: {multiplier})"); return true; } }

生成的配置文件: 当插件第一次运行后,会在BepInEx/config目录下生成一个com.yourname.myfirstbepinexplugin.cfg文件。用户可以用任何文本编辑器打开并修改GoldMultiplier的值,下次启动游戏时就会生效。

[Settings] ## 金币倍率。设置为1.0为原版,2.0为双倍,0.5为减半。 # Setting type: Single # Default value: 2 GoldMultiplier = 2

注意事项

  1. 线程安全Config.Bind和访问Value在Awake中调用是安全的。但如果你的插件在其他线程(非游戏主线程)中读取配置,需要考虑同步问题,不过大多数简单插件不会遇到。
  2. 类型匹配Bind方法的泛型参数T必须与默认值的类型一致。支持基本类型(int, float, bool, string)和枚举。
  3. 文件位置: 配置文件位于BepInEx/config,日志位于BepInEx/LogOutput.log。教会用户如何找到这些文件,是提供支持的第一步。

5. 高级技巧:与Unity游戏对象和UI交互

很多插件不仅需要修改数据,还需要创建或修改游戏内的UI、监听游戏事件等。这需要与Unity引擎的运行时进行更深入的交互。

5.1 使用MonoBehaviour创建游戏内行为

BaseUnityPlugin本身继承自MonoBehaviour,这意味着你的主插件类可以像普通的Unity组件一样使用Update(),OnGUI()等生命周期方法。但对于需要独立GameObject或更复杂行为的情况,最好创建单独的MonoBehaviour。

using UnityEngine; namespace MyFirstBepInExPlugin.Components { public class GoldDisplayHUD : MonoBehaviour { private Player _player; private GUIStyle _labelStyle; void Start() { // 尝试在场景中寻找Player实例 // 注意:游戏对象的查找时机很关键,可能需要在游戏加载完成后的某个时刻进行 _player = FindObjectOfType<Player>(); if (_player == null) { Plugin.Log.LogError("未找到Player对象!"); enabled = false; // 禁用此组件 return; } // 创建GUI样式 _labelStyle = new GUIStyle(GUI.skin.label); _labelStyle.fontSize = 24; _labelStyle.normal.textColor = Color.yellow; } void OnGUI() { // 使用IMGUI在屏幕左上角绘制当前金币 if (_player != null) { GUI.Label(new Rect(10, 10, 300, 50), $"金币: {_player.CurrentGold}", _labelStyle); // 可以绘制更多信息,比如倍率 GUI.Label(new Rect(10, 50, 300, 50), $"当前倍率: {PluginSettings.GoldMultiplier}x", _labelStyle); } } void Update() { // 每帧都可以在这里做一些事情,例如检查玩家状态 // 但要注意性能,避免每帧进行昂贵的查找或计算。 } } }

然后,你需要在插件加载时的某个时机(例如,在确认游戏场景已加载后),将这个组件添加到游戏中的一个GameObject上。

// 在Plugin类中添加一个方法 private void CreateHUD() { // 创建一个新的空GameObject来承载我们的HUD组件 GameObject hudGO = new GameObject("MyPlugin_HUD"); DontDestroyOnLoad(hudGO); // 非常重要!防止场景切换时对象被销毁 // 将我们的显示组件添加上去 hudGO.AddComponent<Components.GoldDisplayHUD>(); Log.LogInfo("金币显示HUD已创建。"); }

调用CreateHUD()的时机需要谨慎。一个常见且相对安全的地方是在Awake()中启动一个协程,等待几帧或等待某个特定的游戏管理器初始化完成后再创建。

5.2 监听与触发游戏事件

除了修改方法,有时我们还需要在特定游戏事件发生时执行代码,比如玩家死亡、场景加载完成等。如果游戏本身使用了C#事件(event)或标准的Unity事件(如UnityEngine.Events.UnityEvent),我们可以通过Harmony进行订阅。更通用的方法是,找到负责触发这些事件的方法,并对它们进行Postfix补丁。

例如,假设游戏有一个GameEvents.OnPlayerDied的静态事件:

[HarmonyPatch(typeof(GameEvents))] [HarmonyPatch(nameof(GameEvents.TriggerPlayerDied))] internal class PlayerDiedEventPatch { static void Postfix() { // 玩家死亡后执行的逻辑 Plugin.Log.LogWarning("玩家已死亡!"); // 可以在这里重置一些插件状态,或者弹出自定义提示 } }

如果游戏没有暴露这样清晰的事件,你可能需要去修补具体的功能方法,比如Player.TakeDamagePlayer.Respawn

高级技巧与避坑指南

  1. 时机就是一切: 在Unity中,很多操作都依赖于正确的执行时机。不要在Awake()里试图访问尚未实例化的游戏对象。使用Start()协程、或监听SceneManager.sceneLoaded事件来确保你的代码在正确的时机运行。
  2. 性能考量OnGUI()每帧调用多次,效率不高,仅适用于简单的调试信息显示。对于复杂的UI,可以考虑使用游戏自带的UI系统(如果暴露了的话)或者更高级的UI框架(如Unity官方的UI Toolkit,但这需要游戏包含相关程序集)。
  3. 对象持久化: 用new GameObject()创建的对象,务必记得DontDestroyOnLoad(),否则在切换场景时它会被销毁,导致你的插件功能失效。
  4. 错误处理: 对任何可能为null的对象(如FindObjectOfType的结果)进行判空。健壮的插件不应该因为一个意外的null引用而导致游戏崩溃。

6. 调试、测试与发布全流程

6.1 本地调试与日志追踪

调试BepInEx插件最直接的方式就是通过日志。除了使用Plugin.Log,你还可以通过BepInEx的日志查看器来实时监控。

  1. 控制台输出: 如果游戏是通过BepInEx的启动器(如doorstop_config.ini配置)启动的,并且游戏本身有控制台窗口(或通过winhttp.dll等方式启用了控制台),那么Log.LogInfo等信息会直接打印在控制台。
  2. 日志文件: 所有日志都会写入BepInEx/LogOutput.log文件。这是最可靠的记录。
  3. BepInEx控制台: 一些工具(如BepInEx自带的BepInEx.ConfigurationManager插件)可以提供游戏内的控制台,方便查看日志。

调试技巧

  • 在关键逻辑分支处添加详细的日志,包括变量值。
  • 使用Log.LogDebug输出更详细的信息,并在发布版本中通过修改BepInEx的日志等级配置来关闭它们,避免日志文件过大。
  • 如果插件导致游戏崩溃,第一时间检查LogOutput.log文件的末尾,通常会有堆栈跟踪信息。

6.2 插件测试流程

  1. 编译: 在Visual Studio中生成你的项目(通常是Ctrl+Shift+B)。
  2. 部署: 将编译生成的MyFirstBepInExPlugin.dll(位于项目的bin\Debug\bin\Release\目录下)复制到游戏的BepInEx\plugins文件夹下。如果该文件夹不存在,请先运行一次已安装BepInEx的游戏,它会自动生成。
    • 正确的路径示例:Steam\steamapps\common\Risk of Rain 2\BepInEx\plugins\MyFirstBepInExPlugin\MyFirstBepInExPlugin.dll
    • 建议为你的插件创建一个子文件夹,这样更整洁。
  3. 启动游戏: 正常启动游戏。观察游戏启动时控制台或日志中是否有你的插件加载信息。
  4. 功能验证: 在游戏中触发你插件设计的功能(比如捡金币),观察游戏内效果和日志输出是否符合预期。
  5. 配置测试: 修改BepInEx\config下的配置文件,重启游戏或触发配置重载(如果支持),检查功能是否随之改变。

6.3 打包与发布

当你确认插件稳定后,就可以准备分享给其他玩家了。

  1. 清理与编译Release版本: 在Visual Studio中将解决方案配置切换到“Release”,然后重新生成。这会对代码进行优化,并移除调试符号,使DLL文件更小。
  2. 创建发布包: 通常是一个压缩包(ZIP),包含以下内容:
    • README.md: 必含!用清晰的语言说明插件功能、安装方法、配置说明、已知问题等。
    • CHANGELOG.md: 版本更新日志。
    • plugins/YourPluginName/YourPlugin.dll: 你的插件主文件。
    • plugins/YourPluginName/icon.png(可选): 插件图标,供模组管理器显示。
    • config/(可选): 如果插件有默认配置文件,可以包含一个示例。
    • manifest.json(如果发布到Thunderstore等模组平台): 这是模组平台的元数据文件,定义了插件名、版本、作者、依赖等。
  3. 选择发布平台
    • Thunderstore: 许多热门Unity游戏(如《雨中冒险2》、《英灵神殿》)的模组社区都使用它。你需要注册账号,按照平台指引上传你的ZIP包。
    • GitHub Releases: 适合技术向玩家和作为备用下载源。可以很好地管理版本和issue。
    • 游戏专属论坛/社区: 如Steam创意工坊、Reddit板块、Discord频道等。

6.4 常见问题排查速查表

在开发和测试过程中,你几乎一定会遇到下面这些问题。这里提供一个快速排查指南。

问题现象可能原因解决方案
游戏启动时BepInEx控制台一闪而过/插件未加载1. BepInEx安装不正确。
2. 插件DLL放错了位置。
3. 插件依赖的BepInEx或Harmony版本与游戏不匹配。
1. 检查游戏根目录下是否有BepInEx文件夹及doorstop_config.ini等文件。
2. 确认DLL放在BepInEx/plugins或其子目录下。
3. 检查游戏使用的BepInEx版本,并确保你的插件项目引用了兼容版本的NuGet包。
插件已加载,但日志显示Harmony补丁失败1. 目标方法签名(参数、返回类型)不匹配。
2. 目标类或方法名错误(包括重载)。
3. 游戏更新,方法已改名或移除。
1. 用dnSpy再次确认目标方法的完整签名(包括参数类型和返回类型)。
2. 检查[HarmonyPatch]特性中的类名和方法名是否正确。对于重载方法,需要使用[HarmonyPatch(Type, new Type[] {参数类型数组})]来指定。
3. 更新你的插件以适应新版本游戏。
游戏运行正常,但插件功能未生效1. 补丁逻辑有误(如Prefix返回了false阻止了原方法)。
2. 配置未正确加载或使用。
3. 代码执行时机不对(如访问的对象为null)。
1. 在补丁方法开始处添加日志,确认补丁是否被执行。
2. 检查配置文件路径和内容,在代码中打印配置值确认。
3. 添加更多的空值检查和日志,确认代码执行路径。考虑使用StartCoroutine延迟初始化。
修改配置后,插件行为未改变1. 配置值改变事件未正确订阅或处理。
2. 插件代码中缓存了旧的配置值,未读取最新值。
1. 确保在BindConfigurations中订阅了SettingChanged事件,并在事件处理中更新静态变量。
2. 避免在局部变量中长期保存配置值,每次都从ConfigEntry.Value或你的静态设置类中读取。
游戏崩溃,日志末尾有NullReferenceException代码中访问了未初始化的Unity对象(如FindObjectOfType返回null)。1. 在所有可能访问Unity对象的地方添加严格的null检查。
2. 确保你的组件在合适的时机(如场景加载完成后)再去查找对象。使用GameObject.FindFindObjectOfType时要意识到它们的性能开销和可能返回null。

开发BepInEx插件的旅程,就像是在一个已经建好的乐高城堡上添加自己设计的模块。你需要细心观察原有结构(用dnSpy分析),使用安全的连接方式(Harmony),并确保你的模块不会让城堡倒塌(稳定性)。这个过程充满了挑战,但当看到自己的创意在喜爱的游戏中变为现实,那种成就感是无与伦比的。从最简单的数值修改开始,逐步尝试创建UI、监听事件,最终你将能打造出功能丰富、影响深远的模组。记住,社区是你的后盾,遇到难题时,去游戏的模组Discord或相关论坛提问,通常会有热心的前辈为你解答。