我是幽白银Undertale真好玩。是否已经有人注意到了——一些模组已经使用上了炫酷的动态Icon?没错,我说的就是——Candlight
(光烛)!
在本章教学里,你将能学习到如何使用On
与反射——实现动态的Icon!
一、准备
你需要准备许多图片——不是帧图,是一张一张的图片。当然你也可以使用帧图,但这已经不是本教程的内容了。如果你决定使用本教程的方法,请使用如截图所示多张贴图。如果你不想一张张手动填写Icon图片名的话,你得注意——图片的名字有格式。
二、了解模组选择页面的构成
(注:以下将UIElement类中的名为Elements的集合称为UI部件集)
我们可以看到,在Mod页面里的显示是这样的
而UI的构成,请看下图
画得不好,见谅。接下来就是愉
痛悦苦的查源码时间了。

通过这段源码,我们可以知道模组选择页面是被 SetState
进了 Main.MenuUI
。通过VS,我们可以得知, Main.MenuUI
是一个 UserInterface
类。于是我们移步来到 UserInterface
类的源码,查看 SetState
函数,观察模组选择界面被储存在了哪里。
这时我们可以注意到,只要我们输入的参数不是 null
, SetState
就会调用一个名为 AddToHistory
的函数。我们按下Ctrl键,单击 AddToHistory
函数 ,就能跳转到这个函数的实现部分。我们继续观察这个函数。
我们可以看到,我们输入的 UIState
参数最终是被添加进了一个名为 _history
的集合内。
然后我们移步来到 UIMods
类,观察它的修饰语。

可以看到,这个类的修饰语为 internal
,显然,我们无法获得这个类的类型,只能作为object处理。
(值得一提的是,所有被Tr的UI系统所承认的UI部件都必须继承于 UIElements
类,所以它们必定都含有UI部件集,也就是字段 Elements
。里面是所有被其包含的UI部件。)
观察其UI部件的注册方法
可以发现,有一个 UIList
部件被 Append
(包含)进了 UIPanel
部件里,那么这个 UIList
就是我们需要找的UI部件,里面储存着模组的阅览部件—— UIModItem
。通过查阅源码

我们可以得知,UIModItem
同样是 internal
的。里面包含了我们模组的Icon。

那么思路就很明确了。我们需要使用反射,一路获取到 UIModItem
包含的UI部件集,最后检测类型为 UIImage
,并且其包含的图片大小为80*80的UI部件,将图片修改。而如何判定那个 UIModItem
是我们mod的,则需要检测位于 UIModItem
的字段 _mod
。
三、开始代码的编写
你得在UI绘制之前将其修改——所以我们得使用On
,来在原版的DrawMenu
函数之前修改模组的Icon。
首先,我们需要两个 int
字段来完成动画——一个是计时器,一个是帧图记录器。
//time是计时器,iconFrame为当前帧数,由于贴图名字后面的数字从1开始,所以我设定初始值为1 private int time = 0, iconFrame = 1;
在 Mod
主类的 Load
函数里面加上这么一行—— On.Terraria.Main.DrawMenu += Main_DrawMenu;
,给位于 Main
类的 DrawMenu
函数挂上 On
。
public class FirstMod : Mod { public override void Load() { On.Terraria.Main.DrawMenu += Main_DrawMenu; } }
之后,就开始今天的重点了!
创建一个方法 Main_DrawMenu
private void Main_DrawMenu(On.Terraria.Main.orig_DrawMenu orig, Terraria.Main self, Microsoft.Xna.Framework.GameTime gameTime) { }
我们将在里面完成接下来的操作。
首先,我们要获取 UIMods
的实例。我们可以尝试这么操作:
//以下两行为获取Main.MenuUI的UIState集 FieldInfo uiStateField = Main.MenuUI.GetType().GetField("_history", BindingFlags.NonPublic | BindingFlags.Instance); List<UIState> _history = (List<UIState>)uiStateField.GetValue(Main.MenuUI); //使用for遍历UIState集,寻找UIMods类的实例 for (int x = 0; x < _history.Count; x++) { //检测当前UIState的类名全称是否是ModLoader的UIMods if (_history[x].GetType().FullName == "Terraria.ModLoader.UI.UIMods") { } }
在 if
内,我们首先需要获取 UIMods
的UI部件集。可以尝试这么写:
//以下两行为获取UIMods的UI部件集 FieldInfo elementsField = _history[x].GetType().GetField("Elements", BindingFlags.NonPublic | BindingFlags.Instance); List<UIElement> elements = (List<UIElement>)elementsField.GetValue(_history[x]);
然后获取 UIList
的实例。可以这么写:
//由之前 了解模组选择页面的构成 一节可知,包含了 包含UIList部件的UIPanel 的UIElement第一个被UIMods包含,故此UIElement位于UIMods的部件集的0号索引处 //以下两行用于获取UIElement的UI部件集 FieldInfo uiElementsField = elements[0].GetType().GetField("Elements", BindingFlags.NonPublic | BindingFlags.Instance); List<UIElement> uiElements = (List<UIElement>)uiElementsField.GetValue(elements[0]); //同理,由 了解模组选择页面的构成 一节可知,UIPanel第一个被UIElements包含,故UIPanel位于UIElement的UI部件集的0号索引处 //以下两行用于获取UIPanel的UI部件集 FieldInfo myModUIPanelField = uiElements[0].GetType().GetField("Elements", BindingFlags.NonPublic | BindingFlags.Instance); List<UIElement> myModUIPanel = myModUIPanelField.GetValue(uiElements[0]) as List<UIElement>; //同理,由 了解模组选择页面的构成 一节可知,UIList第一个被UIPanel包含,故UIList位于UIPanel的UI部件集的0号索引处 UIList uiList = (UIList)myModUIPanel[0];
接下来,我们需要使用 for
来查找我们mod的 UIModItem
部件。可以尝试这么写:
//遍历uiList包含的子部件,寻找我们mod的UIModItem部件 for (int i = 0; i < uiList._items.Count; i++) { //反射获取mod实例,检测其是否是我们的mod if (uiList._items[i].GetType().GetField("_mod", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(uiList._items[i]).ToString() == Name) { } }
在 if
内,我们需要获取当前 UIModItem
的UI部件集。可以试试这样:
//以下两行为获取我们mod的UIModItem的UI部件集 FieldInfo myUIModItemField = uiList._items[i].GetType().GetField("Elements", BindingFlags.NonPublic | BindingFlags.Instance); List<UIElement> myUIModItem = (List<UIElement>)myUIModItemField.GetValue(uiList._items[i]);
之后又是喜闻乐见(x)的 for
了:
//遍历UIModItem的UI部件集 for (int j = 0; j < myUIModItem.Count; j++) { }
在 for
内,我们需要判断哪个UI部件是我们需要找的。可以这么写:
//如果当前UI部件是UIImage,且其宽高均为80 if (myUIModItem[j] is UIImage && myUIModItem[j].Width.Pixels == 80 && myUIModItem[j].Height.Pixels == 80) { }
那么在 if
之内,就可以尝试修改此 UIImage
的贴图并退出循环了
//修改此UI部件的贴图 (myUIModItem[j] as UIImage).SetImage(GetTexture($"Icons/Icon_{iconFrame}")); //退出循环 break;
组合起来,就是
//遍历UIModItem的UI部件集 for (int j = 0; j < myUIModItem.Count; j++) { //如果当前UI部件是UIImage,且其宽高均为80 if (myUIModItem[j] is UIImage && myUIModItem[j].Width.Pixels == 80 && myUIModItem[j].Height.Pixels == 80) { //修改此UI部件的贴图 (myUIModItem[j] as UIImage).SetImage(icon[iconFrame]); //退出循环 break; } }
最后,通过反射逐一修改之前获取的值,并退出循环。
//最后按逆序逐一SetValue myUIModItemField.SetValue(uiList._items[i], myUIModItem); myModUIPanel[0] = uiList; myModUIPanelField.SetValue(uiElements[0], myModUIPanel); uiElementsField.SetValue(elements[0], uiElements); elementsField.SetValue(_history[x], elements); uiStateField.SetValue(Main.MenuUI, _history); //退出循环 break;
最后的最后,运行计时器,启动帧图记录器,并执行原方法
//计时器递增 time++; //如果计时器取余6为0(计时器的值能被6整除)(此处的6用于控制动画速度) if (time % 6 == 0) { //帧图记录器的值+1 iconFrame++; //重置计时器的值为0 time = 0; } //如果帧图记录器的值大于7(因为贴图后缀数字最大为7) if (iconFrame > 7) //重置帧图记录器的值为1 iconFrame = 1; //执行原方法 orig(self, gameTime);
大功告成!完整代码应该如下:
private void Main_DrawMenu(On.Terraria.Main.orig_DrawMenu orig, Main self, GameTime gameTime) { //以下两行为获取Main.MenuUI的UIState集 FieldInfo uiStateField = Main.MenuUI.GetType().GetField("_history", BindingFlags.NonPublic | BindingFlags.Instance); List<UIState> _history = (List<UIState>)uiStateField.GetValue(Main.MenuUI); //使用for遍历UIState集,寻找UIMods类的实例 for (int x = 0; x < _history.Count; x++) { //检测当前UIState的类名全称是否是ModLoader的UIMods if (_history[x].GetType().FullName == "Terraria.ModLoader.UI.UIMods") { //以下两行为获取UIMods的UI部件集 FieldInfo elementsField = _history[x].GetType().GetField("Elements", BindingFlags.NonPublic | BindingFlags.Instance); List<UIElement> elements = (List<UIElement>)elementsField.GetValue(_history[x]); //由之前 了解模组选择页面的构成 一节可知,包含了 包含UIList部件的UIPanel 的UIElement第一个被UIMods包含,故此UIElement位于UIMods的部件集的0号索引处 //以下两行用于获取UIElement的UI部件集 FieldInfo uiElementsField = elements[0].GetType().GetField("Elements", BindingFlags.NonPublic | BindingFlags.Instance); List<UIElement> uiElements = (List<UIElement>)uiElementsField.GetValue(elements[0]); //同理,由 了解模组选择页面的构成 一节可知,UIPanel第一个被UIElements包含,故UIPanel位于UIElement的UI部件集的0号索引处 //以下两行用于获取UIPanel的UI部件集 FieldInfo myModUIPanelField = uiElements[0].GetType().GetField("Elements", BindingFlags.NonPublic | BindingFlags.Instance); List<UIElement> myModUIPanel = myModUIPanelField.GetValue(uiElements[0]) as List<UIElement>; //同理,由 了解模组选择页面的构成 一节可知,UIList第一个被UIPanel包含,故UIList位于UIPanel的UI部件集的0号索引处 UIList uiList = (UIList)myModUIPanel[0]; //遍历uiList包含的子部件,寻找我们mod的UIModItem部件 for (int i = 0; i < uiList._items.Count; i++) { //反射获取mod实例,检测其是否是我们的mod if (uiList._items[i].GetType().GetField("_mod", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(uiList._items[i]).ToString() == Name) { //以下两行为获取我们mod的UIModItem的UI部件集 FieldInfo myUIModItemField = uiList._items[i].GetType().GetField("Elements", BindingFlags.NonPublic | BindingFlags.Instance); List<UIElement> myUIModItem = (List<UIElement>)myUIModItemField.GetValue(uiList._items[i]); //遍历UIModItem的UI部件集 for (int j = 0; j < myUIModItem.Count; j++) { //如果当前UI部件是UIImage,且其宽高均为80 if (myUIModItem[j] is UIImage && myUIModItem[j].Width.Pixels == 80 && myUIModItem[j].Height.Pixels == 80) { //修改此UI部件的贴图 (myUIModItem[j] as UIImage).SetImage(icon[iconFrame]); //退出循环 break; } } //最后按逆序逐一SetValue myUIModItemField.SetValue(uiList._items[i], myUIModItem); myModUIPanel[0] = uiList; myModUIPanelField.SetValue(uiElements[0], myModUIPanel); uiElementsField.SetValue(elements[0], uiElements); elementsField.SetValue(_history[x], elements); uiStateField.SetValue(Main.MenuUI, _history); //退出循环 break; } } //退出循环 break; } } //计时器递增 time++; //如果计时器取余6为0(计时器的值能被6整除)(此处的6用于控制动画速度) if (time % 6 == 0) { //帧图记录器的值+1 iconFrame++; //重置计时器的值为0 time = 0; } //如果帧图记录器的值大于7(因为贴图后缀数字最大为7) if (iconFrame > 7) //重置帧图记录器的值为1 iconFrame = 1; //执行原方法 orig(self, gameTime); }
作业
最后布置个小作业(?)
如何修改mod的名字为其他字体呢?
此教程及作业的源码在此处下载。
好耶,又更新了(我好像来晚了)
Pingback: 通过 HookEndpointManager 动态挂钩子 - 裙中世界