跳至正文

使用反射实现动态Icon

我是幽白银Undertale真好玩。是否已经有人注意到了——一些模组已经使用上了炫酷的动态Icon?没错,我说的就是——Candlight(光烛)!

在本章教学里,你将能学习到如何使用On与反射——实现动态的Icon!

一、准备

你需要准备许多图片——不是帧图,是一张一张的图片。当然你也可以使用帧图,但这已经不是本教程的内容了。如果你决定使用本教程的方法,请使用如截图所示多张贴图。如果你不想一张张手动填写Icon图片名的话,你得注意——图片的名字有格式。

Icon们

二、了解模组选择页面的构成

(注:以下将UIElement类中的名为Elements的集合称为UI部件集)

我们可以看到,在Mod页面里的显示是这样的

而UI的构成,请看下图

画得不好,见谅。接下来就是愉的查源码时间了。

通过这段源码,我们可以知道模组选择页面是被 SetState 进了 Main.MenuUI 。通过VS,我们可以得知, Main.MenuUI 是一个 UserInterface 类。于是我们移步来到 UserInterface 类的源码,查看 SetState 函数,观察模组选择界面被储存在了哪里。

这时我们可以注意到,只要我们输入的参数不是 null SetState 就会调用一个名为 AddToHistory函数。我们按下Ctrl键,单击 AddToHistory 函数 ,就能跳转到这个函数实现部分。我们继续观察这个函数。

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的名字为其他字体呢?

此教程及作业的源码在此处下载。

《使用反射实现动态Icon》有2个想法

  1. Pingback: 通过 HookEndpointManager 动态挂钩子 - 裙中世界

发表回复