原创教程 · 环境:Minecraft 1.20.1 / Forge 47.x · 示例 modid
mymod前置:03. 物品注册、04. 注册表;方法签名经 Forge 1.20.1-47.3.0 jar 实证
20. 物品进阶:功能与动画¶
目录¶
- 物品进阶:从「一个图标」到「一件装备」
- 让物品能右键使用(use / useOn / 冷却)
- 食物、耐久与工具:让物品有「数值」
- 属性修饰符与提示文本
- 物品动画(一):零代码的贴图逐帧与手持姿态
- 物品动画(二):自定义渲染器 BlockEntityWithoutLevelRenderer
- 物品动画(三):路线选择与踩坑清单
物品进阶:从「一个图标」到「一件装备」¶
你已经学会了注册物品,并且可以在游戏中看到它了。但是,仅仅能拿在手里还不够,我们需要让物品有功能、有动画。本章将带你了解如何让物品变得更加丰富和有趣。
首先,我们来看一下物品进阶的全景图: 1. 覆写点全景表:了解哪些方法可以覆写来实现不同的功能。 2. 右键使用与冷却:让物品有右键使用的功能,并且可以设置冷却时间。 3. 食物/耐久/工具:让物品成为食物、有耐久度的工具或者攻击工具。 4. 属性与提示:设置物品的属性和提示文本。 5. 贴图逐帧:实现物品的贴图逐帧动画。 6. 自定义渲染器:使用自定义渲染器来实现更加复杂的动画和效果。
下面是「我想做 X → 覆写哪个方法」的对照表:
| 功能 | 覆写方法 |
| --- | --- |
| 右键使用 | use |
| 右键使用方块 | useOn |
| 长按吃 | finishUsingItem |
| 命中实体 | hurtEnemy |
| 挖掘方块 | mineBlock |
| 设置属性 | getDefaultAttributeModifiers |
| 设置提示文本 | appendHoverText |
| 设置光效 | isFoil |
注意:所有这些覆写方法都在服务端生效,客户端只负责表现。也就是说,你的物品功能都是在服务端实现的,客户端只是显示出来。接下来,我们将逐步介绍每个部分的实现细节。
让物品能右键使用(use / useOn / 冷却)¶
让我们开始学习如何让物品能够右键使用。我们将介绍 use 和 useOn 的区别、返回值、冷却机制以及动作栏提示。
首先,我们需要了解 use 和 useOn 的区别。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()
耐久¶
接下来,我们来看一下耐久的实现。我们可以使用 durability(n) 来设置物品的耐久度。例如:
hurtAndBreak 来减少物品的耐久度:
此外,我们可以使用 getDamageValue 和 setDamageValue 来获取和设置物品的当前耐久度:
最后,我们可以使用 isValidRepairItem 来检查一个物品是否可以用来修复我们的物品:
工具¶
最后,我们来看一下工具的实现。我们可以使用 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 方法来设置物品是否具有附魔光效。
通过这些方法,我们可以自定义物品的属性修饰符和提示文本,丰富游戏的体验。例如,我们可以创建一个头盔,戴在头上可以增加最大生命值。
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 文件来实现贴图逐帧动画。这个文件需要与你的贴图文件同名同目录。下面是一个例子:
frametime 指定了每一帧的显示时间(单位为 tick),frames 列出了帧的顺序。你的贴图文件应该按照这个顺序排列,例如 xx_0.png、xx_1.png、xx_2.png 和 xx_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]
}
}
}
- 旋转参数使用的是欧拉角,单位为度。顺序为 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只在注册事件里创建一次,之后永远从EntityModelSet里bakeLayer取。每帧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 对象 |
怎么验证¶
- 拿在手里:应该能看到模型自转 + 上下漂浮;放进物品栏(GUI)时姿态更"正"
- 换个玩家视角看:模型跟随视角方向正常渲染,没有糊到脸上或穿进屏幕(说明
pose变换合理) - 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。