从0开始教你使用BepInEx为unity游戏制作插件Mod

作者: 小莫
更新时间: 2021-08-18 18:20:17

转载时,请在显著的位置标明作者小莫以及本贴地址!

更直观的查看可以到小莫的wiki库:https://wiki.aoe.top/BepInEx/

目录:

[TOC]

BepInEx的说明与介绍

> - 所有内容已经在我的博客更新完毕:https://www.aoe.top/mod/434

  • 如果你对Mod的制作感兴趣,可以考虑申请加入我们的Mod组:https://bbs.3dmgame.com/thread-5838764-1-1.html
  • 如果我有任何写错或者遗漏的地方,欢迎大家随时指出;
  • 如果你有任何不懂的地方,欢迎留言,我有空会进行回复;

相关链接

BepInEx:https://github.com/BepInEx/BepInEx 官网:https://bepinex.github.io/bepinex_docs/master/index.html 官方文档:https://bepinex.github.io/bepinex_docs/master/articles/index.html

介绍:

什么是BepInEx?BepInEx是一个注入unity引擎游戏,为Mod开发者提供开发入口的一个插件; BepInEx提供了诸多功能,包括:

与 Unity Mod Manager的区别

> 摘要自宵夜97:https://www.bilibili.com/read/cv8997376 两者的相同点: UMM和BepInEx都使用Harmony库作为核心来对游戏进行功能修改。两者的不允许游戏路径出现中文或其他特殊符号,所以请保证游戏安装在纯英文路径。

> 两者的不同点: UMM使用一个单独的exe程序来安装Mod环境,此程序基于.Net,需要玩家电脑安装.Net4.5之后的版本才能启动。Mod环境的安装有两种方式,一种是直接修改程序集来加入环境,一种是通过UnityDoorStop在游戏启动时加载Mod环境,第一种方式因为破坏了游戏原有的程序,所以每次游戏更新的时候,都会导致Mod环境丢失需要重新安装,第二种方式不会破坏游戏的原有程序集,所以不会因为游戏更新而丢失环境。在Mod安装方面,UMM也有两种方式,一种是直观的,将Mod压缩包拖动到安装器内来安装,另一种是手动解压Mod包,放到正确的路径。UMM还内置了一个Mod管理界面,在游戏中按Ctrl+F10可以呼出管理界面,修改Mod配置、开启关闭Mod等。

> BepInEx没有单独的exe程序,环境的安装只是需要将BepInEx解压到游戏根目录即可。插件的加载方式上,等于UMM的第二种,也是通过UnityDoorStop来运行时加载。插件的安装方式上只有一种,将插件解压到指定的目录下(BepInEx/plugins)即可,没有UMM要求目录那么严格。BepInEx本身并不含有可视化的插件管理等功能。

BepInEx的下载与安装

下载BepInEx

从GitHub中下载最新版本:https://github.com/BepInEx/BepInEx/releases 从Mod站下载国内分流版本:https://mod.3dmgame.com/mod/172563

安装BepInEx

BepInEx支持:

  • Windows7、8、9 32和64位系统,
  • 带有GCC 10 或更高版本的GNU/Linux系统
  • macOS 10.13或更高版本

请根据自己的系统版本,进行下载和安装。

下载之后打开压缩包,将“BepInEx(文件夹)”、“winhttp.dll”、“doorstop_config.ini”放入游戏根目录; 首次运行游戏后,将自动在“BepInEx”目录下生成其他相关文件。 至此,基础的BepInEx就已经安装完成。

只需将Mod放入“BepInEx/plugins”目录中,即可使其生效。

开发环境的搭建与调试以及有用的工具们

必备工具

在正式开始编写Mod插件之前,我们需要准备一些工具:

开启BepInEx Debug模式

用VSC打开“BepInEx\config\BepInEx.cfg”, 修改

[Logging.Console]
Enabled = false

[Logging.Console]
Enabled = true

提取需要的库

我们在项目文件夹中新建一个文件夹,取名为\"libs\",然后从游戏目录中复制一些我们需要用到的dll文件库出来: “XXXX_Data\Managed”中的

"Assembly-CSharp.dll","UnityEngine.dll"
// 可选复制文件
"UnityEngine.CoreModule.dll","UnityEngine.UI.dll"

“\BepInEx\core”中的

"0Harmony.dll","BepInEx.dll","BepInEx.Harmony.dll"

到此,开发环境搭建好了,下一章我们开始创建项目

可选安装

说明:

  • BepInEx.ConfigurationManager: BepInEx.ConfigurationManager是BepInEx的一个内置管理UI,解压放到“BepInEx/plugins”, 进游戏按F1即可查看当前所有在运作中的插件;

  • BepInEx.Debug: BepInEx.Debug是BepInEx的一个调试工具库,里面也包含了一些其他的工具:

  • ScriptEngine:允许直接重新加载插件而无需重新运行游戏,将你的插件放到“BepInEx/scripts”文件夹中,并在游戏中按F6即可。你可以通过创建“OnDestroy()”方法来处理脚本重载时的需要卸载的资源;

  • Startup profiler:记录每个脚本的加载所需时间;

  • Mono Profiler:unity游戏的监听器,用来记录所有被调用的方法,调用次数和调用时间;

  • Demystify Exceptions:更加人性化的处理报错格式,并正确解析IEnumerables,lambdas 和 async 的状态

创建插件,实现“Hello, world”的输出

创建插件

通过前面几章的熟悉,现在可以开始创建我们的BepInEx插件了。

新建项目 打开Visual Studio,新建项目,选择“C# NET Framework 类库”

接着继续创建项目,.NET框架暂时没有什么特别的要求,别太低就行了

引入游戏类库 将我们之前拿出来的几个dll库引用进来

接下来,将刚刚引用进来的库全选,然后将“复制到本地”改为“false”

顺便将默认的“Class1.cs”改名成我们自己的名称。

定义插件

我们需要从 BaseUnityPlugin 继承类,并将BepInPlugin添加到该类

using System;
using BepInEx;
using UnityEngine;

namespace MyFirstBepInExMod
{
    [BepInPlugin("aoe.top.plugins.MyFirstBepInExMod", "这是我的第一个BepIn插件", "1.0.0.0")]
    public class MyFirstBepInExMod : BaseUnityPlugin
    {

    }
}

至此,我们就已经完成一个插件的基本结构了,现在来解释一下

public BepInPlugin(string GUID, string Name, string Version);

> string GUID:插件的唯一ID标识符,绝对且是唯一的,不能存在重复,大家在命名的时候尽量避免重复的现象,请用自己专属的唯一标识符来命名; > string Name:插件名称,可以使用中文, > string Version:插件版本,必须使用系统可解析的格式,如“1.0.0”、“0.2.1”、“2.6.0”之类的

启动函数

BepInEx中有4个函数比较常用的函数,大家可以简单记一下:

> Awake():在插件启动时会直接调用Awake()方法; > Start():在所有插件全部执行完成后会调用Start()方法,执行顺序在Awake()后面; > Update():插件启动后会一直循环执行Update()方法,可用于监听事件或判断键盘按键,执行顺序在Start()后面; > OnDestroy():在插件关闭时会调用OnDestroy()方法,可处理前面提到的“ScriptEngine”插件重启时需要做的操作。

测试和编译插件

了解了上面这些后,现在,我们开始写代码

// 在插件启动时会直接调用Awake()方法
void Awake()
{
    // 使用Debug.Log()方法来将文本输出到控制台
    Debug.Log("Hello, world!");

}

// 在所有插件全部启动完成后会调用Start()方法,执行顺序在Awake()后面;
void Start()
{
    Debug.Log("这里是Start()方法中的内容!");
}

// 插件启动后会一直循环执行Update()方法,可用于监听事件或判断键盘按键,执行顺序在Start()后面
void Update()
{
    var key = new BepInEx.Configuration.KeyboardShortcut(KeyCode.F9);

    if (key.IsDown())
    {
        Debug.Log("这里是Updatet()方法中的内容,你看到这条消息是因为你按下了F9");
    }
}
// 在插件关闭时会调用OnDestroy()方法
void OnDestroy()
{
    Debug.Log("当你看到这条消息时,就表示我已经被关闭一次了!");
}

确认没有报错后,在VS中按F6,编译我们的插件,然后打开“MyFirstBepInExMod\bin\Debug”就可以看到我们刚刚编译好的插件了, 将插件复制到“\BepInEx\scripts”中,运行游戏,

注意:我这里是默认你安装了“ScriptEngine”,如果没有,则是放入“BepInEx\plugins”文件夹,如需安装,可以参考上一章的“开发环境的搭建与调试以及有用的工具们”

在游戏中按F6,载入“\BepInEx\scripts”文件夹中的脚本,就可以看到我们的内容了

完整代码

using System;
using BepInEx;
using UnityEngine;

namespace MyFirstBepInExMod
{
    [BepInPlugin("aoe.top.plugins.MyFirstBepInExMod", "这是我的第一个BepIn插件", "1.0.0.0")]
    public class MyFirstBepInExMod : BaseUnityPlugin
    {
        // 在插件启动时会直接调用Awake()方法
        void Awake()
        {
            // 使用Debug.Log()方法来将文本输出到控制台
            Debug.Log("Hello, world!");

        }

        // 在所有插件全部启动完成后会调用Start()方法,执行顺序在Awake()后面;
        void Start()
        {
            Debug.Log("这里是Start()方法中的内容!");
        }

        // 插件启动后会一直循环执行Update()方法,可用于监听事件或判断键盘按键,执行顺序在Start()后面
        void Update()
        {
            var key = new BepInEx.Configuration.KeyboardShortcut(KeyCode.F9);

            if (key.IsDown())
            {
                Debug.Log("这里是Updatet()方法中的内容,你看到这条消息是因为你按下了F9");
            }
        }
        // 在插件关闭时会调用OnDestroy()方法
        void OnDestroy()
        {
            Debug.Log("当你看到这条消息时,就表示我已经被关闭一次了!");
        }
    }
}

拦截/注入 游戏函数实现高级操作

> 本章中的部分内容参考了: > - [MOD制作教程 : 如何从零开始制作一个Unity Mod Manager MOD - By:JKstring](https://bbs.3dmgame.com/thread-5870433-1-1.html "MOD制作教程 : 如何从零开始制作一个Unity Mod Manager MOD - By:JKstring") > - [Unity游戏Mod/插件制作教程07 - Harmony补丁基础 -By:宵夜97](https://www.bilibili.com/read/cv9019946 "Unity游戏Mod/插件制作教程07 - Harmony补丁基础 -By:宵夜97")

通过前面的学习,想必大家已经对BepInEx的运行方式有个基本的认识了,现在,来讲解一下如何对游戏中的函数进行拦截和注入;

HarmonyPrefix

HarmonyPrefix是Harmony为我们提供的一个接口,它将在我们指定的函数前进行执行,并且我们可以返回一个bool值来控制是否继续继续执行游戏原函数; 需要配合HarmonyPatch一起使用

// 19 个重载
public HarmonyPatch();
public HarmonyPatch(Type declaringType);
public HarmonyPatch(MethodType methodType);
public HarmonyPatch(string methodName);
public HarmonyPatch(Type[] argumentTypes);
public HarmonyPatch(MethodType methodType, params Type[] argumentTypes);
public HarmonyPatch(string methodName, MethodType methodType);
public HarmonyPatch(string methodName, params Type[] argumentTypes);
public HarmonyPatch(Type[] argumentTypes, ArgumentType[] argumentVariations);
public HarmonyPatch(Type declaringType, MethodType methodType);
public HarmonyPatch(Type declaringType, string methodName);
public HarmonyPatch(Type declaringType, Type[] argumentTypes);
public HarmonyPatch(Type declaringType, string methodName, MethodType methodType);
public HarmonyPatch(string methodName, Type[] argumentTypes, ArgumentType[] argumentVariations);
public HarmonyPatch(Type declaringType, string methodName, params Type[] argumentTypes);
public HarmonyPatch(MethodType methodType, Type[] argumentTypes, ArgumentType[] argumentVariations);
public HarmonyPatch(Type declaringType, MethodType methodType, params Type[] argumentTypes);
public HarmonyPatch(Type declaringType, string methodName, Type[] argumentTypes, ArgumentType[] argumentVariations);
public HarmonyPatch(Type declaringType, MethodType methodType, Type[] argumentTypes, ArgumentType[] argumentVariations);
public HarmonyPatch(string assemblyQualifiedDeclaringType, string methodName, MethodType? methodType = null, Type[] argumentTypes = null, ArgumentType[] argumentVariations = null);

如: 我们想要对Mecha类下的SetForNewGame函数进行拦截,那么就是:

[HarmonyPrefix]
[HarmonyPatch(typeof(Mecha), "SetForNewGame")]
public static bool Mecha_SetForNewGame_Prefix()
{
    // 这里写入我们自己的内容            
    Debug.Log("这里的内容将会在游戏函数执行前进行执行");
    // 返回 true为继续执执行游戏原函数,返回 false为不执行游戏原函数,
    return true;
}

HarmonyPostfix

HarmonyPostfix一样也是Harmony为我们提供的一个接口,它将在我们指定的函数执行完毕后,再执行。 一样需要配合HarmonyPatch一起使用

如:

[HarmonyPostfix]
[HarmonyPatch(typeof(Mecha), "SetForNewGame")]
public static void Mecha_SetForNewGame_Postfix()
{
    // 这里写入我们自己的内容            
    Debug.Log("这里的内容需要等待游戏原函数执行完后才会执行");
}

> 注意: > 1.我们的函数需要使用static静态函数,不然会报错; > 2.函数名可以自定义,但尽量不要和游戏原有函数冲突; > 3.两种拦截方式大同小异,希望大家举一反三。

this

在游戏原函数中难免会出现this参数,万能的Harmony当然也考虑到了这一点,针对于this,我们可以向函数中传递一个__instance。

游戏原函数内容:

public void SetForNewGame()
{
    ModeConfig freeMode = Configs.freeMode;
    this.coreEnergyCap = freeMode.mechaCoreEnergyCap;
    this.coreEnergy = this.coreEnergyCap;
    this.corePowerGen = freeMode.mechaCorePowerGen;
    this.reactorPowerGen = freeMode.mechaReactorPowerGen;
    this.reactorEnergy = 0.0;
    this.reactorItemId = 0;
}

我们可以这样写:

[HarmonyPostfix]
[HarmonyPatch(typeof(Mecha), "SetForNewGame")]
public static void Mecha_SetForNewGame_Postfix(Mecha __instance)
{
    ModeConfig freeMode = Configs.freeMode;
    __instance.coreEnergyCap = freeMode.mechaCoreEnergyCap;
    __instance.coreEnergy = __instance.coreEnergyCap;
    __instance.corePowerGen = freeMode.mechaCorePowerGen;
    __instance.reactorPowerGen = freeMode.mechaReactorPowerGen;
    __instance.reactorEnergy = 0.0;
    __instance.reactorItemId = 0;
}

> 注释: > - Mecha__instance中,Mecha 为当前类的名称,__instance为Harmony的固有写法(有两个“_”); > - 这种方法只限于操作公共public变量和函数;

游戏私有变量

刚刚提到,“__instance”只能获取游戏的公共变量和方法,如果我们要获取游戏中私有的变量和方法的话,就需要用到Traverse工具; 我们可以通过Traverse工具,方便访问游戏里所有公有,私有,受保护的变量,方法,以及属性,

如我们想获取游戏中的变量,那么在我们的插件中就可以这么写:

[HarmonyPostfix]
[HarmonyPatch(typeof(Mecha), "SetForNewGame")]
public static void Mecha_SetForNewGame_Postfix(Mecha __instance)
{
    // 获取 private float _dronesSpeed; 的值
    var _droneCount= Traverse.Create(__instance).Field("_droneCount").GetValue();
}

拓展知识

> 来自 https://bbs.3dmgame.com/thread-5870433-1-1.html 关于Traverse的使用: Traverse是harmony类库下的一个工具类,也就是在一开始引用的using harmony;这条语句后,方便我们使用的一个类,首先我们要明白private和public还有protected三个关键词的区别,具体可以百度,我这里仅从结论讲明,除了public,其他的private和protected从外界是无法访问到的,但是用Traverse类不管它是public,private,protected,均可以强行访问,为什么不任何地方都使用Traverse去访问呢,因为性能问题,用Traverse要走映射,简单来说运行速度会有些许影响

> Traverse的具体使用方法简单的来说明一下,Traverse.create(类的实例),表名我要将一个类的实例转为Traverse对象,简单来说就是附加功能,比如我们以前都是自己买菜,后来有了XX外卖,我们不需要亲力亲为了,XX外卖就等于Traverse对象了(这里就是将映射功能简单化了,不需要自己打代码了),这样我们就有一个可以访问类实例的Traverse对象了,在上面法宝的例子中,我是直接写为了 var itemID = Traverse.Create(instance).Field(\"itemID\").GetValue(); 这是一种简化的写法的,下面我分开并且逐步注释一下 Traverse t = Traverse.Create(instance);//根据__instance (ToilRefining类的实例) 创建 Traverse对象,并且用t表示 Traverse f = t.Field(\"itemID\");//在ToilRefining实例里面有个itemID的字段,找到他并且创建一个Traverse对象,用f表示,这样可以强行访问 itemID,因为itemID是私有的没法直接访问 int itemID = f.GetValue();//将Traverse版本的itemID提取成可以直接访问的数值,因为Traverse并不知道原本itemID是什么类型的,所以我们要用标注这是个int类型了,从源代码中我们可以知道itemID的变量类型,对应修改即可 于是我们就访问到itemID了 既然有获取,自然就有修改,修改我们可以用f.SetValue(数值),这里就不需要指定了,因为你在输入数值的时候,他会自动把你输入的数据转成对应的类型

> 这里我说一下字段,属性,方法的意思,这是C#的基础,字段代表类变量,可以理解为类中的全局变量,可以再类中任意地方访问到

> 属性是字段的升级版,他在源代码中的样子是这样的

> 他跟字段的定义差不多,但是后面会有括号,里面还有set和get,这种样子的就是属性,我们不能通过Traverse.Field(字段名字),而是通过Traverse.Property(属性名字)来访问,如果定义中只有get,表名这个东西只能获取,不能更改(就是游戏开发者也不能),get和set都在就是可以获取也可以更改

> 最后就是方法,在C#中称为方法,C语言中称为函数,比如游戏源码中,MakeFaBao就是制作法宝的方法,他定义时后面跟随的是()这种括号,我们想要访问游戏private的方法可以用Traverse.Method(方法名字).GetValue()来运行,注意后面要加上.GetValue(),因为仅仅Traverse.Method(方法名字)是获取的方法的Traverse对象,而没有运行他

> C#中有一个关键词是var,这个关键词是这个变量是智能根据你后面赋值来判断他的变量类型的 比如 var a = 6;//a是int类型 var b = \"我是文字\";//b就是string类型的 于是之前为了itemID那么多行的代码就可以省略为var itemID = Traverse.Create(instance).Field(\"itemID\").GetValue();一句话搞定 当然也可以var t = Traverse.Create(instance); var itemID = t.Field(\"itemID\").GetValue();var XXXX = t.Field(\"XXXX\").GetValue(); 来多次获取

使用BepInEx.ConfigurationManager来制作简易配置和UI

下载和安装BepInEx.ConfigurationManager:

GitHub下载:https://github.com/BepInEx/BepInEx.ConfigurationManager/releases 中文汉化版:https://mod.3dmgame.com/mod/172600

下载后将“ConfigurationManager.dll”和“ConfigurationManager.xml”放入“\BepInEx\plugins”目录; 进入游戏后按F1即可打开管理菜单.

为你的Mod添加配置UI

ConfigurationManager 会自动将插件的配置内容显示出来; ConfigurationManager 将使用所有元素(例如:说明,范围)向用户展示; 在大多数情况下,不必引用ConfigurationManager.dll或对其进行特殊的数据处理; 只需确保添加尽可能多的参数(这样可以帮助玩家更好的进行修改)。别忘了添加描述、键名、值范围。

添加滑块 创建时,指定 AcceptableValueRange 即可,如果范围是0f-1f,或0-100,则滑块将显示百分比%。 示例:

using BepInEx;
using BepInEx.Configuration;
using UnityEngine;

namespace SimpleUI
{
    [BepInPlugin("aoe.top.plugins.SimpleUI", "简易UI示例", "1.0.0.0")]
    public class SimpleUI : BaseUnityPlugin
    {
        // 默认值
        public static int Count = 10;
        public static ConfigEntry userCount;
        void Start()
        {
            userCount = Config.AddSetting("自定义参数", "值:", Count, new ConfigDescription("你可以根据自己的需求,自由的调整这个参数", new AcceptableValueRange(1, 100)));
        }

        void Update()
        {
            if (Count != userCount.Value)
            {
                Debug.Log("值发生了变化,新的值为:" + userCount.Value);
                Count = userCount.Value;
            }
        }
    }
}

运行结果:

添加下拉列表

1.使用AcceptableValueList来创建下拉列表

示例:

using System;
using BepInEx;
using BepInEx.Configuration;
using UnityEngine;

namespace SimpleUI
{
    [BepInPlugin("aoe.top.plugins.SimpleUI", "简易UI示例", "1.0.0.0")]
    public class SimpleUI : BaseUnityPlugin
    {
        // 默认值
        public static string[] strList = {
                "下拉列表1",
                "下拉列表2",
                "下拉列表3",
                "下拉列表4",
                "下拉列表5"
            };
        public static string str = strList[0];
        public static ConfigEntry ValueList;

        [Obsolete]
        void Start()
        {
            ValueList = Config.AddSetting("下拉列表", "值:", strList[0], new ConfigDescription("这是一个下拉列表", new AcceptableValueList(strList)));

        }

        void Update()
        {
            if (str != ValueList.Value)
            {
                Debug.Log("下拉列表发生了编号,新的值为:" + ValueList.Value);
                str = ValueList.Value;
            }
        }

    }
}

运行结果:

2.使用枚举来创建下拉列表

使用枚举后就无需设置AcceptableValueList了,可以通过System.ComponentModel.DescriptionAttribute来自定义显示文本,否则默认显示枚举名。 示例:

using System;
using System.ComponentModel;
using BepInEx;
using BepInEx.Configuration;
using UnityEngine;

namespace SimpleUI
{
    [BepInPlugin("aoe.top.plugins.SimpleUI", "简易UI示例", "1.0.0.0")]
    public class SimpleUI : BaseUnityPlugin
    {
        // 默认值
        public static MyEnum MyEnumVal = MyEnum.Entry1;
        public static ConfigEntry ValueList2;
        [Obsolete]
        void Start()
        {
            ValueList2 = Config.AddSetting("下拉列表2", "值:", MyEnumVal, new ConfigDescription("这是一个下拉列表", null, new MyEnum()));
        }

        void Update()
        {
            if (MyEnumVal != ValueList2.Value)
            {
                Debug.Log("下拉列表2发生了变化,新的值为:" + ValueList2.Value);
                MyEnumVal = ValueList2.Value;
            }
        }

        // 枚举可以根据需求转换为int,只需在前面加一个 (int) 即可
        // 如 int a = (int)MyEnum.Entry4;
        public enum MyEnum
        {
            // Entry1 将在下拉框中 显示 Entry1
            Entry1,
            [Description("自定义显示")]
            Entry2,
            Entry3 = 10,
            Entry4 = 36,
            [Description("自定义显示2")]
            Entry5 = 47,
            Entry6
        }
    }
}

运行结果:

添加键盘快捷键配置

只需使用KeyboardShortcut即可轻松的帮你快速配置快捷键。 示例:

using System;
using System.ComponentModel;
using BepInEx;
using BepInEx.Configuration;
using UnityEngine;

namespace SimpleUI
{
    [BepInPlugin("aoe.top.plugins.SimpleUI", "简易UI示例", "1.0.0.0")]
    public class SimpleUI : BaseUnityPlugin
    {
        private ConfigEntry ShowCounter { get; set; }

        [Obsolete]
        void Start()
        {
            // 配置默认快捷键为 左Ctrl + U
            ShowCounter = Config.AddSetting("配置快捷键", "快捷键", new BepInEx.Configuration.KeyboardShortcut(KeyCode.U, KeyCode.LeftControl));
        }

        void Update()
        {
            if (ShowCounter.Value.IsDown())
            {
                Debug.Log("按下了快捷键");
            }
        }
    }
}

运行结果:

使用GUILayout来定制化高级UI

介绍

GUI和GUILayout都是unity的UI布局类,且都寄托在UnityEngine.IMGUIModule中,所以,我们在使用时,需要从游戏目录中将 UnityEngine.IMGUIModule.dll 引用到我们的项目中来。 其中, GUI是手动布局,需要我们手动给定x,y,w,h的值; GUILayout是自动布局,我们只需设置每行排列数量即可; GUI的布局比较的繁琐,且非常不灵活,所以我比较推荐使用GUILayout来进行UI自动布局; 关于GUILayout的使用与介绍,Unity官网有更加详细的说明,我这边就不做复杂的讲解,大家可以另行见:https://docs.unity3d.com/ScriptReference/GUILayout.html

我这边只做基础的使用方法以及对一些常用的控件进行说明

制作一个窗口

上面说过GUILayout是寄托在UnityEngine.IMGUIModule中的类了,那么,请自行引用UnityEngine.IMGUIModule.dll(在游戏目录 XXX_Data\Managed中)文件到vs工程中。

现在来给大家介绍一个新函数—— OnGUI(); OnGUI()的作用和之前我们介绍的Update()是同类型的,都是在游戏运行后会一直循环执行函数中的内容,不过OnGUI()是unity专门为GUI设定的一个函数,虽然我们窗口写到OnGUI()和写到Update()都可以达到我们想要的效果,但是为了代码更加的规范,我建议大家将UI相关的代码写到OnGUI()中,监听插件相关的代码写到Update()中。

所以,若我们想实现按F9打开/关闭窗口的“开关”,我们可以这样写:

using System;
using BepInEx;
using BepInEx.Configuration;
using UnityEngine;

namespace AdvancedUI
{
    [BepInPlugin("aoe.top.plugins.AdvancedUI", "高级UI示例", "1.0.0.0")]
    public class AdvancedUI : BaseUnityPlugin
    {
        // 窗口开关
        public bool DisplayingWindow = false;
        // 启动按键
        private ConfigEntry ShowCounter { get; set; }

        [Obsolete]
        public void Start()
        {
            // 允许用户自定义启动快捷键
            ShowCounter = Config.AddSetting("打开窗口快捷键", "Key", new BepInEx.Configuration.KeyboardShortcut(KeyCode.F9));
        }
        public void Update()
        {
            // 监听脚本按键按下
            if (ShowCounter.Value.IsDown())
            {
                DisplayingWindow = !DisplayingWindow;
                if (DisplayingWindow)
                {
                    Debug.Log("打开窗口");
                }
                else
                {
                    Debug.Log("关闭窗口");
                }
            }
        }

        // GUI函数
        private void OnGUI()
        {
            if (this.DisplayingWindow)
            {
                // 定义窗口位置 x y 宽 高
                Rect windowRect = new Rect(500, 200, 500, 300);
                // 创建一个新窗口
                // 注意:第一个参数(20210218)为窗口ID,ID尽量设置的与众不同,若与其他Mod的窗口ID相同,将会导致窗口冲突
                windowRect = GUI.Window(20210218, windowRect, DoMyWindow, "我的一个窗口");

            }
        }

        public void DoMyWindow(int winId)
        {

        }
    }
}

以上代码即可简单实现按F9打开/关闭窗口:

自动UI布局

定义新的区域:GUILayout.BeginArea 默认,GUILayout都是从左上角0,0坐标开始进行自动布局的,有时候,我们希望它显示在窗口中的其他位置,我们则需要使用GUILayout.BeginArea来定义一个新的区域,然后再将我们的控件写进去。

注意:GUILayout.BeginArea需要搭配 GUILayout.EndArea一起使用,否则会报错。

示例:

public void DoMyWindow(int winId)
{
        GUILayout.BeginArea(new Rect(10, 20, 490, 250));
        // 这里的大括号是可选的,我个人为了代码的阅读性,习惯性的进行了添加
        // 建议大家也使用大括号这样包裹起来,让代码看起来不那么的乱
        {
                GUILayout.Label("这是一行普通的文本框");
                if (GUILayout.Button("按钮1"))
                {
                        Debug.Log("你点击了按钮1");
                }
                if (GUILayout.Button("按钮2"))
                {
                        Debug.Log("你点击了按钮2");
                }
        }
        GUILayout.EndArea();
}

运行结果:

横向自动布局:GUILayout.BeginHorizontal

默认,GUILayout是竖向自动排列的,但我们可以使用GUILayout.BeginHorizontal让我们的控件进行横向排列。

注意:GUILayout.BeginHorizontal需要与GUILayout.EndHorizontal配合使用,否则会报错。

示例:

public void DoMyWindow(int winId)
{
        GUILayout.BeginArea(new Rect(10, 20, 480, 250));
        {
                GUILayout.Label("这是一行普通的文本框");
                // 第一行
                GUILayout.BeginHorizontal();
                {
                        if (GUILayout.Button("按钮1.1"))
                        {
                                Debug.Log("你点击了按钮1.1");
                        }
                        if (GUILayout.Button("按钮1.2"))
                        {
                                Debug.Log("你点击了按钮1.2");
                        }
                        if (GUILayout.Button("按钮1.3"))
                        {
                                Debug.Log("你点击了按钮1.3");
                        }
                        if (GUILayout.Button("按钮1.4"))
                        {
                                Debug.Log("你点击了按钮1.4");
                        }
                }
                GUILayout.EndHorizontal();

                // 第二行
                GUILayout.BeginHorizontal();
                {
                        if (GUILayout.Button("按钮2.1"))
                        {
                                Debug.Log("你点击了按钮2.1");
                        }
                        if (GUILayout.Button("按钮2.2"))
                        {
                                Debug.Log("你点击了按钮2.2");
                        }
                        if (GUILayout.Button("按钮2.3"))
                        {
                                Debug.Log("你点击了按钮2.3");
                        }
                        if (GUILayout.Button("按钮2.4"))
                        {
                                Debug.Log("你点击了按钮2.4");
                        }
                }
                GUILayout.EndHorizontal();

        }
        GUILayout.EndArea();
}

运行结果:

窗口滚动条:GUILayout.BeginScrollView

若我们的内容太多,又不想制作分页,那么就可能需要用到滚动条了,滚动条需要定义一个全局 Vector2 变量来接收、传递、储存滚动条的x,y坐标位置;

注意:GUILayout.BeginScrollView需与GUILayout.EndHorizontal配合使用,否则会报错

示例:

private Vector2 scrollPosition;
public void DoMyWindow(int winId)
{
        GUILayout.BeginArea(new Rect(10, 20, 480, 250));
        {
                GUILayout.Label("这是一行普通的文本框");

                // 第一行
                GUILayout.BeginHorizontal();
                {
                        if (GUILayout.Button("按钮1.1"))
                        {
                                Debug.Log("你点击了按钮1.1");
                        }
                        if (GUILayout.Button("按钮1.2"))
                        {
                                Debug.Log("你点击了按钮1.2");
                        }
                        if (GUILayout.Button("按钮1.3"))
                        {
                                Debug.Log("你点击了按钮1.3");
                        }
                        if (GUILayout.Button("按钮1.4"))
                        {
                                Debug.Log("你点击了按钮1.4");
                        }
                }
                GUILayout.EndHorizontal();

                // 第二行
                GUILayout.BeginHorizontal();
                {
                        if (GUILayout.Button("按钮2.1"))
                        {
                                Debug.Log("你点击了按钮2.1");
                        }
                        if (GUILayout.Button("按钮2.2"))
                        {
                                Debug.Log("你点击了按钮2.2");
                        }
                        if (GUILayout.Button("按钮2.3"))
                        {
                                Debug.Log("你点击了按钮2.3");
                        }
                        if (GUILayout.Button("按钮2.4"))
                        {
                                Debug.Log("你点击了按钮2.4");
                        }
                }
                GUILayout.EndHorizontal();

                // 滚动条区域
                scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, false, GUILayout.Width(480), GUILayout.Height(150));
                {
                        if (GUILayout.Button("按钮3"))
                        {
                                Debug.Log("你点击了按钮3");
                        }
                        if (GUILayout.Button("按钮4"))
                        {
                                Debug.Log("你点击了按钮4");
                        }
                        if (GUILayout.Button("按钮5"))
                        {
                                Debug.Log("你点击了按钮5");
                        }
                        if (GUILayout.Button("按钮6"))
                        {
                                Debug.Log("你点击了按钮6");
                        }
                        if (GUILayout.Button("按钮7"))
                        {
                                Debug.Log("你点击了按钮7");
                        }
                        if (GUILayout.Button("按钮8"))
                        {
                                Debug.Log("你点击了按钮8");
                        }
                        if (GUILayout.Button("按钮9"))
                        {
                                Debug.Log("你点击了按钮9");
                        }
                        if (GUILayout.Button("按钮10"))
                        {
                                Debug.Log("你点击了按钮10");
                        }
                }
                GUILayout.EndScrollView();

        }
        GUILayout.EndArea();
}

运行结果:

结语

GUILayout还可通过GUIStyle来为控件制作样式,但这部分我还研究的不够升入,我只会制作一些简单的样式(比如说设置背景颜色,固定宽高等,这些可以在vs中直接看到相关参数),等我研究出更加深入的样式后,可能会更新续篇。 到现在为止,BepInEx的教程系列就算完结了,感谢大家的游览和阅读,希望你能成为Mod领域中的一座大山,造福更多的玩家。

标签