参考文献
前言
很多人在刚接触 Minecraft 模组开发时,第一次看到 Mixin 都会觉得它像“黑魔法”
它确实和普通的继承、接口、事件监听不太一样,因为它做的事情是:在类加载时,直接把你的逻辑插进目标类的方法里
但换个角度看,它其实没有那么神秘 对初学者来说,Mixin 最重要的不是高级技巧,而是先搞清楚下面三件事:
- 你要改哪个类
- 你要改哪个方法
- 你要把代码插到哪里
只要这三件事清楚了,Mixin 的基础用法就已经掌握了一大半
快速理解 Mixin
可以先把 Mixin 理解成一句话:
找到目标类,找到目标方法,选择注入点,把你的代码放进去
它最大的价值是:
当你没有办法优雅地继承或覆盖 Minecraft 原版代码时,你仍然可以在指定位置插入自己的逻辑
这也是为什么它在 Fabric、Sponge 以及很多 Minecraft 模组开发场景里都非常常见
Mixin 能做什么
Mixin 常见用途包括:
- 在方法开头插入逻辑
- 在方法结束后追加逻辑
- 在返回前修改返回值
- 读取或修改目标类里的字段
- 拦截某次特定的方法调用
- 在某些条件下取消原方法继续执行
如果你只是刚入门,其实先掌握前三种就已经够用了
(Mixin 同时也适用于其它 mod,比如 sodium extra )
先记住三个核心概念
1. 目标类
目标类就是你要修改的那个类 比如某个实体类、某个屏幕类、某个方块类,或者原版里的世界逻辑类
2. 目标方法
目标方法就是你真正想插逻辑的方法
比如 tick、render、interact、canUse 这类方法
3. 注入点
注入点决定你的代码在方法中的哪个位置执行 是方法刚开始执行时,还是执行结束后,还是在返回前,又或者是在某次具体调用附近
最常见的几个注解
| 注解 | 作用 | 可以怎么理解 |
|---|---|---|
@Mixin | 声明要修改哪个目标类 | 我要改谁 |
@Inject | 把方法注入到目标方法里 | 我要插代码 |
@At | 指定注入位置 | 插在哪里 |
@Shadow | 引用目标类已有成员 | 借用原版字段或方法 |
@Unique | 给 Mixin 添加自己的成员 | 这是我自己的变量 |
@Redirect | 重定向某个具体调用 | 把某一步换掉 |
@ModifyArg | 修改一次调用的参数 | 改传进去的值 |
@ModifyVariable | 修改局部变量 | 改中间过程的值 |
对新手来说,最先要会的是:
@Mixin@Inject@AtCallbackInfo
先把这几个吃透,比一开始就学高级写法更重要
最小可用示例
先看一个最基础、最典型的例子
下面这段代码表示:
给目标类的 tick 方法做一个头部注入,也就是目标方法一开始执行,你的逻辑就先运行
@Mixin(TargetClass.class)public class TargetClassMixin {
@Inject(method = "tick", at = @At("HEAD")) private void onTick(CallbackInfo ci) { // 这里写你的逻辑 }}这段代码可以拆成四部分来理解:
@Mixin(TargetClass.class):说明你想修改TargetClassmethod = "tick":说明你要改的方法是tick@At("HEAD"):说明你要插在方法开头CallbackInfo ci:这是当前注入过程的上下文对象
如果你第一次写 Mixin,建议先从这种最简单的 HEAD 注入开始
常见注入点
@At("HEAD")
表示在方法刚开始执行时注入
适合用来做:
- 日志输出
- 预处理
- 条件判断
- 提前取消原逻辑
@At("TAIL")
表示在方法主体执行完成后再注入
适合用来做:
- 收尾逻辑
- 追加效果
- 在原逻辑后再补充处理
@At("RETURN")
表示在方法即将返回之前注入
适合用来做:
- 观察返回值
- 修改返回值
@At(value = "INVOKE", target = "...")
表示注入到某次具体的方法调用附近
这类写法更精确,但也更脆弱 Minecraft 内部实现一改,注入点就可能失效
如果你是新手,优先学会
HEAD、TAIL、RETURN等你已经能稳定读懂目标方法的执行流程,再去碰INVOKE、FIELD、Redirect这类精细写法
如何取消原方法执行
如果你希望某个方法不要继续往下运行,就需要使用 cancellable = true
例如:
@Inject(method = "canUse", at = @At("HEAD"), cancellable = true)private void beforeCanUse(CallbackInfoReturnable<Boolean> cir) { boolean forceResult = true; if (forceResult) { cir.setReturnValue(true); }}这里要注意两点:
1. 有返回值的方法要用 CallbackInfoReturnable<T>
因为 canUse 这类方法会返回结果,所以这里不能再用普通的 CallbackInfo
2. setReturnValue 通常就足够了
调用 setReturnValue(true) 后,Mixin 通常会直接把返回值设好,并阻止后续原逻辑继续执行
如果目标方法没有返回值,就使用普通的 CallbackInfo,并在合适的时候调用 ci.cancel()
如何访问目标类里的字段和方法
很多时候你不只是想插一段代码,你还想读取目标类里面已有的字段,或者调用它内部的方法
这时最常见的工具就是 @Shadow
@Mixin(TargetClass.class)public abstract class TargetClassMixin {
@Shadow private int cooldown;
@Unique private boolean mymod$logged;}这里的意思是:
@Shadow private int cooldown;表示这个字段不是新建的,而是目标类本来就有@Unique private boolean mymod$logged;表示这是你这个 Mixin 自己新增的字段
实践里经常会给 @Unique 成员加上类似 mymod$ 的前缀,这样更不容易和目标类冲突
Mixin 配置文件长什么样
Mixin 不只有 Java 类,还需要一个配置文件来告诉框架要加载哪些 Mixin
最基础的配置通常像这样:
{ "required": true, "package": "com.example.mymod.mixin", "compatibilityLevel": "JAVA_17", "mixins": [ "TargetClassMixin" ], "client": [], "injectors": { "defaultRequire": 1 }}几个关键字段可以这样理解:
package:你的 Mixin 类所在包路径mixins:通用 Mixin 列表client:仅客户端使用的 Mixininjectors.defaultRequire:默认要求至少命中多少个注入点
至于这个文件怎么注册进工程,要看你用的加载器
以 Fabric 为例,通常会在 fabric.mod.json 里声明 Mixin 配置文件
新手最容易踩的坑
方法签名不匹配
有些方法存在重载
如果你只写方法名,Mixin 可能找不到你真正想注入的那个版本
注入方法参数写错
你写的注入方法参数必须和目标方法环境匹配,包括:
- 是否是实例方法或静态方法
- 原方法参数列表
CallbackInfo或CallbackInfoReturnable
注入点过于脆弱
INVOKE、FIELD 这种精确定位很强,但 Minecraft 版本一变,目标代码结构可能就不一样了
客户端和服务端类混用
客户端专用类不要随便写进通用 Mixin,否则很容易在服务端环境崩溃
映射变化
开发环境里的方法名通常比较可读,但升级版本时,映射和方法签名都可能变化
调试建议
Mixin 出问题时,不要一上来就怀疑所有东西
更有效的排查顺序是:
- 配置文件有没有被正确加载
- 目标类是不是你以为的那个类
- 方法名和描述符是否正确
- 注入点是否真的命中
- 注入进去的代码本身是否抛错
一个很实用的策略是:
先写一个最简单的
@At("HEAD")注入,只输出一条日志 确认这个最小版本生效以后,再逐步增加复杂度
这样调试会轻松很多
推荐学习顺序
如果你刚开始学,建议按这个顺序来:
@Mixin@Inject@At("HEAD")TAIL和RETURNCallbackInfoReturnable@Shadow@Unique@Redirect、@ModifyArg、@ModifyVariable
这个顺序的好处是:
你会先掌握最稳定、最常用、最容易成功的写法,而不是一开始就陷进复杂注入点里
一句话总结
Mixin 的基础并不在于“记住多少注解”,而在于你能不能稳定回答下面三个问题:
我要改谁?我要改哪个方法?我要把代码插在哪里?
把这三件事想明白,Minecraft 模组开发里绝大多数基础 Mixin 场景,你就已经能处理了
部分信息可能已经过时









