Fabric Java · Magic wand event

挥动代码,做一根会点火的魔法棒

基于真实的 maik-template-1.20 工程,从物品注册、左键事件、服务端判定一路讲到模型、中文名、32×32 像素贴图和合成配方。完成以后,玩家手持魔法棒左键方块,目标位置会燃起火焰并响起火焰弹的声音。

Minecraft 1.20 Fabric API 0.83.0 JDK 21 左键施法 约 90 分钟

Mission · 先看最终效果

这不是“贴一张魔法棒图片”,而是让一次玩家操作真正改变游戏世界。

想象一下:你走到草地上,手里拿着一根橙红色的小魔杖,瞄准脚边的方块轻点左键。代码先像门卫一样确认“拿的是不是魔法棒”,再检查玩家是否有修改这里的权限。只有全部通过,服务器才把目标方块替换成火焰,并让附近玩家听见火焰弹的声音。这个小效果同时串起了 Fabric 模组里最重要的四条线:注册表、玩家事件、客户端与服务器、资源文件。

01 · ITEM

认识一件物品

注册 maik:magic_wand,让游戏、命令、物品栏和资源系统使用同一个唯一身份。

02 · EVENT

听见玩家动作

使用 AttackBlockCallback 监听“攻击方块”,理解为什么本课是左键,而不是右键使用物品。

03 · SERVER

安全修改世界

先判断旁观模式和修改权限,再只在服务器端放置火焰、同步方块并广播声音。

04 · ASSETS

让它看起来像魔法棒

用翻译 JSON、手持模型 JSON 和透明 PNG,把一段注册代码变成玩家看得见、拿得起的道具。

本课完成标准:运行 ./gradlew.bat build 显示 BUILD SUCCESSFUL;开发客户端里能用 /give @s maik:magic_wand 获得“魔法棒”;手持它左键可修改的方块时,方块变成火焰并播放声音;普通物品仍保持原来的攻击或挖掘逻辑。

Version truth · 以模板为准

先读工程的版本声明,再决定装哪个 JDK;不要凭“1.20 应该是……”来猜。

本教程逐行核对了 C:\TeacherMa\Mod\maik-template-1.20。它的目标游戏是 Minecraft 1.20,使用 Yarn 1.20+build.1、Fabric Loader 0.18.4、Fabric API 0.83.0+1.20 和 Loom 1.14.10。更值得注意的是,模板在 build.gradle 中明确设置 release = 21,并在 fabric.mod.json 中要求 java >= 21,所以本课使用 JDK 21。

组件模板中的值在哪个文件确认作用
Minecraft1.20gradle.properties决定游戏类、资源格式和可用 API
Yarn mappings1.20+build.1gradle.properties给混淆后的 Minecraft 类和方法提供可读名称
Fabric Loader0.18.4gradle.properties发现并加载模组入口
Fabric API0.83.0+1.20gradle.properties提供物品组和玩家交互事件等常用 API
Fabric Loom1.14.10gradle.properties配置开发环境、映射、运行和重映射 JAR
Gradle Wrapper9.2.1gradle/wrapper/gradle-wrapper.properties确保每台电脑使用同一 Gradle 版本
JDK21build.gradlefabric.mod.json编译和运行本模板
最常见的第一处坑:很多旧版 Minecraft 1.20 教程会让你安装 JDK 17,但这个具体模板已经把源码级别和运行依赖写成 21。照搬别人的环境结论,IntelliJ 可能看起来能打开工程,Gradle 却会报 toolchain、release 或 class file 版本错误。本课的原则很简单:先看当前工程,再配环境。

打开 IntelliJ 后,在 Project Structure 把 Project SDK 设为 JDK 21;在 Settings → Build Tools → Gradle 中,把 Gradle JVM 也指向同一套 JDK 21,并保持 Distribution 使用 Wrapper。然后在工程根目录执行:

PowerShell · 环境与空工程检查
java -version
javac -version
./gradlew.bat build

本模板已实际执行干净构建,10 个 Gradle 任务全部完成,最终显示 BUILD SUCCESSFUL。先让原工程成功,再开始改代码,是最省时间的调试方法。

Flow · 一眼看懂施法链

左键之后并不会立刻放火,代码要经过三道“关卡”。

流程图里的每一个出口都有意义。普通物品走 PASS,等于告诉 Fabric:“这不是我的业务,请继续执行 Minecraft 原本的逻辑。”旁观者或没有修改权限的玩家走 FAIL,表示这次动作被明确拒绝。只有手持魔法棒、权限允许并且来到服务器端时,才真正改变世界。这样的分层能防止魔法棒抢走所有物品的左键行为,也能避免客户端制造只存在一瞬间的“假火焰”。

Fabric 魔法棒左键方块事件流程图:检查手持物、权限和服务端,然后放火、播放声音并返回成功
完整调用链:攻击事件 → 判断魔法棒 → 判断权限 → 区分客户端与服务器 → 替换方块 → 播放声音 → 返回结果。排错时沿着箭头逐层检查,比盯着整段代码猜更可靠。

File map · 先认路再写代码

这个项目很小,但 Java、显示资源和游戏数据各有自己的房间。

src/main/java 放运行逻辑;assets/maik 放名称、模型和贴图;data/maik 放合成配方。这里的 maik 是 Mod ID,也是资源命名空间。只要路径里不小心写成 magicMaik 或别的名字,代码仍可能编译,但游戏会找不到对应资源。

maik-template-1.20 · 关键文件结构
maik-template-1.20/
├─ build.gradle
├─ gradle.properties
├─ gradlew.bat
└─ src/main/
   ├─ java/com/malaoshi/maik/
   │  ├─ Maik.java
   │  ├─ item/
   │  │  └─ ModItems.java
   │  └─ event/
   │     └─ MagicWandEvents.java
   └─ resources/
      ├─ fabric.mod.json
      ├─ assets/maik/
      │  ├─ lang/
      │  │  ├─ zh_cn.json
      │  │  └─ en_us.json
      │  ├─ models/item/magic_wand.json
      │  └─ textures/item/magic_wand.png
      └─ data/maik/recipes/magic_wand.json

Java 决定行为

Maik 启动注册,ModItems 创造物品,MagicWandEvents 处理玩家左键。

assets 决定“看起来怎样”

名称、手持显示方式和像素图都在这里。紫黑错误纹理通常就是这一层断开。

data 决定玩法数据

配方不需要写进 Java。JSON 正确后,数据包系统会自动读取。

命名空间负责连接

maik:magic_wand 像物品的完整地址,所有文件最终都要回到这个地址。

Entrypoint · 总开关

Maik.java 不负责施法,它负责把两个系统接上电源。

fabric.mod.jsoncom.malaoshi.maik.Maik 声明为主入口。Fabric Loader 创建模组时会调用 onInitialize()。这里先注册物品和物品栏,再注册攻击方块事件。把入口类想成剧场的舞台监督:它不亲自扮演魔法师,但要确认演员、灯光和音效都已经到位。

src/main/java/com/malaoshi/maik/Maik.java
package com.malaoshi.maik;

import com.malaoshi.maik.event.MagicWandEvents;
import com.malaoshi.maik.item.ModItems;
import net.fabricmc.api.ModInitializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Maik implements ModInitializer {
    public static final String MOD_ID = "maik";
    public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);

    @Override
    public void onInitialize() {
        ModItems.register();
        MagicWandEvents.register();
        LOGGER.info("Maik mod initialized");
    }
}

MOD_ID 只定义一次,其他类通过 Maik.MOD_ID 使用它,可以减少手写字符串造成的拼写分叉。日志也使用同一个 ID;将来读 run/logs/latest.log 时,很容易看出哪一行来自自己的模组。

顺序为什么这样写?事件回调运行时会访问 ModItems.MAGIC_WAND。先完成物品注册,再安装事件监听,阅读上更符合依赖顺序。真正的事件只会在玩家进入世界并攻击方块时触发,不会在初始化阶段直接放火。

Registry · 给魔法棒发“身份证”

ModItems.java 做两件事:注册物品,并把它放进创造模式分类。

src/main/java/com/malaoshi/maik/item/ModItems.java
package com.malaoshi.maik.item;

import com.malaoshi.maik.Maik;
import net.fabricmc.fabric.api.itemgroup.v1.ItemGroupEvents;
import net.minecraft.item.Item;
import net.minecraft.item.ItemGroups;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.util.Identifier;

public final class ModItems {
    public static final Item MAGIC_WAND = Registry.register(
            Registries.ITEM,
            new Identifier(Maik.MOD_ID, "magic_wand"),
            new Item(new Item.Settings().maxCount(1))
    );

    private ModItems() {
    }

    public static void register() {
        ItemGroupEvents.modifyEntriesEvent(ItemGroups.TOOLS)
                .register(entries -> entries.add(MAGIC_WAND));
    }
}
1

选择物品注册表

Registries.ITEM 表示我们要登记的是物品。方块、实体、声音都有各自的注册表,不能混用。

2

组合唯一 ID

new Identifier("maik", "magic_wand") 得到完整 ID maik:magic_wand。命令、翻译、模型和配方都围绕它工作。

3

设置最多堆叠一个

maxCount(1) 让魔法棒像工具一样单独占一格。它现在还没有耐久度,因此使用不会损耗。

4

加入工具分类

ItemGroups.TOOLS 让创造模式玩家能在工具与实用物品分类找到它。这只影响展示,不等于注册本身。

如果漏掉物品组代码,/give @s maik:magic_wand 仍可能成功;如果漏掉 Registry.register,那么翻译、模型和配方写得再漂亮也没有对象可以连接。排错时一定要分清“游戏里有没有这个物品”和“创造物品栏有没有展示它”是两件事。

Spell logic · 魔法发生的地方

真正的咒语不是一句 setBlockState,而是前后完整的安全检查。

src/main/java/com/malaoshi/maik/event/MagicWandEvents.java
package com.malaoshi.maik.event;

import com.malaoshi.maik.item.ModItems;
import net.fabricmc.fabric.api.event.player.AttackBlockCallback;
import net.minecraft.block.Block;
import net.minecraft.block.Blocks;
import net.minecraft.sound.SoundCategory;
import net.minecraft.sound.SoundEvents;
import net.minecraft.util.ActionResult;

public final class MagicWandEvents {
    private MagicWandEvents() {
    }

    public static void register() {
        AttackBlockCallback.EVENT.register((player, world, hand, pos, direction) -> {
            if (!player.getStackInHand(hand).isOf(ModItems.MAGIC_WAND)) {
                return ActionResult.PASS;
            }

            if (player.isSpectator() || !world.canPlayerModifyAt(player, pos)) {
                return ActionResult.FAIL;
            }

            if (!world.isClient) {
                world.setBlockState(pos, Blocks.FIRE.getDefaultState(), Block.NOTIFY_ALL);
                world.playSound(
                        null,
                        pos,
                        SoundEvents.ITEM_FIRECHARGE_USE,
                        SoundCategory.BLOCKS,
                        1.0F,
                        1.0F
                );
            }

            return ActionResult.SUCCESS;
        });
    }
}

第一步:监听的是“攻击方块”,所以操作是左键

AttackBlockCallback 在玩家尝试攻击方块时触发。回调把五份现场信息交给我们:player 是谁在操作,world 是哪个世界,hand 是哪只手,pos 是被瞄准方块的位置,direction 是点击到的面。当前模板没有使用 direction,因为它直接替换被点击的方块,而不是在方块旁边生成火。

第二步:不是魔法棒就马上 PASS

player.getStackInHand(hand) 取得触发这次动作的手持堆栈,isOf(ModItems.MAGIC_WAND) 判断它是不是注册过的那根魔法棒。条件前的感叹号表示“不是”。返回 PASS 后,Fabric 会允许其他监听器或原版游戏继续处理,玩家拿镐子挖矿、空手打方块都不会被魔法逻辑劫持。

第三步:旁观者和无权限位置不能施法

player.isSpectator() 防止旁观者改世界;world.canPlayerModifyAt(player, pos) 会结合游戏规则和位置权限判断玩家能否修改目标。任一条件不满足就返回 FAIL。这不是多余的礼貌检查:服务器出生点保护、冒险玩法或其他权限系统都可能依赖它。

第四步:世界修改只在服务器端执行

Minecraft 单人世界内部也同时有客户端和逻辑服务器。客户端负责画面和输入,服务器拥有权威世界状态。if (!world.isClient) 意味着只有服务器端执行方块替换和声音广播。如果两边都改,可能出现重复动作;如果只在客户端改,玩家会先看见火,等服务器同步后又突然恢复,像一个失败的幻术。

第五步:把目标方块本身替换为火焰

world.setBlockState(pos, Blocks.FIRE.getDefaultState(), Block.NOTIFY_ALL) 使用的正是 pos,所以原方块会被替换,而不是在表面上方多放一格火。Block.NOTIFY_ALL 要求相关方块更新并把变化同步给客户端。火焰能否持续存在还要看周围方块和原版火焰规则;如果目标位置不适合,火可能很快熄灭。

第六步:声音由服务器广播

playSound 的第一个参数是 null,表示没有需要排除的玩家,附近玩家都能听见。声音选择火焰弹使用音效,分类是 BLOCKS,音量和音调都是 1.0F。画面和声音同时出现,魔法棒的反馈会比静悄悄换方块更有“施法感”。

这根魔法棒会破坏目标方块:因为代码直接把 pos 替换成火焰。请先在新建测试世界使用,不要对重要建筑试验。教程后面的扩展会演示如何改成在点击面旁边放火,从而保留原方块。

Texture pipeline · 从 ID 走到像素

Java 只告诉游戏“有一根魔法棒”;它长什么样,要由三层资源接力完成。

maik 模组的 32×32 透明像素魔法棒贴图

Original texture · 原工程贴图

32×32 透明 PNG,像素小但信息完整

原图只有 32×32 像素,文件名是 magic_wand.png。橙红色杖身沿对角线展开,尖端有明亮高光,透明区域让它能自然叠在物品栏和玩家手中。预览区使用最近邻方式放大,因此每个像素仍保持清晰方块,而不是被浏览器抹成模糊色块。

制作自己的版本时,请保留透明背景,使用 PNG,并避免把文件保存成隐藏的 magic_wand.png.png。Minecraft 支持更大贴图,但 16×16、32×32、64×64 这类尺寸更容易保持原版像素风格。

注册 ID
maik:magic_wand
模型文件
models/item/magic_wand.json
纹理引用
maik:item/magic_wand
PNG 文件
textures/item/magic_wand.png

模型:告诉游戏按“手持工具”方式绘制

assets/maik/models/item/magic_wand.json
{
  "parent": "minecraft:item/handheld",
  "textures": {
    "layer0": "maik:item/magic_wand"
  }
}

minecraft:item/handheld 适合剑、斧、魔杖等斜着拿的工具。layer0 的值不写 assets、不写 textures、也不写 .png;资源系统会把 maik:item/magic_wand 自动翻译成 assets/maik/textures/item/magic_wand.png

翻译:让注册 ID 变成读者看得懂的名字

assets/maik/lang/zh_cn.json
{
  "item.maik.magic_wand": "魔法棒"
}
assets/maik/lang/en_us.json
{
  "item.maik.magic_wand": "Magic Wand"
}

物品翻译键的格式是 item.命名空间.路径。如果游戏显示原始键 item.maik.magic_wand,说明物品注册已经成功,问题只在语言文件的路径、文件名、JSON 语法或当前游戏语言,不要回头乱改 Java 注册代码。

Recipe · 给玩家一条获得路线

一份烈焰粉、两根木棍,竖着摆出一根带火焰核心的法杖。

烈焰粉
B
木棍
S
木棍
S
合成结果魔法棒
魔法棒 × 1
maik:magic_wand
data/maik/recipes/magic_wand.json
{
  "type": "minecraft:crafting_shaped",
  "pattern": [
    " B ",
    " S ",
    " S "
  ],
  "key": {
    "B": { "item": "minecraft:blaze_powder" },
    "S": { "item": "minecraft:stick" }
  },
  "result": {
    "item": "maik:magic_wand"
  }
}

pattern 中的空格也算格子,所以三行字符串必须保持三个字符。BS 只是配方内部代号,可以换字母,但必须与 key 对应。结果使用完整注册 ID。这个配方属于 data,因为服务器需要知道玩家放入哪些材料以及产出什么;贴图则属于 assets,因为它负责显示。

Run & verify · 分层验收

不要只看“游戏启动了”;用四个证据确认每一层都真的连通。

1

先编译

在工程根目录执行 ./gradlew.bat build。这一步会编译 Java、处理 JSON 与资源并生成重映射 JAR。看到 BUILD SUCCESSFUL 才进入游戏测试。

2

启动开发客户端

在 IntelliJ 的 Gradle 工具窗口运行 Tasks → fabric → runClient,或者执行 ./gradlew.bat runClient。第一次启动会准备依赖,控制台暂时停在下载信息不等于卡死。

3

取得物品

进入允许作弊的测试世界,输入 /give @s maik:magic_wand。中文语言下名称应为“魔法棒”,物品栏中显示橙红色像素贴图。

4

验证施法

把魔法棒拿在手里,瞄准一块允许修改、下方有支撑的测试方块并按左键。观察目标变成火焰,同时听见火焰弹声音。请远离木屋、树林和重要建筑。

PowerShell 与游戏内命令
./gradlew.bat build
./gradlew.bat runClient

/give @s maik:magic_wand

真实运行效果:魔法棒已经在游戏里把方块点燃

下面不是概念图,也不是后期合成的效果图。我们用本课的 maik-template-1.20 工程启动了真实的 Fabric 1.20 开发客户端,进入创造模式测试世界,把 maik:magic_wand 放到快捷栏第一格,再对中央的下界岩方块按下左键。事件回调通过手持物与权限检查后,由服务器把目标方块替换成火焰;截图是在效果仍然运行时直接按游戏的 F2 键保存的。

Minecraft Fabric 1.20 开发客户端中,玩家手持自定义魔法棒,中央下界岩方块被左键点燃并冒出烟雾
真实游戏验证:画面右侧是玩家手中的橙红色魔法棒,底部快捷栏第一格显示同一个自定义道具;准星下方的目标方块已经变成火焰,并持续冒出黑色烟雾。为了避免误烧建筑,测试区特意搭在高空的下界岩平台上。
你也可以截出同样的图:让火焰、手持魔法棒和快捷栏同时留在画面中,等聊天提示淡出后按 F2。原始 PNG 会保存在项目的 run/screenshots 目录;这比系统截屏更干净,因为不会把 IntelliJ、窗口边框或其他桌面内容一起拍进去。

证据 A:命令有自动补全

输入 maik: 后能看到 magic_wand,说明注册表里确实存在这件物品。

证据 B:名称不是原始键

显示“魔法棒”,说明 zh_cn.json 已被读取且键名正确。

证据 C:图像不是紫黑格

手中和物品栏都显示法杖,说明模型 JSON 与 PNG 路径连接成功。

证据 D:普通物品不施法

换成木棍左键方块,不应触发放火,证明 PASS 分支工作正常。

证据 E:火焰同步稳定

方块变化对服务器和客户端一致,不会闪一下又恢复,说明修改发生在服务器端。

证据 F:能听到声音

附近玩家听见火焰弹音效,说明服务端声音广播已经执行。

Debug clinic · 按症状找层级

一次只追第一条根因;不要同时改 Java、JSON、版本和目录。

Gradle 报 Java 版本或 release 21 错误

执行 java -versionjavac -version,再检查 IntelliJ 的 Project SDK 与 Gradle JVM。本模板的 build.gradlefabric.mod.json 都要求 21,只把编辑器语言级别改成 21、但让 Gradle 继续使用 JDK 17,并不能解决构建问题。

我右键方块,什么都没有发生

这是预期行为。本模板注册的是 AttackBlockCallback,对应攻击方块,也就是左键。右键要使用 UseBlockCallback 或自定义 Item#use 等另一套入口,不能只把教程里的“左键”三个字理解成右键。

/give 找不到 maik:magic_wand

先看 fabric.mod.json 的 main 入口是否仍指向 com.malaoshi.maik.Maik,再看 onInitialize() 是否调用 ModItems.register()。检查控制台有没有 Maik mod initialized。如果入口都没加载,先不要查贴图。

有物品,但它是紫黑错误贴图

Java 注册已经通过。依次检查 assets/maik/models/item/magic_wand.json、其中的 maik:item/magic_wand、以及 assets/maik/textures/item/magic_wand.png。大小写、下划线和扩展名必须准确,JSON 不能有注释或尾逗号。

名称显示 item.maik.magic_wand

这是翻译层失联。简体中文必须放在小写的 zh_cn.json,键必须是 item.maik.magic_wand,文件保存为 UTF-8。游戏使用英语时会读取 en_us.json,所以也要检查当前语言。

拿到魔法棒,但左键仍在普通挖掘

检查 MagicWandEvents.register() 是否在 onInitialize() 中调用;再确认回调里的比较对象是同一个 ModItems.MAGIC_WAND。如果新建了第二个 Item 实例来比较,即使外观看起来一样,也不是注册表中的同一个对象。

火焰出现一下就熄灭

事件很可能已经成功。原版火焰还会检查所在位置是否能存活,周围没有可燃物、下方不合适或方块更新都可能让它熄灭。用一块独立的地面方块测试,并记住当前代码是替换目标方块本身。

在某些区域完全不能施法

模板主动调用 canPlayerModifyAt。旁观模式、服务器出生点保护、冒险规则或权限插件都可能拒绝修改。这是安全保护,不一定是 bug。换到自己拥有权限的测试区域再试。

客户端看到火,随后又恢复

这种现象通常说明世界修改错误地发生在客户端。确认 setBlockStateplaySound 仍位于 if (!world.isClient) 内。世界状态应由服务器决定,再同步给客户端。

推荐排错顺序:build 判断 Java 与 JSON;再查日志确认入口;再用 /give 判断注册;随后查名称、模型和贴图;最后才测试事件、权限、服务端同步和火焰规则。每一步只回答一个问题。

Creative extensions · 把范例变成玩法

先做一个最温和的改进:保留目标方块,只在点击的那一面放火。

回调已经提供 direction,它表示玩家点中了方块的哪一面。把目标位置沿这个方向移动一格,就得到贴着表面的相邻位置。例如点击地面顶面,pos.offset(direction) 就是地面上方的空气格。加入空气检查后,魔法棒不会直接吞掉石头、箱子或建筑方块。

可选扩展 · 替换服务器端的放火代码
BlockPos firePos = pos.offset(direction);

if (world.getBlockState(firePos).isAir()) {
    world.setBlockState(
            firePos,
            Blocks.FIRE.getDefaultState(),
            Block.NOTIFY_ALL
    );
    world.playSound(
            null,
            firePos,
            SoundEvents.ITEM_FIRECHARGE_USE,
            SoundCategory.BLOCKS,
            1.0F,
            1.0F
    );
}

使用这段代码时要导入 net.minecraft.util.math.BlockPos。扩展后再次按“编译 → 启动 → 测试世界 → 普通物品对照”的顺序验证。不要一次同时加入冷却、耐久和粒子,否则出错时很难判断是哪一步造成的。

LEVEL 1

给魔法棒加耐久

研究 Item.Settings().maxDamage(...),施法成功后损耗 1 点。思考为什么有耐久物品不能再设置普通堆叠数量。

LEVEL 2

增加冷却时间

施法后用玩家的物品冷却管理器限制连续触发,让魔法棒不再像打火机一样高速连点。

LEVEL 3

加入粒子轨迹

从玩家视线到目标位置生成少量火焰或末地烛粒子,但世界修改仍必须以服务器为准。

三分钟自测

为什么拿木棍左键不会触发魔法?

回调首先比较手持物是否为 ModItems.MAGIC_WAND。不是就返回 PASS,把操作交还给其他监听器和原版逻辑。

为什么不能删掉 !world.isClient

真正的世界状态由服务器维护。两端同时修改容易重复,只在客户端修改又会被服务器状态覆盖。

模型中的纹理为什么不写 .png

Minecraft 资源位置使用 命名空间:目录/文件名 的逻辑 ID,加载器会自动到 assets/命名空间/textures 下寻找 PNG。

PASSFAILSUCCESS 有什么区别?

PASS 表示本监听器不处理,让后续逻辑继续;FAIL 明确拒绝动作;SUCCESS 表示已经处理完成。正确返回值能避免魔法棒干扰其他物品。

Build & deliver · 生成可安装模组

测试世界通过后,执行一次干净构建,把源码变成最终 JAR。

PowerShell · 最终构建
./gradlew.bat clean build

构建成功后,成品位于 build/libs/maik-1.0.0.jar。同目录里的 maik-1.0.0-sources.jar 是源码包,方便阅读或开发,不是普通玩家要安装的模组文件。将成品放入匹配 Minecraft 1.20、Fabric Loader 和 Fabric API 的客户端实例 mods 文件夹,并先用备份过的测试世界启动。

下载本课完整 Fabric 魔法棒工程

下载包来自本教程分析的 maik-template-1.20,包含 Gradle Wrapper、Java 源码、Fabric 元数据、中文与英文翻译、模型、32×32 魔法棒贴图和合成配方,不包含本机的运行日志与缓存目录。

Evidence · 本课依据

每一个版本号、路径和行为都能回到模板中的具体文件。

  • gradle.properties:Minecraft、Yarn、Loader、Loom、Fabric API 和模组版本。
  • build.gradle:Loom 配置、依赖、Java 21 源码级别、资源处理和重映射构建。
  • fabric.mod.json:Mod ID、入口类、运行环境、Java 与 Fabric 依赖。
  • ModItems.javamaik:magic_wand 注册、最大堆叠数量和创造模式分类。
  • MagicWandEvents.java:左键事件、手持物检查、权限、服务端放火和音效。
  • assets/maikdata/maik:翻译、模型、原始 32×32 PNG 贴图和工作台配方。

Lesson complete

现在你看到的不是一根魔法棒,而是一条完整的事件编程链。

玩家的左键是入口,注册 ID 是身份,回调参数是现场信息,权限判断是安全门,服务器端是最终裁判,模型与贴图负责把逻辑变成视觉反馈。掌握这条链以后,你可以把“放火”替换成种花、结冰、召唤闪电、生成粒子或触发机关。每次只换一个环节,先预测结果,再运行验证,你就从复制代码走向真正设计玩法了。