跳转至

原创教程 · 环境:Minecraft 1.20.1 / Forge 47.x · 示例 modid mymod 前置:03. 物品注册04. 注册表;方法签名经 Forge 1.20.1-47.3.0 jar 实证

20. 物品进阶:功能与动画

目录

  1. 物品进阶:从「一个图标」到「一件装备」
  2. 让物品能右键使用(use / useOn / 冷却)
  3. 食物、耐久与工具:让物品有「数值」
  4. 属性修饰符与提示文本
  5. 物品动画(一):零代码的贴图逐帧与手持姿态
  6. 物品动画(二):自定义渲染器 BlockEntityWithoutLevelRenderer
  7. 物品动画(三):路线选择与踩坑清单

物品进阶:从「一个图标」到「一件装备」

你已经学会了注册物品,并且可以在游戏中看到它了。但是,仅仅能拿在手里还不够,我们需要让物品有功能、有动画。本章将带你了解如何让物品变得更加丰富和有趣。

首先,我们来看一下物品进阶的全景图: 1. 覆写点全景表:了解哪些方法可以覆写来实现不同的功能。 2. 右键使用与冷却:让物品有右键使用的功能,并且可以设置冷却时间。 3. 食物/耐久/工具:让物品成为食物、有耐久度的工具或者攻击工具。 4. 属性与提示:设置物品的属性和提示文本。 5. 贴图逐帧:实现物品的贴图逐帧动画。 6. 自定义渲染器:使用自定义渲染器来实现更加复杂的动画和效果。

下面是「我想做 X → 覆写哪个方法」的对照表: | 功能 | 覆写方法 | | --- | --- | | 右键使用 | use | | 右键使用方块 | useOn | | 长按吃 | finishUsingItem | | 命中实体 | hurtEnemy | | 挖掘方块 | mineBlock | | 设置属性 | getDefaultAttributeModifiers | | 设置提示文本 | appendHoverText | | 设置光效 | isFoil |

注意:所有这些覆写方法都在服务端生效,客户端只负责表现。也就是说,你的物品功能都是在服务端实现的,客户端只是显示出来。接下来,我们将逐步介绍每个部分的实现细节。


让物品能右键使用(use / useOn / 冷却)

让我们开始学习如何让物品能够右键使用。我们将介绍 useuseOn 的区别、返回值、冷却机制以及动作栏提示。

首先,我们需要了解 useuseOn 的区别。use 方法用于处理右键空气或物品的行为,而 useOn 方法用于处理右键方块的行为。两者都返回一个 InteractionResultHolder 对象,表示操作的结果。

InteractionResultHolder 对象有四种返回值: - success(stack): 操作成功,物品不消耗。 - consume(stack): 操作成功,物品消耗。 - fail(stack): 操作失败,物品不消耗。 - pass(stack): 操作失败,物品不消耗,不会挥手。

use 方法中,我们可以使用 level.isClientSide 来判断当前是否在客户端。如果在客户端,我们可以直接返回 pass(stack),因为客户端不需要处理物品的使用逻辑。

如果我们想要添加冷却机制,我们可以使用 player.getCooldowns().addCooldown(this, 20) 来设置冷却时间。同时,我们可以使用 isOnCooldown 来检查物品是否正在冷却。

下面是一个完整的例子,实现一个 WandItem 类,右键发射一颗雪球并进 1 秒冷却:

public class WandItem extends Item {
    public WandItem(Properties properties) {
        super(properties);
    }

    @Override
    public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
        if (level.isClientSide) {
            return InteractionResultHolder.pass(player.getItemInHand(hand));
        }

        if (player.getCooldowns().isOnCooldown(this)) {
            player.displayClientMessage(Component.translatable("cooldown"), true);
            return InteractionResultHolder.fail(player.getItemInHand(hand));
        }

        Snowball snowball = new Snowball(level, player);
        snowball.shootFromRotation(player, player.getXRot(), player.getYRot(), 0.0F, 1.5F, 1.0F);
        level.addFreshEntity(snowball);

        player.getCooldowns().addCooldown(this, 20);
        return InteractionResultHolder.success(player.getItemInHand(hand));
    }
}
另一个例子是使用 useOn 方法来记录坐标:
public class CoordinateItem extends Item {
    public CoordinateItem(Properties properties) {
        super(properties);
    }

    @Override
    public InteractionResult useOn(UseOnContext context) {
        BlockPos pos = context.getClickedPos();
        context.getPlayer().displayClientMessage(Component.translatable("coordinate", pos.getX(), pos.getY(), pos.getZ()), true);
        return InteractionResult.SUCCESS;
    }
}
在这个例子中,我们使用 useOn 方法来处理右键方块的行为。我们获取点击的方块坐标,并将其显示在动作栏中。

Tip

记得在使用 displayClientMessage 时,检查是否在客户端,以避免服务端崩溃。同时,使用 isOnCooldown 来检查物品是否正在冷却,可以避免重复使用物品。


食物、耐久与工具:让物品有「数值」

让我们开始学习如何让物品拥有「数值」,使其变得更加有趣和实用。我们将探讨食物、耐久和工具的相关内容。

食物

首先,我们来看一下食物的实现。我们可以使用 FoodProperties.Builder 来创建一个食物的属性。例如:

new FoodProperties.Builder()
    .nutrition(6) // 饱食度
    .saturationMod(0.6F) // 饱和度系数
    .effect(() -> new MobEffectInstance(MobEffects.MOVEMENT_SPEED, 200, 0), 1.0F) // 效果
    .alwaysEat() // 总是可以吃
    .fast() // 吃得快
    .meat() // 给狼吃
    .build()
然后,我们可以将这个属性应用到我们的物品上:
new Item.Properties().food(FOOD)
这样,我们就可以创建一个具有特定属性的食物物品了。

耐久

接下来,我们来看一下耐久的实现。我们可以使用 durability(n) 来设置物品的耐久度。例如:

new Item.Properties().durability(10)
这意味着我们的物品可以使用 10 次后就会破损。我们也可以使用 hurtAndBreak 来减少物品的耐久度:
stack.hurtAndBreak(1, entity, e -> e.broadcastBreakEvent(slot))
此外,我们可以使用 getDamageValuesetDamageValue 来获取和设置物品的当前耐久度:
int damage = stack.getDamageValue();
stack.setDamageValue(damage + 1);
最后,我们可以使用 isValidRepairItem 来检查一个物品是否可以用来修复我们的物品:
if (stack.isValidRepairItem(toRepair, repair)) {
    // 修复物品
}

工具

最后,我们来看一下工具的实现。我们可以使用 Tier 接口来创建一个工具的等级。例如:

public enum MyTier implements Tier {
    MY_TIER;

    @Override
    public int getUses() {
        return 100;
    }

    @Override
    public float getSpeed() {
        return 5.0F;
    }

    @Override
    public float getAttackDamageBonus() {
        return 2.0F;
    }

    @Override
    public int getLevel() {
        return 2;
    }

    @Override
    public int getEnchantmentValue() {
        return 10;
    }

    @Override
    public Ingredient getRepairIngredient() {
        return Ingredient.of(Items.DIAMOND);
    }
}
然后,我们可以使用这个等级来创建一个工具物品。例如:
public class MySwordItem extends SwordItem {
    public MySwordItem() {
        super(MyTier.MY_TIER, 3, -2.4F, new Item.Properties());
    }
}
我们也可以使用 TierSortingRegistry 来注册我们的等级:
TierSortingRegistry.registerTier("my_tier", MyTier.MY_TIER, Arrays.asList(Tier.WOOD), Arrays.asList(Tier.STONE));
这样,我们就可以创建一个具有特定等级的工具物品了。

完整的代码示例:

public enum MyTier implements Tier {
    MY_TIER;

    @Override
    public int getUses() {
        return 100;
    }

    @Override
    public float getSpeed() {
        return 5.0F;
    }

    @Override
    public float getAttackDamageBonus() {
        return 2.0F;
    }

    @Override
    public int getLevel() {
        return 2;
    }

    @Override
    public int getEnchantmentValue() {
        return 10;
    }

    @Override
    public Ingredient getRepairIngredient() {
        return Ingredient.of(Items.DIAMOND);
    }
}

public class MySwordItem extends SwordItem {
    public MySwordItem() {
        super(MyTier.MY_TIER, 3, -2.4F, new Item.Properties());
    }
}

public class MyMod {
    public static void init() {
        TierSortingRegistry.registerTier("my_tier", MyTier.MY_TIER, Arrays.asList(Tier.WOOD), Arrays.asList(Tier.STONE));
    }
}

Tip

记得注册你的等级和工具物品,否则它们不会被游戏识别。


属性修饰符与提示文本

在 Minecraft 中,物品的属性修饰符可以影响玩家的能力和游戏体验。我们可以通过 getDefaultAttributeModifiers 方法来设置物品的默认属性修饰符。

@Override
public Multimap<Attribute, AttributeModifier> getDefaultAttributeModifiers(EquipmentSlot slot) {
    // 只在头盔槽位生效
    if (slot == EquipmentSlot.HEAD) {
        ImmutableMultimap.Builder<Attribute, AttributeModifier> builder = ImmutableMultimap.builder();
        builder.put(Attributes.MAX_HEALTH, new AttributeModifier(UUID.randomUUID(), "mymod.max_health", 10.0, AttributeModifier.Operation.ADDITION));
        return builder.build();
    }
    return super.getDefaultAttributeModifiers(slot);
}

在上面的例子中,我们设置了头盔槽位的最大生命值属性修饰符。注意,我们使用了 ImmutableMultimap.Builder 来构建属性修饰符的集合,这样可以确保只在头盔槽位生效。

除了设置默认属性修饰符外,我们还可以使用 Forge 事件 ItemAttributeModifierEvent 来动态地修改物品的属性修饰符。

@SubscribeEvent
public void onItemAttributeModifierEvent(ItemAttributeModifierEvent event) {
    // 添加属性修饰符
    event.addModifier(Attributes.ATTACK_DAMAGE, new AttributeModifier(UUID.randomUUID(), "mymod.attack_damage", 2.0, AttributeModifier.Operation.ADDITION));
    // 删除属性修饰符
    event.removeModifier(Attributes.MAX_HEALTH, new AttributeModifier(UUID.randomUUID(), "mymod.max_health", 10.0, AttributeModifier.Operation.ADDITION));
    // 删除所有属性修饰符
    event.clearModifiers();
}

在物品的提示文本中,我们可以使用 appendHoverText 方法来添加自定义的提示文本。

@Override
public void appendHoverText(ItemStack stack, @Nullable Level level, List<Component> tooltip, TooltipFlag flag) {
    tooltip.add(Component.translatable("tooltip.mymod.example").withStyle(ChatFormatting.GRAY));
    // 插到第一行
    tooltip.add(0, Component.translatable("tooltip.mymod.example2").withStyle(ChatFormatting.GRAY));
    // 只在高级提示中显示
    if (flag.isAdvanced()) {
        tooltip.add(Component.translatable("tooltip.mymod.example3").withStyle(ChatFormatting.GRAY));
    }
}

最后,我们可以使用 isFoil 方法来设置物品是否具有附魔光效。

@Override
public boolean isFoil(ItemStack stack) {
    return true;
}

通过这些方法,我们可以自定义物品的属性修饰符和提示文本,丰富游戏的体验。例如,我们可以创建一个头盔,戴在头上可以增加最大生命值。

public class MyItem extends Item {
    public MyItem(Properties properties) {
        super(properties);
    }

    @Override
    public Multimap<Attribute, AttributeModifier> getDefaultAttributeModifiers(EquipmentSlot slot) {
        if (slot == EquipmentSlot.HEAD) {
            ImmutableMultimap.Builder<Attribute, AttributeModifier> builder = ImmutableMultimap.builder();
            builder.put(Attributes.MAX_HEALTH, new AttributeModifier(UUID.randomUUID(), "mymod.max_health", 10.0, AttributeModifier.Operation.ADDITION));
            return builder.build();
        }
        return super.getDefaultAttributeModifiers(slot);
    }
}

Tip

注意,属性修饰符和提示文本的设置需要在物品的构造方法中完成。


物品动画(一):零代码的贴图逐帧与手持姿态

你会看到,给物品添加动画不一定需要写 Java 代码。我们先来看两种零代码的方法:贴图逐帧动画和手持姿态调整。

贴图逐帧动画

首先,我们可以通过创建一个 xx.png.mcmeta 文件来实现贴图逐帧动画。这个文件需要与你的贴图文件同名同目录。下面是一个例子:

{
  "animation": {
    "frametime": 2,
    "frames": [0, 1, 2, 3]
  }
}
在这个例子中,frametime 指定了每一帧的显示时间(单位为 tick),frames 列出了帧的顺序。你的贴图文件应该按照这个顺序排列,例如 xx_0.pngxx_1.pngxx_2.pngxx_3.png。你也可以将这些帧竖着拼在一起,形成一张长图。

手持姿态调整

另一种方法是调整物品的手持姿态。我们可以通过修改物品模型的 JSON 文件来实现这一点。具体来说,我们需要修改 display 段。下面是一个例子:

{
  "display": {
    "thirdperson_righthand": {
      "rotation": [0, 0, 0],
      "translation": [0, 0, 0],
      "scale": [1, 1, 1]
    },
    "firstperson_righthand": {
      "rotation": [0, 0, 0],
      "translation": [0, 0, 0],
      "scale": [1, 1, 1]
    },
    "gui": {
      "rotation": [30, 225, 0],
      "translation": [0, 0, 0],
      "scale": [0.625, 0.625, 0.625]
    }
  }
}
在这个例子中,我们定义了三个不同的显示方式:第三人称右手、第一人称右手和 GUI。每一种显示方式都有自己的旋转、平移和缩放参数。

  • 旋转参数使用的是欧拉角,单位为度。顺序为 yaw、pitch、roll。
  • 平移参数的单位是 1/16 个方块。
  • 缩放参数的单位是 1。

你可以使用 Blockbench 的 Display 面板来预览和调整这些参数。

做完这一步,你应该能看到你的物品在不同情况下的显示效果有所变化。这些变化可以让你的物品看起来更有趣、更生动。下一节,我们将继续探索更多关于物品动画的内容。!!! tip 你可以使用不同的贴图和手持姿态来创建出各种各样的动画效果。


物品动画(二):自定义渲染器 BlockEntityWithoutLevelRenderer

贴图逐帧只能让"画面"动,想让模型本体动起来(漂浮、旋转、开合、随蓄力变形),就要接管物品的渲染。Forge 的入口是 IClientItemExtensions#getCustomRenderer(),返回值是一个 BlockEntityWithoutLevelRenderer(社区简称 BEWLR,虽然名字带 Block,但它也负责物品)。

第 1 步:物品声明自己的渲染器

public class MagicCoreItem extends Item {
    public MagicCoreItem(Properties properties) {
        super(properties);
    }

    @Override
    public void initializeClient(Consumer<IClientItemExtensions> consumer) {
        consumer.accept(new IClientItemExtensions() {
            private MyItemRenderer renderer;

            @Override
            public BlockEntityWithoutLevelRenderer getCustomRenderer() {
                // 懒加载:第一次要用时才创建(构造器需要客户端的两个单例)
                if (renderer == null) {
                    renderer = new MyItemRenderer(
                            Minecraft.getInstance().getBlockEntityRenderDispatcher(),
                            Minecraft.getInstance().getEntityModels());
                }
                return renderer;
            }
        });
    }
}

initializeClient 来自 Forge 的 IForgeItem只在客户端调用,所以这个类里出现 Minecraft 是安全的(不会让服务端崩)。

第 2 步:定义模型层(只注册一次)

模型用 LayerDefinition 描述(跟实体模型同一套体系),通过 ModelLayerLocation 起个名字,之后渲染时"按名字取缓存":

public class MagicCoreModel {
    public static final ModelLayerLocation LAYER =
            new ModelLayerLocation(new ResourceLocation(MyMod.MODID, "magic_core"), "main");

    public static LayerDefinition createBodyLayer() {
        MeshDefinition mesh = new MeshDefinition();
        PartDefinition root = mesh.getRoot();
        // 中心一个 8×8×8 的立方体作为"核心"
        root.addOrReplaceChild("core", CubeListBuilder.create()
                        .texOffs(0, 0).addBox(-4.0F, -4.0F, -4.0F, 8, 8, 8),
                PartPose.ZERO);
        return LayerDefinition.create(mesh, 32, 32);
    }
}
@Mod.EventBusSubscriber(modid = MyMod.MODID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
public class ClientSetup {
    @SubscribeEvent
    public static void onRegisterLayers(EntityRenderersEvent.RegisterLayerDefinitions event) {
        event.registerLayerDefinition(MagicCoreModel.LAYER, MagicCoreModel::createBodyLayer);
    }
}

⚠️ LayerDefinition 只在注册事件里创建一次,之后永远EntityModelSetbakeLayer 取。每帧 LayerDefinition.create(...) 是典型的帧率杀手。

第 3 步:写 BEWLR

public class MyItemRenderer extends BlockEntityWithoutLevelRenderer {
    private static final ResourceLocation TEXTURE =
            new ResourceLocation(MyMod.MODID, "textures/item/magic_core.png");

    private final EntityModelSet modelSet;   // 构造器给的,存起来备用
    private ModelPart core;                  // 懒加载的模型部位

    public MyItemRenderer(BlockEntityRenderDispatcher dispatcher, EntityModelSet modelSet) {
        super(dispatcher, modelSet);
        this.modelSet = modelSet;
    }

    @Override
    public void renderByItem(ItemStack stack, ItemDisplayContext ctx, PoseStack pose,
                             MultiBufferSource buffer, int packedLight, int packedOverlay) {
        if (core == null) {
            core = modelSet.bakeLayer(MagicCoreModel.LAYER);
        }

        // renderByItem 没有 partialTick 参数,时间用毫秒换算(50ms = 1 tick)
        float time = (System.currentTimeMillis() % 100000L) / 50.0F;

        pose.pushPose();
        pose.translate(0.5D, 0.5D, 0.5D);                                  // 模型原点挪到中心
        pose.mulPose(Axis.YP.rotationDegrees(time * 3.0F));                // 绕竖轴转
        pose.translate(0.0D, Mth.sin(time * 0.1F) * 0.06D, 0.0D);          // 上下漂浮
        if (ctx != ItemDisplayContext.GUI) {
            pose.scale(0.6F, 0.6F, 0.6F);                                  // 手持时小一点,GUI 里保持原大小
        }
        core.render(pose, buffer.getBuffer(RenderType.entityCutout(TEXTURE)),
                packedLight, packedOverlay);
        pose.popPose();
    }
}

要点:

要点 说明
ItemDisplayContext GUI / GROUND / FIRST_PERSON_RIGHT_HAND / THIRD_PERSON_RIGHT_HAND / HEAD … 可按场景分支(GUI 里通常不需要漂浮旋转,看起来更"静")
时间源 renderByItem 没有 partialTick,用 System.currentTimeMillis() 或自己在 ClientTickEvent 里维护的 tick 计数;进阶可覆写 m_108829_ 之外的方式配合帧插值
贴图 BEWLR 用的是独立贴图textures/item/xxx.png),直接传给 RenderType.entityCutout(资源路径),不走物品图集
光照 packedLight 由调用方传入,GUI 里用 LightTexture.FULL_BRIGHT 的等效值;想"自发光"就传满亮度
性能 bakeLayer 结果缓存字段、pose 变换尽量少、渲染里不要 new 对象

怎么验证

  1. 拿在手里:应该能看到模型自转 + 上下漂浮;放进物品栏(GUI)时姿态更"正"
  2. 换个玩家视角看:模型跟随视角方向正常渲染,没有糊到脸上或穿进屏幕(说明 pose 变换合理)
  3. F3 看帧率:不应该比同类物品明显掉帧(掉了就是每帧重建模型或渲染里 new 对象)

Warning

  • BEWLR 不需要给物品设置 ItemBlockRenderTypes.setRenderLayer —— 那是方块渲染层的东西;物品这里只靠 RenderType.entityCutout/translucent 指定渲染方式
  • 物品类里的 initializeClient 必须保持"只在客户端被调用"的写法,不要Minecraft 相关字段写成 static 初始化(会让专用服务器启动崩溃)
  • 想让 GUI 里的图标也跟着动,注意 GUI 渲染的光照和缩放与手持不同,最好按 ItemDisplayContext 分开处理

Tip

需要骨骼动画(多个部件联动、关键帧动画、动画文件)时,手写 BEWLR 会很累,业界常用 GeckoLib 这类动画库来接管;普通"旋转+漂浮+开合"用本章这套原生写法足够了。


物品动画(三):路线选择与踩坑清单

你会看到四条路线:贴图逐帧、display 姿态、自定义渲染器(BlockEntityWithoutLevelRenderer,简称 BEWLR)和 GeckoLib。贴图逐帧适用于简单的帧动画,几乎零代码;display 姿态则用于调整物品在手里的显示位置和角度,需要在 Blockbench 里设置好模型的显示属性。BEWLR 适用于复杂的模型动画和自定义渲染,需要编写渲染器代码;GeckoLib 则是用于骨骼动画的强大库,能实现非常复杂的动画效果。

做完这一步,你应该能根据自己的需求选择合适的路线。然而,在实现物品动画时,常常会遇到一些坑。客户端代码放 common 侧会导致服务端崩溃,所以必须确保客户端专属代码放在 client 侧。忘了 Dist.CLIENT 注解会导致客户端代码在服务端执行,同样会崩溃。LayerDefinition 每帧重建会导致卡顿,所以必须通过 RegisterLayerDefinitions 注册一次。动画时间源不统一会导致不同步的问题,必须统一使用相同的时间源。GUI 里 BEWLR 不生效需要特殊处理,必须判 ItemDisplayContext.GUI 且注意光照问题。最后,透光贴图必须使用 RenderType.translucent 才能正确渲染。

Warning

想要骨骼动画上 GeckoLib。