本文将通过目标学习法带你快速上手 CGH 玩法开发,在本文中你将与笔者一同开发一个《药剂工艺》的郊狼联动游戏模组
准备工作
确保你的电脑上安装了以下必备软件:
《药剂工艺:炼金模拟器》,并已经安装了 BepInEx(教程)
Coyote Game Hub
.NET SDK
可进行 C# 开发的 IDE(可选:Visual Studio Community, JetBrains Rider, Visual Studio Code)
开发环境准备
配置 BepInEx
为了更方便地进行调试,建议启用 BepInEx Console
进入 /BepInEx/config
文件夹找到 BepInEx.cfg
,找到并编辑下面的内容:
[Logging.Console]
## Enables showing a console for log output.
# Setting type: Boolean
# Default value: false
Enabled = true
安装 BepInEx 插件模板
在 PowerShell 中执行下面的命令:
dotnet new install BepInEx.Templates::2.0.0-be.4 --nuget-source https://nuget.bepinex.dev/v3/index.json
初始化插件项目
dotnet new bepinex5plugin -n MyFirstPlugin -T <TFM> -U <Unity>
要初始化插件项目,需要先获取插件的 .NET 目标框架 和 Unity 版本,详细可以参考 BepInEx 的开发指南,这里直接给出:
TFM:netstandard2.1
Unity:2023.1.13
在合适位置右键打开 PowerShell,执行下面的命令:
dotnet new bepinex5plugin -n PotionCraftPlugin -T netstandard2.1 -U 2023.1.13
这条命令将新建一个 PotionCraftPlugin
文件夹,在其中生成下面两个文件:
现在你就可以使用 IDE 打开这个项目开始插件开发了
确定目标
在开发插件前我们先来确定目标 —— 炼药失败时执行一键开火进行惩罚
插件开发

程序结构
using BepInEx;
using BepInEx.Logging;
namespace PotionCraftPlugin;
[BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)]
public class Plugin : BaseUnityPlugin
{
internal static new ManualLogSource Logger;
private void Awake()
{
// Plugin startup logic
Logger = base.Logger;
Logger.LogInfo([imath:0]"Plugin {MyPluginInfo.PLUGIN_GUID} is loaded!");
}
}
整体大致是:
- 继承自
BaseUnityPlugin
的 Plugin
类
Plugin
类的各种特性
Plugin
类的各种方法
配置插件元数据
可以注意到下面一行代码
[BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)]
此属性表示这个类是 BepInEx 插件,并指定插件的元数据:
参数名称 | 描述 |
GUID | 插件的唯一标识符,建议使用反向域名表示法 |
Name | 人类可读的插件名称 |
Version | 插件版本 |
我们可以在 IDE 中右键解决方案,选择“属性”,进行配置:
GUID
:应用程序 > 程序集名称
Name
:程序集 > 产品
Version
:NuGet > 版本
在本文中 GUID
为 com.fangs.PotionCraftPlugi
配置插件依赖
在解决方案中右键新建一个 lib
文件夹,进入游戏的 \Potion Craft_Data\Managed
文件夹,在其中复制下面列出的文件到 lib
文件夹:
PotionCraft.AchievementsSystem.dll
PotionCraft.Core.dll
PotionCraft.ManagerSystem.dll
PotionCraft.SceneLoader.dll
PotionCraft.Scripts.dll
PotionCraft.Settings.dll
UnityEngine.CoreModule.dll
UnityEngine.dll
右键解决方案中的依赖项,选择“引用”,在弹出窗口中点击“添加自”,选择 bin
文件夹中的所有文件
对游戏中的方法进行补丁
我们需要用到 Harmony 库对游戏中的某些方法进行补丁,例如我们想在炼药失败时执行一键开火,那么我们需要补丁游戏的 ResetPotion
方法
我们先写一个 ResetPotion_Patch
方法,之后就在这里编写主要逻辑
public static void ResetPotion_Patch()
{
Logger.LogInfo("PotionManager.ResetPotion is called");
}
要想使用 Harmony 库对某个方法进行补丁,需要先了解:HarmonyPatch
特性和补丁方式
HarmonyPatch
特性
这里我们只需要用到最简单的用法:
[HarmonyPatch(typeof(PotionManager), "ResetPotion")]
第一个参数是要补丁的类型,第二个参数是要补丁的目标类
补丁方式
常用的补丁方式有两种:Prefix(前置补丁)和 Postfix(后置补丁)
前置补丁
在原方法前执行补丁方法,可以选择是否执行原方法
// 例子1
[HarmonyPrefix]
[HarmonyPatch(typeof(PotionManager), "ResetPotion")]
public static bool ResetPotion_Patch()
{
Logger.LogInfo("PotionManager.ResetPotion is called");
return true;
}
// 例子2
[HarmonyPrefix]
[HarmonyPatch(typeof(PotionManager), "ResetPotion")]
public static bool ResetPotion_Patch()
{
Logger.LogInfo("PotionManager.ResetPotion is called");
return false;
}
例子1在执行了 ResetPotion_Patch
方法后接着完成了对药剂的重置
而例子2在执行了 ResetPotion_Patch
方法后取消了对药剂的重置 ——

后置补丁
在目标方法执行结束后执行补丁方法,它可以使用 __result
参数接收目标的返回值
【阶段性成果】成功补丁游戏方法
对于我们要补丁的 ResetPotion
方法,前置补丁和后置补丁并无区别,这里使用了后置补丁:
[HarmonyPostfix]
[HarmonyPatch(typeof(PotionManager), "ResetPotion")]
public static void ResetPotion_Patch()
{
Logger.LogInfo("PotionManager.ResetPotion is called");
}
另外,需要在插件加载时应用所有补丁,在 Awake
方法中加上下面一行
Harmony.CreateAndPatchAll(typeof(Plugin));
现在我们的 Plugin.cs
是这样的:
using BepInEx;
using BepInEx.Logging;
using HarmonyLib;
using PotionCraft.ManagersSystem.Potion;
namespace PotionCraftPlugin;
[BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)]
public class Plugin : BaseUnityPlugin
{
internal static new ManualLogSource Logger;
[HarmonyPostfix]
[HarmonyPatch(typeof(PotionManager), "ResetPotion")]
public static void ResetPotion_Patch()
{
Logger.LogInfo("PotionManager.ResetPotion is called");
}
private void Awake()
{
// Plugin startup logic
Logger = base.Logger;
Logger.LogInfo([/imath:0]"Plugin {MyPluginInfo.PLUGIN_GUID} is loaded!");
Harmony.CreateAndPatchAll(typeof(Plugin));
Logger.LogInfo([imath:0]"Plugin {MyPluginInfo.PLUGIN_GUID} patch succeeded!");
}
}
编译插件
要编译插件只需要点击 IDE 窗口最上方的小槌头图标,编译成功后可以在 \PotionCraftPlugin\bin\Debug\netstandard2.1
找到模组的 dll
文件,将它放进游戏的 \BepInEx\plugins
文件夹,启动游戏即可
在游戏启动的过程中可以在 Console 看到下面的日志:
[Info : BepInEx] Loading [My first plugin 1.0.0]
[Info :My first plugin] Plugin com.fangs.PotionCraftPlugin is loaded!
[Info :My first plugin] Plugin com.fangs.PotionCraftPlugin patch succeeded!
进入游戏试试炼药失败一次

控制台输出 ——
[Info :My first plugin] PotionManager.ResetPotion is called
你也可以尝试一下使用前置补丁,并且试试拦截原方法的执行,这很有趣
了解 CGH 的 API
运行 CGH 服务端,打开 /api/docs
就可以查看最新版本 API 的文档了
若要调用这些 API,对 API 进行 HTTP 请求即可,下面是我们会用到的两个 API
获取游戏信息
GET /api/v2/game/{clientId}
请求参数
无
响应
{
"status": 1,
"code": "OK",
"strengthConfig": {
"strength": 5, // 基础强度
"randomStrength": 5 // 随机强度,(强度范围:[strength, strength+randomStrength])
},
"gameConfig": {
"strengthChangeInterval": [15, 30], // 随机强度变化间隔,单位:秒
"enableBChannel": false, // 是否启用B通道
"bChannelStrengthMultiplier": 1, // B通道强度倍数
"pulseId": "d6f83af0", // 当前波形列表,可能是string或者string[]
"pulseMode": "single", // 波形播放模式,single: 单个波形, sequence: 列表顺序播放, random: 随机播放
"pulseChangeInterval": 60
},
"clientStrength": {
"strength": 0, // 客户端当前强度
"limit": 20 // 客户端强度上限
},
"currentPulseId": "d6f83af0" // 当前正在播放的波形ID
}
一键开火
POST /api/v2/game/{clientId}/action/fire
请求参数
如果服务器配置 allowBroadcastToClients: true
,可以将请求地址中的 {clientId}
设置为 all
,将设置到所有客户端
以下是请求参数的类型定义:
{
"strength": 20, // 一键开火强度,最高40
"time": 5000, // (可选)一键开火时间,单位:毫秒,默认为5000,最高30000(30秒)
"override": false, // (可选)多次一键开火时,是否重置时间,true为重置时间,false为叠加时间,默认为false
"pulseId": "d6f83af0" // (可选)一键开火的波形ID
}
强度配置在服务端已做限制,不会超出范围
响应
{
"status": 1,
"code": "OK",
"message": "成功向 1 个游戏发送了一键开火指令",
"successClientIds": [
"3ab0773d-69d0-41af-b74b-9c6ce6507f65"
]
}
在插件中进行网络请求
.NET 提供了一个 System.Net.Http
命名空间来实现 HTTP 网络请求
GET 请求
我们可以封装一个 GET 请求方法:
public static bool HttpGetRequest(string url, out string response)
{
using (HttpClient client = new HttpClient())
{
try
{
HttpResponseMessage httpResponse = client.GetAsync(url).Result;
httpResponse.EnsureSuccessStatusCode();
response = httpResponse.Content.ReadAsStringAsync().Result;
return true;
}
catch (Exception ex)
{
Logger.LogError($"HTTP GET request failed: {ex.Message}");
response = null;
return false; // Request failed
}
}
}
现在我们只需要调用这个方法即可对 API 发起 GET 请求了 ——
string host = "http://127.0.0.1:8920";
string clientId = "3f698dcb-509d-4e18-b34a-572d7b0ac767";
string response;
if (HttpGetRequest([/imath:0]"{host}/api/v2/game/{clientId}", out response))
{
Logger.LogInfo([imath:0]"Connection to CHG server succeeded. Response: {response}");
}
else
{
Logger.LogWarning("Connection to CHG server failed.");
}
POST 请求
同理,我们可以封装一个 POST 请求方法来实现 POST 请求:
参数
url
(string
):要发送 GET 请求的完整 URL
postData
(string
):要 POST 的数据(JSON)
response
(out string
):一个 out
参数,用于接收请求成功后返回的字符串内容
返回值
public static bool HttpPostRequest(string url, string postData, out string response)
{
using (HttpClient client = new HttpClient())
{
try
{
HttpContent content = new StringContent(postData);
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
HttpResponseMessage httpResponse = client.PostAsync(url, content).Result;
httpResponse.EnsureSuccessStatusCode();
response = httpResponse.Content.ReadAsStringAsync().Result;
return true;
}
catch (Exception ex)
{
Logger.LogError($"HTTP POST request failed: {ex.Message}");
response = null;
return false;
}
}
}
示例:
string host = "http://127.0.0.1:8920";
string clientId = "3f698dcb-509d-4e18-b34a-572d7b0ac767";
string json = "{\"strength\": 20, \"duration\": 5000}";
string response;
if (HttpPostRequest([/imath:0]"{host}/api/v1/game/{clientId}/action/fire", json, out response))
{
Logger.LogInfo([imath:0]"HTTP POST request succeeded. Response: {response}");
}
else
{
Logger.LogWarning("HTTP POST request failed.");
}
【阶段性成果】通过 HTTP 请求 CGH API
为了确保 CGH 在正常运行,我们可以在插件加载阶段对 API 发起一个请求,测试连通性,若 CGH 未在线则后续不再发起请求:
private void Awake()
{
// Plugin startup logic
Logger = base.Logger;
Logger.LogInfo([/imath:0]"Plugin {MyPluginInfo.PLUGIN_GUID} is loaded!");
Harmony.CreateAndPatchAll(typeof(Plugin)
Logger.LogInfo([imath:0]"Plugin {MyPluginInfo.PLUGIN_GUID} patch succeeded!");
if (HttpGetRequest([/imath:0]"{host}/api/v2/game/{clientId}", out string response))
{
Logger.LogInfo([imath:0]"Connection to CHG server succeeded. Response: {response}");
isConnectedCGH = true;
}
else
{
Logger.LogWarning("Connection to CHG server failed.");
isConnectedCGH = false;
}
}
在炼药失败时对一键开火 API 发起 POST 请求:
[HarmonyPostfix]
[HarmonyPatch(typeof(PotionManager), "ResetPotion")]
public static void ResetPotion_Patch()
{
Logger.LogInfo("PotionManager.ResetPotion is called");
if (!isConnectedCGH)
{
Logger.LogInfo("NOT connected to CGH, no firing.");
return;
}
if (HttpPostRequest([/imath:0]"{host}/api/v2/game/{clientId}/action/fire", json, out string response))
{
Logger.LogInfo([imath:0]"POST request to CHG server succeeded. Response: {response}");
}
else
{
Logger.LogWarning("POST request to CHG server failed.");
}
}
完整的 Plugin.cs
如下:
using System;
using System.Net.Http;
using BepInEx;
using BepInEx.Logging;
using HarmonyLib;
using PotionCraft.ManagersSystem.Potion;
namespace PotionCraftPlugin;
[BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)]
public class Plugin : BaseUnityPlugin
{
internal static new ManualLogSource Logger;
static string host = "http://127.0.0.1:8920";
static string clientId = "3f698dcb-509d-4e18-b34a-572d7b0ac767";
static string json = "{\"strength\": 20, \"duration\": 5000}";
static bool isConnectedCGH;
public static bool HttpGetRequest(string url, out string response)
{
using (HttpClient client = new HttpClient())
{
try
{
HttpResponseMessage httpResponse = client.GetAsync(url).Result;
httpResponse.EnsureSuccessStatusCode();
response = httpResponse.Content.ReadAsStringAsync().Result;
return true;
}
catch (Exception ex)
{
Logger.LogError($"HTTP GET request failed: {ex.Message}");
response = null;
return false; // Request failed
}
}
}
public static bool HttpPostRequest(string url, string postData, out string response)
{
using (HttpClient client = new HttpClient())
{
try
{
HttpContent content = new StringContent(postData);
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
HttpResponseMessage httpResponse = client.PostAsync(url, content).Result;
httpResponse.EnsureSuccessStatusCode();
response = httpResponse.Content.ReadAsStringAsync().Result;
return true;
}
catch (Exception ex)
{
Logger.LogError($"HTTP POST request failed: {ex.Message}");
response = null;
return false;
}
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(PotionManager), "ResetPotion")]
public static void ResetPotion_Patch()
{
Logger.LogInfo("PotionManager.ResetPotion is called");
if (!isConnectedCGH)
{
Logger.LogInfo("NOT connected to CGH, no firing.");
return;
}
if (HttpPostRequest([/imath:0]"{host}/api/v2/game/{clientId}/action/fire", json, out string response))
{
Logger.LogInfo([imath:0]"POST request to CHG server succeeded. Response: {response}");
}
else
{
Logger.LogWarning("POST request to CHG server failed.");
}
}
private void Awake()
{
// Plugin startup logic
Logger = base.Logger;
Logger.LogInfo([/imath:0]"Plugin {MyPluginInfo.PLUGIN_GUID} is loaded!");
Harmony.CreateAndPatchAll(typeof(Plugin));
Logger.LogInfo([imath:0]"Plugin {MyPluginInfo.PLUGIN_GUID} patch succeeded!");
if (HttpGetRequest([/imath:0]"{host}/api/v2/game/{clientId}", out string response))
{
Logger.LogInfo([imath:0]"Connection to CHG server succeeded. Response: {response}");
isConnectedCGH = true;
}
else
{
Logger.LogWarning("Connection to CHG server failed.");
isConnectedCGH = false;
}
}
}
[Info : BepInEx] Loading [My first plugin 1.0.0]
[Info :My first plugin] Plugin com.fangs.PotionCraftPlugin is loaded!
[Info :My first plugin] Plugin com.fangs.PotionCraftPlugin patch succeeded!
[Info :My first plugin] Connection to CHG server succeeded. Response: {"status":1,"code":"OK","strengthConfig":null,"gameConfig":{"fireStrengthLimit":100,"strengthChangeInterval":[15,30],"enableBChannel":false,"bChannelStrengthMultiplier":1,"pulseId":"bd272001","firePulseId":"dc033dc1","pulseMode":"single","pulseChangeInterval":60},"clientStrength":null,"currentPulseId":""}
... ...
[Info :My first plugin] PotionManager.ResetPotion is called
[Info :My first plugin] POST request to CHG server succeeded. Response: {"status":1,"code":"OK","message":"成功向 1 个游戏发送了一键开火指令","successClientIds":["3f698dcb-509d-4e18-b34a-572d7b0ac767"],"warnings":[]}
插件配置文件
硬编码让用户自行编译插件并不现实,所以我们需要为用户提供一种配置插件的方法
BepInEx 提供了 BepInEx.Configuration
命名空间,允许插件创建、读取、管理配置文件
插件的配置文件将储存在 \BepInEx\config\<GUID>.cfg
,要创建/读取配置的值,首先需要使用 Bind<T>(String, String, T, String)
来初始化变量,初始化通常在 Awake
方法中完成
例如:
using BepInEx;
using BepInEx.Configuration;
namespace PotionCraftPlugin;
[BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)]
public class Plugin : BaseUnityPlugin
{
internal static new ManualLogSource Logger;
private static ConfigEntry<String> configString;
private void Awake()
{
// Plugin startup logic
Logger = base.Logger;
configString = Config.Bind(
"General",
"configString",
"Hello, World!",
"This is config string");
Logger.LogInfo([/imath:0]"Plugin {MyPluginInfo.PLUGIN_GUID} is loaded!");
}
【最终成果】
现在我们来尝试使用配置文件储存我们插件的一些配置:
using System;
using System.Net.Http;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using PotionCraft.ManagersSystem.Potion;
namespace PotionCraftPlugin;
[BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)]
public class Plugin : BaseUnityPlugin
{
internal static new ManualLogSource Logger;
private static ConfigEntry<String> configConnectCode;
private static string ClientId => configConnectCode.Value.Split('@')[0];
private static string ServerUrl => configConnectCode.Value.Split('@')[1];
private static ConfigEntry<int> configFireStrength;
private static ConfigEntry<int> configFireTime;
static bool isConnectedCGH;
public static bool HttpGetRequest(string url, out string response)
{
using (HttpClient client = new HttpClient())
{
try
{
HttpResponseMessage httpResponse = client.GetAsync(url).Result;
httpResponse.EnsureSuccessStatusCode();
response = httpResponse.Content.ReadAsStringAsync().Result;
return true;
}
catch (Exception ex)
{
Logger.LogError([imath:0]"HTTP GET request failed: {ex.Message}");
response = null;
return false; // Request failed
}
}
}
public static bool HttpPostRequest(string url, string postData, out string response)
{
using (HttpClient client = new HttpClient())
{
try
{
HttpContent content = new StringContent(postData);
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
HttpResponseMessage httpResponse = client.PostAsync(url, content).Result;
httpResponse.EnsureSuccessStatusCode();
response = httpResponse.Content.ReadAsStringAsync().Result;
return true;
}
catch (Exception ex)
{
Logger.LogError([/imath:0]"HTTP POST request failed: {ex.Message}");
response = null;
return false;
}
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(PotionManager), "ResetPotion")]
public static void ResetPotion_Patch()
{
Logger.LogInfo("PotionManager.ResetPotion is called");
if (!isConnectedCGH)
{
Logger.LogInfo("NOT connected to CGH, no firing.");
return;
}
var json = [imath:0]"{{\"strength\": {configFireStrength.Value}, \"time\": {configFireTime.Value}}}";
if (HttpPostRequest([/imath:0]"{ServerUrl}/api/v2/game/{ClientId}/action/fire", json, out string response))
{
Logger.LogInfo([imath:0]"POST request to CHG server succeeded. Response: {response}");
}
else
{
Logger.LogWarning("POST request to CHG server failed.");
}
}
private void Awake()
{
// Plugin startup logic
Logger = base.Logger;
configConnectCode = Config.Bind(
"General",
"ConnectCode",
"xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx@http://127.0.0.1:8920",
"CHG Game Connect Code");
configFireStrength = Config.Bind(
"General",
"FireStrength",
20,
"Fire Strength");
configFireTime = Config.Bind(
"General",
"FireTime",
5000,
"Fire Time in milliseconds");
Logger.LogInfo([/imath:0]"Plugin {MyPluginInfo.PLUGIN_GUID} is loaded!");
Harmony.CreateAndPatchAll(typeof(Plugin));
Logger.LogInfo([imath:0]"Plugin {MyPluginInfo.PLUGIN_GUID} patch succeeded!");
if (HttpGetRequest([/imath:0]"{ServerUrl}/api/v2/game/{ClientId}", out string response))
{
Logger.LogInfo($"Connection to CHG server succeeded. Response: {response}");
isConnectedCGH = true;
}
else
{
Logger.LogWarning("Connection to CHG server failed.");
isConnectedCGH = false;
}
}
}
首次启动将生成配置文件:
## Settings file was created by plugin My first plugin v1.0.0
## Plugin GUID: com.fangs.PotionCraftPlugin
[General]
## CHG Game Connect Code
# Setting type: String
# Default value: xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx@http://127.0.0.1:8920
ConnectCode = xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx@http://127.0.0.1:8920
## Fire Strength
# Setting type: Int32
# Default value: 20
FireStrength = 20
## Fire Time in milliseconds
# Setting type: Int32
# Default value: 5000
FireTime = 5000
编辑配置文件后重新启动游戏即可体验
[Info : BepInEx] Loading [My first plugin 1.0.0]
[Info :My first plugin] Plugin com.fangs.PotionCraftPlugin is loaded!
[Info :My first plugin] Plugin com.fangs.PotionCraftPlugin patch succeeded!
[Info :My first plugin] Connection to CHG server succeeded. Response: {"status":1,"code":"OK","strengthConfig":{"strength":5,"randomStrength":5},"gameConfig":{"fireStrengthLimit":100,"strengthChangeInterval":[15,30],"enableBChannel":false,"bChannelStrengthMultiplier":1,"pulseId":"bd272001","firePulseId":"dc033dc1","pulseMode":"single","pulseChangeInterval":60},"clientStrength":{"strength":0,"limit":20},"currentPulseId":"bd272001"}
... ...
[Info :My first plugin] PotionManager.ResetPotion is called
[Info :My first plugin] POST request to CHG server succeeded. Response: {"status":1,"code":"OK","message":"成功向 1 个游戏发送了一键开火指令","successClientIds":["3f698dcb-509d-4e18-b34a-572d7b0ac767"],"warnings":[]}
结语
至此,你已经成功开发了一个《药剂工艺》模组,这个模组可以通过 CGH 与郊狼互联,当炼药失败时执行一键开火惩罚,同时你为了方便用户使用,为用户提供了一个配置文件来配置如连接码的配置项
当然,这个作为 Demo 的插件目前并不完善,欢迎继续完善,你可以将你的改进发布在评论区,期待你的作业
值得一提的是:BepInEx 是 Unity 游戏的通用插件框架,你也可以尝试为其他 Unity 游戏开发插件,实现与郊狼互联
最后,感谢你的阅读