Skip to main content
Version: 1.21.4

可消耗物品(Consumables)

可消耗物品(Consumables)指的是 物品,它们可以在一段时间内被使用,并在这个过程中被“消耗”。在 Minecraft 中,任何可以被吃或喝的东西都是某种类型的可消耗物品。

Consumable 数据组件(Data Component)

任何可以被消耗的物品都拥有 DataComponents#CONSUMABLE 组件。其背后的记录类型 Consumable 定义了物品被消耗的方式,以及消耗后需要应用的效果。

你可以直接调用记录构造函数创建一个 Consumable,也可以通过 Consumable#builder 创建,它会为每个字段设置默认值,最后调用 build 完成创建:

  • consumeSeconds —— 一个 float,表示完全消耗该物品所需的秒数。经过指定时间后会调用 Item#finishUsingItem。默认值为 1.6 秒,即 32 tick。
  • animation —— 设置物品使用过程中播放的 ItemUseAnimation。默认值为 ItemUseAnimation#EAT
  • sound —— 设置消耗物品时播放的 SoundEvent。必须是一个 Holder 实例。默认值为 SoundEvents#GENERIC_EAT
    • 如果原版实例不是 Holder<SoundEvent>,可以通过调用 BuiltInRegistries.SOUND_EVENT.wrapAsHolder(soundEvent) 获取一个 Holder 包装版本。
  • soundAfterConsume —— 设置物品完全被消耗后播放的 SoundEvent。这会委托给 PlaySoundConsumeEffect
  • hasConsumeParticles —— 当为 true 时,每四个 tick 以及物品完全消耗时会生成物品 粒子。默认为 true
  • onConsume —— 添加一个 ConsumeEffect,在物品通过 Item#finishUsingItem 被完全消耗后应用。

原版在 Consumables 类中提供了一些可消耗物品,例如用于 食物#defaultFood,以及用于 药水 和牛奶桶的 #defaultDrink

你可以通过调用 Item.Properties#component 方法为物品添加 Consumable 组件:

// 假设存在一个 DeferredRegister.Items ITEMS
public static final DeferredItem<Item> CONSUMABLE = ITEMS.registerSimpleItem(
"consumable",
new Item.Properties().component(
DataComponents.CONSUMABLE,
Consumable.builder()
// 消耗需要 2 秒(40 游戏刻)
.consumeSeconds(2f)
// 设置消耗时播放的动画
.animation(ItemUseAnimation.BLOCK)
// 每个刻播放消耗时的音效
.sound(SoundEvents.ARMOR_EQUIP_CHAIN)
// 消耗完成后播放的音效
.soundAfterConsume(SoundEvents.BREEZE_WIND_CHARGE_BURST)
// 吃的时候不显示粒子效果
.hasConsumeParticles(false)
.onConsume(
// 消耗完成后,有 30% 概率施加效果
new ApplyStatusEffectsConsumeEffect(new MobEffectInstance(MobEffects.HUNGER, 600, 0), 0.3F)
)
// 可以添加多个效果
.onConsume(
// 随机将实体传送到半径 50 格内的位置
new TeleportRandomlyConsumeEffect(100f)
)
.build()
)
);

ConsumeEffect

当一个可消耗物品(consumable)被使用完毕后,你可能希望触发某些逻辑,比如添加药水效果。这些逻辑由 ConsumeEffect(消耗效果) 处理,通过调用 Consumable.Builder#onConsume 方法将其添加到 Consumable(可消耗物品)中。

原版效果 的列表可以在 ConsumeEffect 中找到。

每个 ConsumeEffect 都有两个方法:getType,用于指定注册表对象 ConsumeEffect.Type;以及 apply,当物品被完全消耗时会调用该方法。apply 接收三个参数:实体所在的 Level(世界)、被消耗的 ItemStack(物品堆叠)、以及正在消耗物品的 LivingEntity(生物实体)。当效果成功应用时,该方法返回 true,如果失败则返回 false

你可以通过实现该接口,并将 ConsumeEffect.Type 及其对应的 MapCodecStreamCodec 注册BuiltInRegistries#CONSUME_EFFECT_TYPE,来创建自定义的 ConsumeEffect

public record UsePortalConsumeEffect(ResourceKey<Level> level)
implements ConsumeEffect, Portal {

@Override
public boolean apply(Level level, ItemStack stack, LivingEntity entity) {
if (entity.canUsePortal(false)) {
entity.setAsInsidePortal(this, entity.blockPosition());

// 可以成功使用传送门
return true;
}

// 无法使用传送门
return false;
}

@Override
public ConsumeEffect.Type<? extends ConsumeEffect> getType() {
// 设置为已注册的对象
return USE_PORTAL.get();
}

@Override
@Nullable
public TeleportTransition getPortalDestination(ServerLevel level, Entity entity, BlockPos pos) {
// 设置传送位置
}
}

// 在某个注册类中
// 假设存在一个 DeferredRegister<ConsumeEffect.Type<?>> CONSUME_EFFECT_TYPES
public static final Supplier<ConsumeEffect.Type<UsePortalConsumeEffect>> USE_PORTAL =
CONSUME_EFFECT_TYPES.register("use_portal", () -> new ConsumeEffect.Type<>(
ResourceKey.codec(Registries.DIMENSION).optionalFieldOf("dimension")
.xmap(UsePortalConsumeEffect::new, UsePortalConsumeEffect::level),
ResourceKey.streamCodec(Registries.DIMENSION)
.map(UsePortalConsumeEffect::new, UsePortalConsumeEffect::level)
));

// 针对添加了 CONSUMABLE 组件的某个 Item.Properties
Consumable.builder()
.onConsume(
new UsePortalConsumeEffect(Level.END)
)
.build();

ItemUseAnimation

ItemUseAnimation 在功能上相当于一个枚举(enum),除了其 id 和名称外并未定义其他内容。它的用途在 ItemHandRenderer#renderArmWithItem(第一人称)和 PlayerRenderer#getArmPose(第三人称)中被硬编码。因此,单纯创建一个新的 ItemUseAnimation,其作用只会类似于 ItemUseAnimation#NONE

如果你想为物品应用动画,需要为第一人称实现 IClientItemExtensions#applyForgeHandTransform,并/或为第三人称实现 IClientItemExtensions#getArmPose

创建 ItemUseAnimation

首先,让我们创建一个新的 ItemUseAnimation。这可以通过 可扩展枚举 系统实现:

{
"entries": [
{
"enum": "net/minecraft/world/item/ItemUseAnimation",
"name": "EXAMPLEMOD_ITEM_USE_ANIMATION",
"constructor": "(ILjava/lang/String;)V",
"parameters": [
// id,应该始终为 -1
-1,
// 名称,应该是唯一的标识符
"examplemod:item_use_animation"
]
}
]
}

然后我们可以通过 valueOf 获取这个枚举常量:

public static final ItemUseAnimation EXAMPLE_ANIMATION = ItemUseAnimation.valueOf("EXAMPLEMOD_ITEM_USE_ANIMATION");

从这里开始,我们就可以开始应用变换(transform)。为此,你需要创建一个新的 IClientItemExtensions,实现你需要的方法,并通过 mod 事件总线 上的 RegisterClientExtensionsEvent 进行注册:

public class ConsumableClientItemExtensions implements IClientItemExtensions {
// 在这里实现方法
}

// 在某个事件监听器类中
@SubscribeEvent
public static void registerClientExtensions(RegisterClientExtensionsEvent event) {
event.registerItem(
// 物品扩展的实例
new ConsumableClientItemExtensions(),
// 使用该扩展的一系列物品
CONSUMABLE
)
}

第一人称(First Person)

所有可消耗物品都拥有的第一人称变换,是通过 IClientItemExtensions#applyForgeHandTransform 实现的:

public class ConsumableClientItemExtensions implements IClientItemExtensions {

// ...

@Override
public boolean applyForgeHandTransform(
PoseStack poseStack, LocalPlayer player, HumanoidArm arm, ItemStack itemInHand,
float partialTick, float equipProcess, float swingProcess
) {
// 首先需要检查物品是否正在被使用,并且拥有我们的动画
HumanoidArm usingArm = entity.getUsedItemHand() == InteractionHand.MAIN_HAND
? entity.getMainArm()
: entity.getMainArm().getOpposite();
if (
entity.isUsingItem() && entity.getUseItemRemainingTicks() > 0
&& usingArm == arm && itemInHand.getUseAnimation() == EXAMPLE_ANIMATION
) {
// 对 pose stack 应用变换(平移、缩放、旋转等)
// ...
return true;
}

// 什么也不做
return false;
}
}

第三人称(Third Person)

除了 EATDRINK 之外,其他所有物品的第三人称变换都需要特殊逻辑,这通过 IClientItemExtensions#getArmPose 实现;其中 HumanoidModel.ArmPose 也可以扩展以实现自定义变换。

由于 ArmPose 构造函数需要一个 lambda,因此必须使用一个 EnumProxy 引用:

{
"entries": [
{
"name": "EXAMPLEMOD_ITEM_USE_ANIMATION",
// ...
},
{
"enum": "net/minecraft/client/model/HumanoidModel$ArmPose",
"name": "EXAMPLEMOD_ARM_POSE",
"constructor": "(ZLnet/neoforged/neoforge/client/IArmPoseTransformer;)V",
"parameters": {
// 指向代理类所在的 class
// 由于这是客户端专用类,建议将其分离
"class": "example/examplemod/client/MyClientEnumParams",
// enum 代理的字段名
"field": "CUSTOM_ARM_POSE"
}
}
]
}
// 创建枚举参数
public class MyClientEnumParams {
public static final EnumProxy<HumanoidModel.ArmPose> CUSTOM_ARM_POSE = new EnumProxy<>(
HumanoidModel.ArmPose.class,
// 该姿势是否使用双臂
false,
// 姿势变换器
(IArmPoseTransformer) MyClientEnumParams::applyCustomModelPose
);

private static void applyCustomModelPose(
HumanoidModel<?> model, HumanoidRenderState state, HumanoidArm arm
) {
// 在这里应用模型变换
// ...
}
}

// 在某个仅限客户端的类中
public static final HumanoidModel.ArmPose EXAMPLE_POSE = HumanoidModel.ArmPose.valueOf("EXAMPLEMOD_ARM_POSE");

然后,可以通过 IClientItemExtensions#getArmPose 方法来设置手臂姿势(arm pose):

public class ConsumableClientItemExtensions implements IClientItemExtensions {

// ...

@Override
public HumanoidModel.ArmPose getArmPose(
LivingEntity entity, InteractionHand hand, ItemStack stack
) {
// 首先需要检查物品是否正在被使用,并且具有我们的动画
if (
entity.isUsingItem() && entity.getUseItemRemainingTicks() > 0
&& entity.getUsedItemHand() == hand
&& itemInHand.getUseAnimation() == EXAMPLE_ANIMATION
) {
// 返回要应用的姿势
return EXAMPLE_POSE;
}

// 否则返回 null
return null;
}
}

重写实体音效(Overriding Sounds on Entity)

有时,某个实体(entity)在消耗物品时,可能需要播放不同的音效。此时,可以让 LivingEntity 实例实现 Consumable.OverrideConsumeSound 接口,并在 getConsumeSound 方法中返回你希望实体播放的 SoundEvent

public class MyEntity extends LivingEntity implements Consumable.OverrideConsumeSound {

// ...

@Override
public SoundEvent getConsumeSound(ItemStack stack) {
// 返回要播放的音效
}
}

ConsumableListener

虽然消耗品(consumables)和消耗后应用的效果很有用,但有时我们希望效果的某些属性能作为其它 [数据组件][datacomponents] 对外提供。例如,猫和狼同样会吃 食物 并查询其营养值,或者带有药水内容的物品会查询其颜色用于渲染。在这些场景下,数据组件可以实现 ConsumableListener 接口,以提供消耗逻辑。

ConsumableListener 只包含一个方法:#onConsume,该方法接受当前的世界(level)、正在消耗物品的实体(entity)、被消耗的物品(item),以及该物品上的 Consumable 实例。当物品被完全消耗时,会在 Item#finishUsingItem 阶段调用 onConsume

要添加你自己的 ConsumableListener,只需 注册一个新的数据组件 并实现 ConsumableListener 接口即可。

```java
public record MyConsumableListener() implements ConsumableListener {

@Override
public void onConsume(
Level level, LivingEntity entity, ItemStack stack, Consumable consumable
) {
// 在这里实现你的逻辑
}
}

食物(Food)

食物(Food)是 ConsumableListener 的一种类型,属于饥饿系统的一部分。所有与食物物品相关的功能都已经在 Item 类中实现好了,因此只需要将 FoodProperties(食物属性)添加到 DataComponents#FOOD,并配合一个 consumable(可消耗对象)即可。如果没有特别指定 consumable,可以使用 Consumables#DEFAULT_FOOD。这里有一个名为 food 的辅助方法(helper method),它接收 FoodPropertiesConsumable 对象作为参数。

FoodProperties(食物属性)可以通过直接调用 record 构造器创建,或者通过 new FoodProperties.Builder() 构建,最后调用 build 方法完成:

  • nutrition - 设置恢复多少饥饿值。单位为半格饥饿值,例如 Minecraft 的牛排可以恢复 8 格饥饿值。
  • saturationModifier - 饱和度修饰符(saturation modifier),用于计算饱和度值,即吃下该食物后恢复的饱和度。计算公式为 min(2 * nutrition * saturationModifier, playerNutrition),也就是说如果设置为 0.5,则实际恢复的饱和度值与营养值相同。
  • alwaysEdible - 是否总是可以食用该物品,即使饥饿条已满。默认值为 false,对于金苹果等除了填充饥饿条还有额外效果的物品应设置为 true
// 假设存在一个 DeferredRegister.Items ITEMS
public static final DeferredItem<Item> FOOD = ITEMS.registerSimpleItem(
"food",
new Item.Properties().food(
new FoodProperties.Builder()
// 恢复 1.5 格生命值
.nutrition(3)
// 胡萝卜为 0.3
// 生鳕鱼为 0.1
// 熟鸡肉为 0.6
// 熟牛排为 0.8
// 金苹果为 1.2
.saturationModifier(0.3f)
// 设置后,即使饥饿条已满也可以食用
.alwaysEdible()
)
);

如果想查看示例,或者了解 Minecraft 中各种食物的具体属性值,可以参考 Foods 类。

要获取某个物品的 FoodProperties,可以调用 ItemStack.get(DataComponents.FOOD)。注意,这个方法可能返回 null,因为并不是所有物品都可食用。要判断某个物品是否可食用,只需对 getFoodProperties 的返回值进行 null 检查即可。

药水内容(Potion Contents)

药水 的内容通过 PotionContents 实现,它也是一种 ConsumableListener,其效果会在消耗时生效。它包含一个可选的药水(potion)效果、一个可选的药水颜色(tint),一组自定义的 MobEffectInstance(生物效果实例),以及一个可选的翻译键(translation key)用于获取物品堆名称。如果不是 PotionItem 的子类,模组开发者需要重写 Item#getName 方法。