[授人以鱼不如授人以渔]MOD制作教程 : 如何从零开始制作一个Unity Mod Manager MOD
本帖最后由 JKstring 于 2019-4-12 19:41 编辑开场白:其实 UnityModManager是一个非常好用的Unity游戏MOD工具,不仅仅是修仙模拟器,任何Unity3D引擎制作的游戏几乎都支持,然而国内的教程很少(也可能是我找的不仔细),相关的资料很少,于是出一个UMM MOD的制作教程,让更多的人有能力做属于自己的MOD,以下的教程基于我新写的修仙模拟器炼宝管理MOD当示例,在结尾会放出源码(学会后其他游戏的MOD也是相同制作方法)
如果对源码不感兴趣,想要MOD的朋友,可以去 点此前往 这里下载
更多源码可以去点此前往这里下载,都是以相同的方式开发,但遇到与以下示例不同的状况,或者其他的处理的方式的写法,有兴趣的朋友可以去看看
使用到的工具:
Visual Studio 2017
1.新建一个项目
首先打开VisualStudio2017(以下简称VS),单击左上角新建,项目,C#,类库(.Net Framework)如上图,为你的MOD起一个名字吧,图例中使用了MakeFaBaoManager,把其改成自己MOD的名字即可(尽量开发过程中避免使用中文字符,命名最好也不要用中文,虽然C#支持中文编译,但是出了BUG找不到原因,最后发现是用了中文的感觉太痛苦了,第一次制作最好与例子中一模一样,第二次在考虑改名)
框架要改为3.5或者4.0,推荐3.5,虽然更新的框架会有更多的功能,但实际上并用不到,而高版本的框架对使用MOD人来讲是一个门槛,他也必须要有对应的框架版本才行,所以推荐最低的3.5
2.导入游戏所用的类库
右键单击"引用",选择添加引用,在浏览中寻找以上dll添加引用
其中0Harmony12和UnityModManager可以在你UnityModManager安装目录下找到
Assembly-CSharp
Assembly-CSharp-firstpass
UnityEngine
UnityEngine.CoreModule
UnityEngine.IMGUIModule
UnityEngine.TextRenderingModule
UnityEngine.UI
UnityEngine.UIModule
以上类库可以再游戏根目录下的Amazing Cultivation Simulator_Data/Managed 文件夹下找到,其他Unity3D游戏也是相同道理Managed文件夹下
添加引用完成后,选中所有引用,在下方会有一个复制到本地,更改值为False,不让他进行复制,然后右键解决方案下面,你为MOD起的名字的菜单,右键,选择添加,新建项,文本文件,名称改为info.json,这个文件将是你MOD的信息文件,UMM依靠这个文件显示你MOD的名字,版本号等
选中新创建的info.json,将他的复制到输出目录改为始终复制,方便发布打包
然后我们右键右边Class1,选择重命名,更改名称为MainMod,会弹出提示是否更新引用路径,我们选择是,这样就与上方截图中的文件名保持一致了
我们双击MainMod.cs文件,切换到代码窗口,将我们添加的引用文件类进行使用,如上图,如此我们的准备工作就结束了,可以具体的来写我们的MOD了!
2.处理使用MOD玩家的配置文件,让其可以保存配置
UnityModManager提供了一个非常简便的方法,用来保存每个使用MOD玩家的配置文件,要使用他我们首先要创建一个类,并起名为Settings(名称可以更改,但是对于初次接触的人来说,最好按照图中示例做一遍再尝试自己修改),他继承了UnityModManager.ModSettings,然后我们重写一下save方法(override),最后完成结果如同上图一样就完成了,我们现在可以方便的存储玩家配置了
接下来我们焦点回到public class MainMod 中,我们需要创建一个入口,来让UMM加载我们的MOD,并且我们还需要几个变量开关,来让MOD是运行还是停止
此处我们新添加了三个变量,enable来确定MOD是开启的还是关闭的,settings,玩家的设置文件,玩家调的MOD选项都保存在里面,logger,用来在游戏中输出信息的,我们还添加了一个Load方法,这里是UMM直接调用我们MOD的入口,当我们的MOD被UMM找到被加载时,就会执行这里,如果我们return true,意思就是告诉umm,这个MOD已经成功加载并且完成初始化了,可以正常使用了~(如果return false,表示我启动失败了,不要加载这个MOD了,UMM此时会把这个MOD的状态改为红色,适用于我们发现MOD已经不兼容这个游戏版本了,或者这个MOD必须要在某个MOD之后,但是我们发现没有加载那个MOD,就可以return false通知使用者)
3.MOD初始化
之前说了UMM会调用我们的Load来加载MOD,我们就可以在加载MOD的时候进行初始化,比如我们之前的玩家配置文件还没读取呢,于是我们就可以这么写
每一行代码的含义都已经写在图上了,但是那里有几行红色报错了,怎么回事呢,是因为我们还没有定义这几个方法,我们将鼠标放到红线上面,就会弹出提示
我们选择显示可能的修补程序,选择生成方法
我们依次对三个带有红色的地方进行相同操作,就能得到如下图的代码
这样我们就可以让用户在开关MOD,保存配置,打开配置面板的时候,执行我们对应的代码了!
只要改为如上的代码(绿色为注释,可以不用打),基本功能就都能看到了!注意OnToggle方法,他要返回一个值,return true;就是告诉UMM我切换状态成功了,如果return false,则表示我切换失败了,可能需要用户重启游戏才能让MOD生效,UMM在接收到return false 的时候,会将MOD的状态更改为红色(UMM红色状态表示需要重启游戏或者无法使用)
4.实际修改游戏内容
**** Hidden Message *****
结束语:UMM相比于官方提供的接口,给我们提供了更强大的功能,最高的权限,让我们能访问到游戏里几乎所有的数据,调用任何的方法,搭配VS的代码提示+自动完成,可以快速,简单的制作自己的MOD,并且只要学会,就可以不局限于一款游戏,为更多的游戏制作MOD,希望能以这一篇教程为引,带出更多的优质MOD!下面放出上面示例的源码,方便学习
======================================拓展知识==========================
关于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<int>();
这是一种简化的写法的,下面我分开并且逐步注释一下
Traverse t = Traverse.Create(__instance);//根据__instance (ToilRefining类的实例) 创建 Traverse对象,并且用t表示
Traverse f = t.Field("itemID");//在ToilRefining实例里面有个itemID的字段,找到他并且创建一个Traverse对象,用f表示,这样可以强行访问 itemID,因为itemID是私有的没法直接访问
int itemID = f.GetValue<int>();//将Traverse版本的itemID提取成可以直接访问的数值,因为Traverse并不知道原本itemID是什么类型的,所以我们要用<int>标注这是个int类型了,从源代码中我们可以知道itemID的变量类型,对应修改即可
于是我们就访问到itemID了
既然有获取,自然就有修改,修改我们可以用f.SetValue(数值),这里就不需要指定<int>了,因为你在输入数值的时候,他会自动把你输入的数据转成对应的类型
这里我说一下字段,属性,方法的意思,这是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<int>();一句话搞定
当然也可以var t = Traverse.Create(__instance);
var itemID = t.Field("itemID").GetValue<int>();var XXXX = t.Field("XXXX").GetValue<int>();
来多次获取
**** Hidden Message *****
谢谢分享:lol
{:3_116:}
前来学习
感谢大佬分享教程
谢谢!感谢您的分享 必须顶。。。。。。。。。。。。。。。。。 顶,66666666666666666 kkkkk看看k:)
3333333333333333333333333333333333333
show times
感谢分享 66666666666 感谢分享!
666666666666666
学习学习具体mod怎么写 膜拜大佬讲解
谢谢楼主分享
9999999999999999999999 6666666666666666666666
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using UnityModManagerNet;
using UnityEngine;
using UnityEngine.UI;
using Harmony12;
using XiaWorld;
using XiaWorld.UI.InGame;
using FairyGUI;
using System.Reflection.Emit;
namespace MakeFuManager
{
public class Settings : UnityModManager.ModSettings
{
public override void Save(UnityModManager.ModEntry modEntry)
{
Save(this, modEntry);
}
}
public class MainMod
{
public static bool enable;
public static Settings settings;
public static UnityModManager.ModEntry.ModLogger logger;
public static bool load(UnityModManager.ModEntry modEntry)
{
//读取用户配置文件
settings = Settings.Load<Settings>(modEntry);
//存储日志输出对象方便以后使用
logger = modEntry.Logger;
//当用户打开MOD配置时调用
modEntry.OnGUI = OnGUI;
//当用户保存MOD配置时调用
modEntry.OnSaveGUI = OnSaveGUI;
//当用户开关MOD配置时调用
modEntry.OnToggle = OnToggle;
//将更改应用到游戏上
var harmony = HarmonyInstance.Create(modEntry.Info.Id);
harmony.PatchAll(Assembly.GetExecutingAssembly());
return true;
}
/// <summary>
/// 当用户开关MOD的时候调用
/// </summary>
/// <param name="arg1">MOD配置对象</param>
/// <param name="arg2">用户是要打开是关闭MOD</param>
/// <returns>切换状态是否成功</returns>
private static bool OnToggle(UnityModManager.ModEntry arg1, bool arg2)
{
//当前开启状态变为用户想要的状态
enable = arg2;
//告诉UMM状态切换好了
return true;
}
private static void OnSaveGUI(UnityModManager.ModEntry obj)
{
settings.Save(obj);
}
private static void OnGUI(UnityModManager.ModEntry obj)
{
}
}
public class PracticeMgr_RandomSpellItem_Path
{
public static bool RandomSpellItem_Path(PracticeMgr __instance)
{
//如果MOD状态是关闭的话
if (!MainMod.enable)
{
//执行游戏原本的方法,不要对其进行修改
return true;
}
//不要再执行原本游戏方法了,本MOD接手了
return false;
}
public ItemThing RandomSpellItem(string otname, string name, float p, float fixp = -1f, int level = -1, bool notemp = false, int rate = 0)
{
var ItemSpellNames = Traverse.Create(__instance).Field("ItemSpellNames").GetValue<int>();
string itemname = string.Empty;
if (string.IsNullOrEmpty(otname))
{
itemname = __instance.ItemSpellNames;
}
else if (otname == "Item_SpellPaperLv2")
{
itemname = __instance.ItemSpellNames;
}
else if (otname == "Item_SpellPaperLv3")
{
itemname = __instance.ItemSpellNames;
}
else
{
itemname = __instance.ItemSpellNames;
}
if (level >= 0)
{
itemname = __instance.ItemSpellNames;
}
ItemThing itemThing = ItemRandomMachine.RandomItem(itemname, null, 0, 12, -1f, 1);
itemThing.Rate = ((rate <= 0) ? itemThing.Rate : rate);
if (string.IsNullOrEmpty(name))
{
itemThing.SetQuality((fixp <= 0f) ? World.RandomRange(0.1f, 0.7f) : fixp);
fixp = -1f;
}
else
{
itemThing.SetQuality(p);
}
if (fixp == -1f)
{
fixp = 0.5f + (float)((rate <= 0) ? itemThing.Rate : rate) * 0.25f;
}
if (!string.IsNullOrEmpty(name))
{
SpellDef spellDef = __instance.GetSpellDef(name);
if (notemp)
{
fixp *= 0.6f;
}
foreach (ModifierPropertyData modifierPropertyData in spellDef.Effects)
{
itemThing.AddEquiptData(modifierPropertyData.Name, modifierPropertyData.AddV * p * fixp, modifierPropertyData.AddP * p * fixp);
}
itemThing.SetName(((!notemp) ? string.Empty : global::TFMgr.Get("伪造的")) + spellDef.DisplayName);
itemThing.SetDesc(spellDef.Desc + ((!notemp) ? string.Empty : global::TFMgr.Get("\n但似乎是伪造的,效果大打折扣。")));
}
else
{
System.Random random = new System.Random((int)p);
int num = random.Next(1, 3);
for (int i = 0; i < num; i++)
{
string name2 = PropertyMgr.Instance.RandomProperty((int)p);
float quality = itemThing.GetQuality();
float num2 = 1f;
if (quality <= 0.5f)
{
num2 = Mathf.Lerp(0f, 0.2f, quality / 0.5f);
}
else if (quality <= 0.7f)
{
num2 = Mathf.Lerp(0.2f, 0.4f, (quality - 0.5f) / 0.2f);
}
else if (quality <= 1f)
{
num2 = Mathf.Lerp(0.4f, 1f, (quality - 0.7f) / 0.3f);
}
itemThing.AddEquiptData(name2, 0f, num2 * World.RandomRange(0.7f, 1.3f) * fixp);
}
itemThing.SetDesc(global::TFMgr.Get("一张随意为之但充满想象力的符咒,不知道会有什么神奇的效果。"));
}
return itemThing;
}
}
}
按照你的方法把 this.ItemSpellNames替换成 __instance.ItemSpellNames可__instance下面的红线始终存在红色波浪线。
ggwshx,如果您要查看本帖隐藏内容请回复
看来还是留给大佬吧,看不懂。 感谢分享!
6666666666666666666666
66666666666666
感谢分享,支持大佬 谢谢大大
感谢分享 学习了对新手帮助很大