本篇教程的视频

本篇教程的源代码

GitHub地址:TutorialMod-Mixin-1.21

介绍

本篇教程就是简单讲讲Mixin的常用注解和方法,以及Mixin的基本使用方法

在后续的教程中,我们会更加深入地讲解Mixin的使用方法,并举一些例子来说明

Mixin的官方Wiki:Mixin Wiki

Fabric Wiki上的Mixin教程:Fabric Wiki - Mixin

Mixin的相关内容可以参考这两个链接,其官方Wiki是对Mixin原理的解释,并不是教程

现在,我们来聊聊这个Mixin。首先看这个词,Mixin,翻译过来就是混入,其实质就是将一部分代码混入到源代码之中,这是Mixin最基本的功能

那我们何时采用Mixin呢?

假设说,你在开发过程中,哎!觉得源代码里面有个方法写的挺好(也可能是你必须得用这个方法)

但是嘞,它那个方法层层叠叠,又调了本类或者其他类的方法,或者说它本类里面的私有字段……反正就是一堆,并不像我们注册物品和方块那样简单,直接搬源代码里的方法就行

要想把那个方法完整写好,就得搬其他的杂七杂八的方法,就变成了“为了一碗醋包了一盘饺子”,而且大多数情况下是不好搬的。它会拖泥带水,带一堆杂七杂八的方法或者字段

这个时候,你可能就需要考虑Mixin了。

另外的情况就是你想修改游戏的一些机制,比如粒子上限;或者说拦截一些事件,或者增加某些功能,这个时候也可以考虑Mixin

注意事项

Mixin的使用需要一定的Java基础,这是肯定的。同时也要求你对Minecraft的一些机制有一定的了解,源代码是如何实现这些功能的,这样你才能更好地使用Mixin

所以我的建议还是先研究源代码,能够看懂源代码在干什么,然后再来尝试使用Mixin

除此之外,Mixin也最好不要大量使用,除了有可能破坏游戏的平衡性,你也不能保证不和其他模组产生冲突(除非就装这么一个模组)。一些光影和优化的模组,它们是会和具有特殊渲染的模组发生冲突的

Such as OptiFine,随着高版本的侵入性越来越强,和其他模组的冲突也越来越多,一些光影显示也会出错。所以现在我玩Forge也不再使用OptiFine了

所以兼容性也是一个值得考虑的问题,能够避免冲突是最好的

Mixin也不要写得乱七八糟,很容易让游戏崩掉,这就不是模组兼容性的问题了

常用注解

这些内容Fabric Wiki上有详细的解释,如果你想了解更多,可以查看Fabric Wiki

这里建议安装一个插件(IDEA插件)Minecraft Development,它可以帮助你修正Mixin的一些参数,自动将相关类加到<modid>.mixin.json文件中

这个json文件在你的模组的resources文件夹下,所有Mixin相关的类在这个文件中都要声明

模板文件中已经有了一个Mixin类,并且也在这个json文件中声明了

1
2
3
4
5
6
7
8
9
10
11
{
"required": true,
"package": "com.besson.tutorialmod.mixin",
"compatibilityLevel": "JAVA_21",
"mixins": [
"ExampleMixin"
],
"injectors": {
"defaultRequire": 1
}
}

@Mixin

@Mixin注解是Mixin的核心注解,用于指定Mixin的目标类,也就是后面括号中的类

1
2
3
4
5
6
7
8
@Mixin(MinecraftServer.class)
public class ExampleMixin {
@Inject(at = @At("HEAD"), method = "loadWorld")
private void init(CallbackInfo info) {
// This code is injected into the start of MinecraftServer.loadWorld()V
System.out.println("Hello Minecraft!");
}
}

我们直接来看当时模板文件自带的那个Mixin类,它的目标类是MinecraftServer.class,这个类是Minecraft服务端的主类,它的loadWorld方法是在服务端启动时加载世界的方法

在编写这个类的时候,其实我们也可以把目标类继承的父类和实现的接口写上,这样我们在编写相关方法的时候也可以采用其父类或接口的东西

所以这个ExampleMixin类完整来写应该是这样(不过大多数情况下不需要这样)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@Mixin(MinecraftServer.class)
public class ExampleMixin extends ReentrantThreadExecutor<ServerTask> implements QueryableServer, ChunkErrorHandler, CommandOutput, AutoCloseable{
public ExampleMixin(String string) {
super(string);
}

@Inject(at = @At("HEAD"), method = "loadWorld")
private void init(CallbackInfo info) {
// This code is injected into the start of MinecraftServer.loadWorld()V
System.out.println("Hello Minecraft!");
}

@Override
public String getServerMotd() {
return "";
}

@Override
public String getVersion() {
return "";
}

@Override
public int getCurrentPlayerCount() {
return 0;
}

@Override
public int getMaxPlayerCount() {
return 0;
}

@Override
public void sendMessage(Text message) {

}

@Override
public boolean shouldReceiveFeedback() {
return false;
}

@Override
public boolean shouldTrackOutput() {
return false;
}

@Override
public boolean shouldBroadcastConsoleToOps() {
return false;
}

@Override
public void onChunkLoadFailure(Throwable exception, StorageKey key, ChunkPos chunkPos) {

}

@Override
public void onChunkSaveFailure(Throwable exception, StorageKey key, ChunkPos chunkPos) {

}

@Override
protected ServerTask createTask(Runnable runnable) {
return null;
}

@Override
protected boolean canExecute(ServerTask task) {
return false;
}

@Override
protected Thread getThread() {
return null;
}
}

然后我们的类也是可以声明为abstractinterface等等,根据具体情况来决定

@Inject

@Inject注解是注入,用于指定注入的位置和方法。这个也是用的比较多的一个注解

1
2
3
4
5
6
7
8
@Mixin(MinecraftServer.class)
public class ExampleMixin {
@Inject(at = @At("HEAD"), method = "loadWorld")
private void init(CallbackInfo info) {
// This code is injected into the start of MinecraftServer.loadWorld()V
System.out.println("Hello Minecraft!");
}
}

我们还是拿模板中的例子来说

@Inject注解的at参数是指定注入的位置

@At注解的参数有很多,比如说这里的HEAD后面我们还将接触到TAILRETURNINVOKE等等,具体的可以查看Fabric Wiki

method参数是指定注入的方法,这个方法是目标类的方法,我们可以在这个方法的前面或者后面插入我们的代码(具体位置是配合这前面的at来的)

当然,后续我们也会接触到一长串的method语句,因为如果这个方法中的参数很多,它就可能需要引用过来(接触到了再讲吧)

方法及其参数

我们接下来先看看@Inject注解的方法及其参数

1
2
3
4
5
6
7
8
@Mixin(MinecraftServer.class)
public class ExampleMixin {
@Inject(at = @At("HEAD"), method = "loadWorld")
private void init(CallbackInfo info) {
// This code is injected into the start of MinecraftServer.loadWorld()V
System.out.println("Hello Minecraft!");
}
}

这个方法的参数是CallbackInfo info,这个参数是Mixin的一个回调信息,它可以通过上面所说的插件修正补全,我们可以通过这个参数来获取一些信息

方法名的话并不是固定的,你可以随便写,但是最好还是按照规范来写,这样别人看起来也方便

另外在这个方法中,这里打印了一句话,目标类的这个方法是在服务端加载世界的时候调用的,所以这句话会在我们加载世界的时候打印出来(你可以启动游戏看一下)

@Accessor

@Accessor注解是用于访问私有字段的,这个注解也是用的比较多的一个注解

1
2
3
4
5
6
7
8
9
10
11
12
13
@Mixin(GrassColors.class)
public interface GrassColorsMixin {
@Accessor("colorMap")
static int[] getColorMap() {
throw new AssertionError();
}

@Accessor("colorMap")
static void setColorMap(int[] colorMap) {
throw new AssertionError();
}

}

这个例子是一个接口,它的目标类是GrassColors.class,这个类是用于获取草方块颜色的类

1
private static int[] colorMap = new int[65536];

在目标类中,有一个私有字段colorMap,假设你要访问它(虽然好像访问它也没什么用),我们就可以使用@Accessor注解

注解中的参数是这个字段的名字

这个注解有两个方法,一个是获取get这个字段的值,一个是设置set这个字段的值

获取这个字段的值,我们的方法名必须以get开头,不然会报错;设置这个字段的值,我们的方法名必须以set开头,不然也会报错

这个方法的返回值和参数类型必须和这个字段的类型一致,不然也会报错

使用

如果你要获取这个字段的值,你可以到我们主类的初始化方法中调用这个方法,然后获取这个字段的值

1
2
int[] colorMap = GrassColorsMixin.getColorMap();
LOGGER.info("Grass color map length: {}", colorMap.length);

我们这里通过日志输出这个字段的长度,这个字段的长度是65536,这个字段是一个int数组,长度是65536,这个字段是用于存储草方块的颜色的

启动游戏我们可以在日志中看到这个长度

如果你要设置这个字段的值,你可以到我们主类的初始化方法中调用这个方法,然后设置这个字段的值

1
2
3
int[] newColorMap = new int[128];
GrassColorsMixin.setColorMap(newColorMap);
LOGGER.info("Grass color map length: {}", GrassColorsMixin.getColorMap().length);

这里我们设置了一个长度为128的新的colorMap,然后我们获取这个字段的长度,这个长度是128,这个字段的长度就变成了128

启动游戏我们也可以在日志中看到这个长度

不过它并不会改变游戏中的草方块的颜色,因为其他的方法还会重新设置这个字段的值(等于说绕了一圈这个数组又被重新设置为65536的长度了)

当然,这也是在开发过程中需要注意的。有些时候你改一个地方看起来起作用了,实际上还有别的方法会影响,导致你的修改就没有效果。

所以在真正的开发过程中,你需要更加深入地了解这个方法,这个字段是如何被使用的,这样你才能更好地使用Mixin,考虑得周全一点

@Invoker

@Invoker注解是用于调用私有方法的,这个注解也是用的比较多的一个注解

1
2
3
4
5
@Mixin(EndermanEntity.class)
public interface EndermanEntityMixin {
@Invoker("teleportTo")
boolean invokeTeleportTo(double x, double y, double z);
}

这个例子是Fabric Wiki上的一个例子,这个类是用于调用末影人传送的方法

这个类中有一个私有方法teleportTo

1
2
3
private boolean teleportTo(double x, double y, double z) {
...
}

这个方法是用于末影人传送的,但Fabric Wiki上并没有举出具体的使用方法,这里的话也暂时搁置一下(嗯,这是一个不太好的例子,不过我也想不出来了qwq)

总结

这篇教程比较草率,毕竟Mixin的东西很多,也较为复杂

我自己的模组开发过程中也没怎么用过Mixin,所以也不太熟悉,只是简单地了解了一下

那么在后续的教程中,我们会更加深入地讲解Mixin的使用方法