最完整BepInEx入门教程:零基础开发Unity插件
引言:为什么选择BepInEx?
你还在为Unity游戏插件开发无从下手而烦恼吗?作为Unity生态中最强大的插件框架之一,BepInEx(Bepis Injector Extensible)提供了跨平台、高兼容性的插件开发环境,支持Mono、IL2CPP和.NET框架游戏。本文将从环境搭建到高级功能,带你从零掌握BepInEx插件开发,让你轻松实现游戏功能扩展。
读完本文你将获得:
- BepInEx完整开发环境搭建指南
- 插件项目从零创建到打包发布的全流程
- 配置系统、日志系统、热键系统的核心应用
- Harmony补丁技术实现游戏逻辑修改
- 调试与排错的专业技巧
- 5个实用插件示例代码(含完整注释)
BepInEx框架解析
核心架构概览
BepInEx采用分层架构设计,确保插件开发的灵活性和游戏兼容性:
支持平台与兼容性
| 游戏引擎 | Windows | macOS | Linux | 移动端 |
|---|---|---|---|---|
| Unity Mono | ✔️ 稳定 | ✔️ 稳定 | ✔️ 稳定 | ❌ |
| Unity IL2CPP | ✔️ 测试 | ❌ | ✔️ 实验性 | ❌ |
| .NET/XNA | ✔️ 稳定 | ✔️ 有限 | ✔️ 有限 | ❌ |
⚠️ 注意:目前IL2CPP后端仍处于测试阶段,部分高级功能可能受限。生产环境建议优先选择Mono后端。
开发环境搭建
基础工具准备
| 工具 | 用途 | 国内下载链接 |
|---|---|---|
| .NET SDK 6.0+ | C#编译环境 | 阿里云镜像 |
| Visual Studio 2022 | 集成开发环境 | 微软中国官网 |
| Rider | 跨平台IDE | JetBrains中国 |
| dnSpy | 反编译与调试 | 蓝奏云镜像 |
| Unity Hub | Unity环境管理 | Unity中国官网 |
项目创建步骤
- 克隆官方模板
git clone https://gitcode.com/GitHub_Trending/be/BepInEx.git
cd BepInEx
- 编译基础库
dotnet build BepInEx.sln -c Release
- 创建插件项目
dotnet new classlib -n MyFirstPlugin -f net48
cd MyFirstPlugin
- 添加项目引用
dotnet add reference ../BepInEx.Core/BepInEx.Core.csproj
dotnet add reference ../Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj
插件开发核心技术
插件基础结构
每个BepInEx插件都遵循以下基本结构,继承自BaseUnityPlugin并使用BepInPlugin特性标记:
using BepInEx;
using BepInEx.Logging;
using UnityEngine;
// 插件元数据:GUID必须唯一,建议使用"作者.插件名"格式
[BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)]
public class MyFirstPlugin : BaseUnityPlugin
{
// 日志源实例,用于输出调试信息
private ManualLogSource _logger;
// 插件加载时调用
private void Awake()
{
_logger = Logger;
_logger.LogInfo($"插件 {PluginInfo.PLUGIN_GUID} 已加载!");
// 初始化配置
InitializeConfig();
// 注册热键
RegisterHotkeys();
// 应用Harmony补丁
ApplyHarmonyPatches();
}
// 游戏更新时调用(每帧)
private void Update()
{
// 实现每帧逻辑
}
// 插件卸载时调用
private void OnDestroy()
{
_logger.LogInfo($"插件 {PluginInfo.PLUGIN_GUID} 已卸载!");
}
// 其他方法...
}
// 插件信息常量
public static class PluginInfo
{
public const string PLUGIN_GUID = "com.example.myfirstplugin";
public const string PLUGIN_NAME = "My First Plugin";
public const string PLUGIN_VERSION = "1.0.0";
}
配置系统详解
BepInEx提供了强大的配置系统,支持自动生成配置文件并实时监听变更:
private ConfigEntry<float> _moveSpeed;
private ConfigEntry<KeyboardShortcut> _menuHotkey;
private ConfigEntry<bool> _enableFeature;
private void InitializeConfig()
{
// 基础数值配置
_moveSpeed = Config.Bind<float>(
"游戏设置", // 配置节名称
"移动速度", // 配置项名称
5.0f, // 默认值
new ConfigDescription(
"玩家移动速度倍率", // 描述
new AcceptableValueRange<float>(1.0f, 10.0f) // 取值范围限制
)
);
// 热键配置
_menuHotkey = Config.Bind<KeyboardShortcut>(
"控制设置",
"菜单热键",
new KeyboardShortcut(KeyCode.F5),
"打开插件菜单的快捷键"
);
// 开关配置
_enableFeature = Config.Bind<bool>(
"功能设置",
"启用新功能",
true,
"是否启用插件的实验性功能"
);
// 监听配置变更
_enableFeature.SettingChanged += (sender, args) =>
{
_logger.LogInfo($"新功能已{(bool)_enableFeature.Value ? "启用" : "禁用"}");
// 应用配置变更逻辑
};
}
生成的配置文件(位于BepInEx/config/com.example.myfirstplugin.cfg)自动包含说明:
## Settings file was created by plugin My First Plugin v1.0.0
## Plugin GUID: com.example.myfirstplugin
[游戏设置]
## 玩家移动速度倍率
# Setting type: Single
# Acceptable value range: From 1 to 10
移动速度 = 5
[控制设置]
## 打开插件菜单的快捷键
# Setting type: KeyboardShortcut
菜单热键 = F5
[功能设置]
## 是否启用插件的实验性功能
# Setting type: Boolean
启用新功能 = true
日志系统应用
BepInEx提供多级别日志系统,帮助开发者调试和监控插件运行状态:
private void LoggingExamples()
{
// 不同级别的日志
Logger.LogDebug("这是调试信息,仅开发时可见");
Logger.LogInfo("这是普通信息,记录正常操作");
Logger.LogWarning("这是警告信息,提示潜在问题");
Logger.LogError("这是错误信息,记录非致命错误");
Logger.LogFatal("这是致命错误,通常导致插件崩溃");
// 格式化日志
var playerName = "Player1";
var score = 100;
Logger.LogInfo($"玩家 {playerName} 获得了 {score} 分");
// 创建自定义日志源
var customLogger = Logger.CreateLogSource("战斗系统");
customLogger.LogInfo("自定义日志源可以更好地组织日志输出");
}
日志输出位置:
- 控制台窗口
BepInEx/LogOutput.log文件- Unity编辑器控制台(开发时)
Harmony补丁技术
使用Harmony库可以修改游戏现有方法,实现功能扩展或修复:
using HarmonyLib;
using UnityEngine;
// 定义补丁类
[HarmonyPatch(typeof(PlayerController), "Jump")] // 目标类和方法
public static class PlayerJumpPatch
{
// 在目标方法执行前调用
static bool Prefix(PlayerController __instance, ref float ___jumpForce)
{
// 修改跳跃力(增加50%)
___jumpForce *= 1.5f;
Logger.LogInfo($"修改跳跃力为: {___jumpForce}");
// 返回true继续执行原方法,false则阻止原方法执行
return true;
}
// 在目标方法执行后调用
static void Postfix(PlayerController __instance, bool __result)
{
if (__result) // 如果跳跃成功
{
Logger.LogInfo("玩家跳跃成功!");
// 添加跳跃特效
Object.Instantiate(jumpEffectPrefab, __instance.transform.position, Quaternion.identity);
}
}
}
// 在插件中应用补丁
private Harmony _harmony;
private void ApplyHarmonyPatches()
{
_harmony = new Harmony(PluginInfo.PLUGIN_GUID);
_harmony.PatchAll(typeof(PlayerJumpPatch));
Logger.LogInfo("Harmony补丁已应用");
}
// 插件卸载时取消补丁
private void OnDestroy()
{
_harmony?.UnpatchSelf();
}
常用补丁类型:
Prefix:目标方法执行前调用Postfix:目标方法执行后调用Transpiler:修改目标方法的IL代码Finalizer:目标方法执行完成后(包括异常情况)调用
高级功能实现
热键系统
BepInEx提供KeyboardShortcut结构,轻松实现自定义热键:
private void RegisterHotkeys()
{
// 在Update中检查热键
void Update()
{
if (_menuHotkey.Value.IsDown())
{
OpenMenu(); // 打开菜单
}
// 组合键示例 (Ctrl+Shift+F1)
if (new KeyboardShortcut(KeyCode.F1, KeyCode.LeftControl, KeyCode.LeftShift).IsPressed())
{
ShowDebugPanel(); // 显示调试面板
}
}
}
// 热键冲突检测
private void CheckHotkeyConflicts()
{
var conflictingHotkeys = new List<KeyboardShortcut>
{
new KeyboardShortcut(KeyCode.F5), // 与我们的菜单热键冲突
new KeyboardShortcut(KeyCode.Escape)
};
foreach (var hotkey in conflictingHotkeys)
{
if (hotkey == _menuHotkey.Value)
{
Logger.LogWarning($"热键冲突: {hotkey} 已被其他插件使用");
}
}
}
协程与异步操作
在Unity环境中使用协程处理异步任务:
// 启动协程示例
private void Start()
{
StartCoroutine(DelayedAction(3.0f, () =>
{
Logger.LogInfo("3秒后执行的操作");
}));
StartCoroutine(DownloadFileCoroutine("https://example.com/data.json", ProcessData));
}
// 延迟执行协程
IEnumerator DelayedAction(float delaySeconds, Action action)
{
yield return new WaitForSeconds(delaySeconds);
action?.Invoke();
}
// 异步下载文件协程
IEnumerator DownloadFileCoroutine(string url, Action<string> onComplete)
{
using (var webRequest = UnityEngine.Networking.UnityWebRequest.Get(url))
{
yield return webRequest.SendWebRequest();
if (webRequest.result == UnityEngine.Networking.UnityWebRequest.Result.Success)
{
onComplete?.Invoke(webRequest.downloadHandler.text);
}
else
{
Logger.LogError($"下载失败: {webRequest.error}");
}
}
}
// 处理下载数据
private void ProcessData(string data)
{
Logger.LogInfo($"下载的数据: {data}");
// 解析JSON等数据处理
}
配置文件管理
高级配置管理技巧,包括配置文件的导入导出和动态更新:
private void AdvancedConfigManagement()
{
// 手动保存配置
Config.Save();
// 监听配置文件修改
Config.ConfigReloaded += (sender, args) =>
{
Logger.LogInfo("配置文件已重新加载");
// 重新应用配置
ApplyConfigChanges();
};
// 导出配置
var configPath = Path.Combine(Paths.ConfigPath, $"{PluginInfo.PLUGIN_GUID}.cfg");
File.Copy(configPath, Path.Combine(Paths.PluginPath, "config_backup.cfg"), overwrite: true);
// 导入配置
var importPath = Path.Combine(Paths.PluginPath, "config_import.cfg");
if (File.Exists(importPath))
{
File.Copy(importPath, configPath, overwrite: true);
Config.Reload(); // 重新加载配置
}
}
private void ApplyConfigChanges()
{
// 重新应用所有配置项
Logger.LogInfo($"应用新的移动速度: {_moveSpeed.Value}");
// 其他配置应用逻辑...
}
调试与排错技巧
调试环境设置
- Visual Studio调试配置:
<!-- .vscode/launch.json -->
{
"version": "0.2.0",
"configurations": [
{
"name": "附加到游戏进程",
"type": "clr",
"request": "attach",
"processId": "${command:pickProcess}",
"justMyCode": false,
"symbolOptions": {
"searchPaths": ["${workspaceFolder}/bin/Debug"]
}
}
]
}
- 启用详细日志: 编辑
BepInEx/config/BepInEx.cfg:
[Logging]
# 设置日志级别为Debug
LogLevel = Debug
[Preloader]
# 启用预加载器调试日志
DebugLog = true
- 常用调试技巧:
- 使用
System.Diagnostics.Debugger.Break()设置断点 - 通过
Logger.LogInfo($"变量值: {variable}")输出调试信息 - 使用dnSpy动态调试已编译插件
常见问题解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 插件未加载 | GUID冲突或依赖缺失 | 检查日志中的GUID冲突,确保所有依赖已安装 |
| Harmony补丁失败 | 方法签名不匹配 | 使用HarmonyLib.Tools验证方法签名,确保参数和返回类型匹配 |
| 配置文件不生成 | 未调用Config.Bind | 确保在Awake或Start中绑定所有配置项 |
| 日志无输出 | 日志级别设置过高 | 在BepInEx.cfg中降低日志级别至Info或Debug |
| 游戏崩溃 | 空引用异常 | 使用空值检查,如if (obj != null) { ... } |
插件打包与发布
打包流程
- 创建发布脚本:
#!/bin/bash
# build_plugin.sh
# 编译项目
dotnet build -c Release
# 创建输出目录
mkdir -p ./dist/MyFirstPlugin
# 复制插件文件
cp ./bin/Release/net48/MyFirstPlugin.dll ./dist/MyFirstPlugin/
cp ./bin/Release/net48/MyFirstPlugin.pdb ./dist/MyFirstPlugin/ # 调试符号(可选)
# 复制依赖项(如果需要)
# cp ../libs/SomeDependency.dll ./dist/MyFirstPlugin/
# 复制配置文件模板
cp ./config_template.cfg ./dist/MyFirstPlugin/com.example.myfirstplugin.cfg
# 创建压缩包
cd dist
zip -r MyFirstPlugin_v1.0.0.zip MyFirstPlugin/
- 手动打包步骤:
- 编译插件为Release版本
- 创建插件文件夹(通常以GUID命名)
- 复制DLL文件和配置模板
- 压缩为ZIP包,包含版本号
发布渠道
- Nexus Mods:主流游戏 mods 分享平台
- Steam创意工坊:针对Steam游戏
- GitHub Releases:适合技术用户
- 国内平台:如3DM论坛、NGA插件区
发布时应包含:
- 插件功能说明
- 安装步骤
- 配置说明
- 变更日志
- 截图或视频演示(可选)
实战示例:5个实用插件模板
1. 简单配置插件
[BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)]
public class SimpleConfigPlugin : BaseUnityPlugin
{
private ConfigEntry<float> _volume;
private ConfigEntry<KeyboardShortcut> _muteHotkey;
private void Awake()
{
// 配置音量
_volume = Config.Bind<float>(
"音频设置",
"音量",
0.7f,
new ConfigDescription(
"游戏主音量",
new AcceptableValueRange<float>(0f, 1f)
)
);
// 配置静音热键
_muteHotkey = Config.Bind<KeyboardShortcut>(
"控制设置",
"静音热键",
new KeyboardShortcut(KeyCode.M, KeyCode.LeftControl),
"快速静音的快捷键"
);
Logger.LogInfo("简单配置插件已加载!");
}
private void Update()
{
if (_muteHotkey.Value.IsDown())
{
var newVolume = _volume.Value > 0 ? 0 : 0.7f;
_volume.Value = newVolume;
Logger.LogInfo(newVolume > 0 ? "取消静音" : "已静音");
}
}
}
2. UI显示插件
using UnityEngine;
using UnityEngine.UI;
[BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)]
public class UiDisplayPlugin : BaseUnityPlugin
{
private GameObject _uiPanel;
private Text _infoText;
private void Awake()
{
// 创建UI面板
_uiPanel = new GameObject("PluginUI");
_uiPanel.transform.SetParent(GameObject.Find("Canvas").transform);
// 添加RectTransform
var rect = _uiPanel.AddComponent<RectTransform>();
rect.anchoredPosition = new Vector2(10, -10);
rect.sizeDelta = new Vector2(300, 100);
// 添加背景
var image = _uiPanel.AddComponent<Image>();
image.color = new Color(0, 0, 0, 0.7f); // 半透明黑色
// 添加文本
_infoText = _uiPanel.AddComponent<Text>();
_infoText.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
_infoText.color = Color.white;
_infoText.fontSize = 14;
_infoText.alignment = TextAnchor.UpperLeft;
_infoText.margin = new RectOffset(5, 5, 5, 5);
// 更新显示信息
UpdateInfoText();
Logger.LogInfo("UI显示插件已加载!");
}
private void UpdateInfoText()
{
_infoText.text = $"FPS: {Mathf.Round(1 / Time.deltaTime)}\n" +
$"位置: {PlayerController.Instance.transform.position}";
}
private void Update()
{
if (_uiPanel != null)
UpdateInfoText();
}
private void OnDestroy()
{
Destroy(_uiPanel);
}
}
3. Harmony补丁插件
using HarmonyLib;
[BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)]
public class HarmonyPatchPlugin : BaseUnityPlugin
{
private Harmony _harmony;
private void Awake()
{
_harmony = new Harmony(PluginInfo.PLUGIN_GUID);
// 应用所有补丁
_harmony.PatchAll(typeof(HealthPatch));
_harmony.PatchAll(typeof(DamagePatch));
Logger.LogInfo("Harmony补丁插件已加载!");
}
private void OnDestroy()
{
_harmony.UnpatchSelf();
}
}
// 生命值显示补丁
[HarmonyPatch(typeof(PlayerStats), "SetHealth")]
public static class HealthPatch
{
static void Postfix(PlayerStats __instance, float value)
{
Logger.LogInfo($"玩家生命值变更为: {value}");
// 在屏幕显示生命值
HUD.ShowNotification($"生命值: {value}/{__instance.maxHealth}");
}
}
// 伤害倍率补丁
[HarmonyPatch(typeof(Weapon), "CalculateDamage")]
public static class DamagePatch
{
static void Prefix(ref float damage)
{
// 增加50%伤害
damage *= 1.5f;
}
}
4. 配置菜单插件
using UnityEngine;
using UnityEngine.UI;
[BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)]
public class ConfigMenuPlugin : BaseUnityPlugin
{
private GameObject _menuWindow;
private ConfigEntry<KeyboardShortcut> _menuHotkey;
private void Awake()
{
// 注册热键
_menuHotkey = Config.Bind<KeyboardShortcut>(
"控制",
"菜单热键",
new KeyboardShortcut(KeyCode.F1),
"打开配置菜单的快捷键"
);
// 创建菜单UI
CreateMenuUI();
_menuWindow.SetActive(false); // 默认隐藏
Logger.LogInfo("配置菜单插件已加载!按F1打开菜单");
}
private void Update()
{
// 切换菜单显示
if (_menuHotkey.Value.IsDown())
{
_menuWindow.SetActive(!_menuWindow.activeSelf);
}
}
private void CreateMenuUI()
{
// 创建菜单窗口
_menuWindow = new GameObject("ConfigMenu");
var canvas = _menuWindow.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
_menuWindow.AddComponent<CanvasScaler>();
_menuWindow.AddComponent<GraphicRaycaster>();
// 创建背景面板
var panel = new GameObject("Panel");
panel.transform.SetParent(_menuWindow.transform);
var rect = panel.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(400, 300);
rect.anchoredPosition = Vector2.zero;
panel.AddComponent<Image>().color = new Color(0.1f, 0.1f, 0.1f, 0.9f);
// 创建标题
var title = new GameObject("Title");
title.transform.SetParent(panel.transform);
title.AddComponent<RectTransform>().sizeDelta = new Vector2(380, 30);
title.GetComponent<RectTransform>().anchoredPosition = new Vector2(0, 130);
var text = title.AddComponent<Text>();
text.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
text.text = "插件配置菜单";
text.fontSize = 20;
text.alignment = TextAnchor.MiddleCenter;
text.color = Color.white;
// 添加更多UI元素...
}
}
5. 保存数据管理插件
using System.IO;
using System.Text.Json;
[BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)]
public class SaveDataPlugin : BaseUnityPlugin
{
private string _savePath;
private PlayerData _playerData;
// 数据结构
public class PlayerData
{
public int TotalKills { get; set; } = 0;
public float BestTime { get; set; } = float.MaxValue;
public string LastPlayed { get; set; } = "";
public int[] UnlockedItems { get; set; } = new int[0];
}
private void Awake()
{
// 设置保存路径
_savePath = Path.Combine(Paths.GameRootPath, "BepInEx", "save_data.json");
// 加载数据
LoadData();
// 注册事件监听
GameEvents.OnEnemyKilled += OnEnemyKilled;
GameEvents.OnLevelCompleted += OnLevelCompleted;
Logger.LogInfo("保存数据管理插件已加载!");
}
private void OnDestroy()
{
// 保存数据
SaveData();
// 取消事件监听
GameEvents.OnEnemyKilled -= OnEnemyKilled;
GameEvents.OnLevelCompleted -= OnLevelCompleted;
}
private void LoadData()
{
try
{
if (File.Exists(_savePath))
{
var json = File.ReadAllText(_savePath);
_playerData = JsonSerializer.Deserialize<PlayerData>(json);
Logger.LogInfo("数据加载成功!");
}
else
{
_playerData = new PlayerData();
Logger.LogInfo("创建新的保存数据!");
}
}
catch (Exception e)
{
Logger.LogError($"数据加载失败: {e.Message}");
_playerData = new PlayerData();
}
}
private void SaveData()
{
try
{
_playerData.LastPlayed = System.DateTime.Now.ToString();
var json = JsonSerializer.Serialize(_playerData, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(_savePath, json);
Logger.LogInfo("数据保存成功!");
}
catch (Exception e)
{
Logger.LogError($"数据保存失败: {e.Message}");
}
}
private void OnEnemyKilled()
{
_playerData.TotalKills++;
Logger.LogInfo($"总击杀数: {_playerData.TotalKills}");
}
private void OnLevelCompleted(float time)
{
if (time < _playerData.BestTime)
{
_playerData.BestTime = time;
Logger.LogInfo($"新纪录: {time}秒");
}
}
}
总结与进阶学习路径
知识点回顾
通过本文学习,你已经掌握了:
- BepInEx框架的核心架构和工作原理
- 插件开发的基础结构和生命周期
- 配置系统、日志系统和热键系统的使用
- Harmony补丁技术实现游戏方法修改
- 插件调试、打包和发布的完整流程
- 5个实用插件模板的实现
进阶学习路径
学习资源推荐
-
官方文档:
- BepInEx官方文档:https://docs.bepinex.dev
- HarmonyLib文档:https://harmony.pardeike.net
-
示例项目:
- BepInEx插件模板:https://gitcode.com/GitHub_Trending/be/BepInEx
- 社区插件集合:https://github.com/bbepis/BepisPlugins
-
开发工具:
- dnSpy:.NET反编译与调试工具
- ILSpy:C#代码反编译工具
- Unity Profiler:性能分析工具
-
社区支持:
- BepInEx Discord:https://discord.gg/MpFEDAg
- 插件开发论坛:https://forum.unity.com/
附录:常用API速查表
| 类别 | 常用API | 用途 |
|---|---|---|
| 插件基础 | BaseUnityPlugin | 插件基类,提供生命周期方法 |
BepInPlugin | 标记插件元数据的特性 | |
PluginInfo | 存储插件GUID、名称和版本 | |
| 配置系统 | Config.Bind<T>() | 绑定配置项 |
ConfigEntry<T>.Value | 获取或设置配置值 | |
Config.Save() | 手动保存配置 | |
| 日志系统 | Logger.LogInfo() | 输出信息日志 |
Logger.LogError() | 输出错误日志 | |
ManualLogSource | 创建自定义日志源 | |
| Harmony | Harmony.PatchAll() | 应用所有补丁 |
[HarmonyPatch] | 标记补丁类 | |
Prefix/Postfix | 补丁方法前缀/后缀 | |
| 工具类 | Paths | 提供常用路径(插件目录、配置目录等) |
UnityEngine.Object | Unity对象操作 | |
Time | 时间相关功能 |
如果觉得本教程对你有帮助,请点赞、收藏并关注作者,获取更多游戏开发教程!下期预告:《BepInEx高级技巧:IL2CPP游戏插件开发实战》
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



