消耗品

消耗品

消耗品是可以持续使用一段时间、并在使用过程中被“消耗”的物品。Minecraft 中所有可以吃或喝的物品都属于某种消耗品。

Consumable 数据组件

所有可消耗物品都具有 DataComponents#CONSUMABLE 组件。其底层记录类 Consumable 定义物品的消耗方式,以及消耗完成后应用的效果。

可直接调用记录类构造函数创建 Consumable,也可使用 Consumable#builder,让各字段先采用默认值,配置完成后再调用 build:

  • consumeSeconds:完全消耗物品所需的秒数,类型为 float。经过此时间后会调用 Item#finishUsingItem。默认为 1.6 秒,即 32 刻。
  • animation:使用物品时播放的 ItemUseAnimation,默认为 ItemUseAnimation#EAT。
  • sound:消耗物品时播放的 SoundEvent,必须是 Holder 实例,默认为 SoundEvents#GENERIC_EAT。
    • 如果原版实例不是 Holder<SoundEvent>,可调用 BuiltInRegistries.SOUND_EVENT.wrapAsHolder(soundEvent) 获取包装后的版本。
  • soundAfterConsume:物品消耗完成后播放的 SoundEvent,内部委托给 PlaySoundConsumeEffect。
  • hasConsumeParticles:为 true 时,每四刻以及物品完全消耗时生成物品粒子,默认为 true。
  • onConsume:添加 ConsumeEffect,在 Item#finishUsingItem 完成物品消耗后应用。

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

通过 Item.Properties#component 添加 Consumable 组件:

// Assume there is some DeferredRegister.Items ITEMS
public static final DeferredItem<Item> CONSUMABLE = ITEMS.registerSimpleItem(
    "consumable",
    props -> props.component(
        DataComponents.CONSUMABLE,
        Consumable.builder()
            // Spend 2 seconds, or 40 ticks, to consume
            .consumeSeconds(2f)
            // Sets the animation to play while consuming
            .animation(ItemUseAnimation.BLOCK)
            // Play sound while consuming every tick
            .sound(SoundEvents.ARMOR_EQUIP_CHAIN)
            // Play sound once finished consuming
            .soundAfterConsume(SoundEvents.BREEZE_WIND_CHARGE_BURST)
            // Don't show particles while eating
            .hasConsumeParticles(false)
            .onConsume(
                // When finished consuming, applies the effects with a 30% chance
                new ApplyStatusEffectsConsumeEffect(new MobEffectInstance(MobEffects.HUNGER, 600, 0), 0.3F)
            )
            // Can have multiple
            .onConsume(
                // Teleports the entity randomly in a 50 block radius
                new TeleportRandomlyConsumeEffect(100f)
            )
            .build()
    )
);

ConsumeEffect

消耗品使用完成后,可能需要执行添加药水效果等逻辑。这由 ConsumeEffect 处理,通过 Consumable.Builder#onConsume 将其添加到 Consumable。

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

每个 ConsumeEffect 有两个方法:getType 指定注册表对象 ConsumeEffect.Type;apply 在物品完全消耗后调用。apply 接收消耗者所在的 Level、正在消耗的 ItemStack 和执行消耗的 LivingEntity 三个参数。效果成功应用时返回 true,失败则返回 false。

要创建 ConsumeEffect,实现该接口,并将带有对应 MapCodec 和 StreamCodec 的 ConsumeEffect.Type 注册到 BuiltInRegistries#CONSUME_EFFECT_TYPE:

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());

            // Can successfully use portal
            return true;
        }

        // Cannot use portal
        return false;
    }

    @Override
    public ConsumeEffect.Type<? extends ConsumeEffect> getType() {
        // Set to registered object
        return USE_PORTAL.get();
    }

    @Override
    @Nullable
    public TeleportTransition getPortalDestination(ServerLevel level, Entity entity, BlockPos pos) {
        // Set teleport location
    }
}

// In some registrar class
// Assume there is some 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)
    ));

// For some Item.Properties that is adding a CONSUMABLE component
Consumable.builder()
    .onConsume(
        new UsePortalConsumeEffect(Level.END)
    )
    .build();

ItemUseAnimation

ItemUseAnimation 本质上是一个只定义 ID 和名称的枚举。它的实际用途硬编码在第一人称的 ItemHandRenderer#renderArmWithItem 和第三人称的 AvatarRenderer#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": [
                // The id, should always be -1
                -1,
                // The name, should be a unique identifier
                "examplemod:item_use_animation"
            ]
        }
    ]
}

然后通过 valueOf 获取枚举常量:

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

之后便可以开始应用变换。创建新的 IClientItemExtensions,实现所需方法,并通过 Mod 事件总线上的 RegisterClientExtensionsEvent 注册:

public class ConsumableClientItemExtensions implements IClientItemExtensions {
    // Implement methods here
}

// In some event handler class
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerClientExtensions(RegisterClientExtensionsEvent event) {
    event.registerItem(
        // The instance of the item extensions
        new ConsumableClientItemExtensions(),
        // A vararg of items that use this
        CONSUMABLE
    )
}

第一人称

所有消耗品都有的第一人称变换,通过 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
    ) {
        // We first need to check if the item is being used and has our animation
        HumanoidArm usingArm = entity.getUsedItemHand() == InteractionHand.MAIN_HAND
            ? entity.getMainArm()
            : entity.getMainArm().getOpposite();
        if (
            entity.isUsingItem() && entity.getUseItemRemainingTicks() > 0
            && usingArm == arm && itemInHand.getUseAnimation() == EXAMPLE_ANIMATION
        ) {
            // Apply transformations to pose stack (translate, scale, mulPose)
            // ...
            return true;
        }

        // Do nothing
        return false;
    }
}

第三人称

除 EAT 和 DRINK 外,各种动画都有特殊的第三人称变换逻辑。这通过 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": {
                // Point to class where the proxy is located
                // Should be separate as this is a client only class
                "class": "example/examplemod/client/MyClientEnumParams",
                // The field name of the enum proxy
                "field": "CUSTOM_ARM_POSE"
            }
        }
    ]
}
// Create the enum parameters
public class MyClientEnumParams {
    public static final EnumProxy<HumanoidModel.ArmPose> CUSTOM_ARM_POSE = new EnumProxy<>(
        HumanoidModel.ArmPose.class,
        // Whether the pose uses both arms
        false,
        // Whether the offhand location should be affected by the model pose
        false,
        // The pose transformer
        (IArmPoseTransformer) MyClientEnumParams::applyCustomModelPose
    );

    private static void applyCustomModelPose(
        HumanoidModel<?> model, HumanoidRenderState state, HumanoidArm arm
    ) {
        // Apply model transforms here
        // ...
    }
}

// In some client only class
public static final HumanoidModel.ArmPose EXAMPLE_POSE = HumanoidModel.ArmPose.valueOf("EXAMPLEMOD_ARM_POSE");

然后通过 IClientItemExtensions#getArmPose 设置手臂姿势:

public class ConsumableClientItemExtensions implements IClientItemExtensions {

    // ...

    @Override
    public HumanoidModel.ArmPose getArmPose(
        LivingEntity entity, InteractionHand hand, ItemStack stack
    ) {
        // We first need to check if the item is being used and has our animation
        if (
            entity.isUsingItem() && entity.getUseItemRemainingTicks() > 0
            && entity.getUsedItemHand() == hand
            && itemInHand.getUseAnimation() == EXAMPLE_ANIMATION
        ) {
            // Return pose to apply
            return EXAMPLE_POSE;
        }

        // Otherwise return null
        return null;
    }
}

在实体上覆盖音效

有时,实体希望在消耗物品时播放不同的声音。此时可让 LivingEntity 实现 Consumable.OverrideConsumeSound,并由 getConsumeSound 返回希望播放的 SoundEvent。

public class MyEntity extends LivingEntity implements Consumable.OverrideConsumeSound {
    
    // ...

    @Override
    public SoundEvent getConsumeSound(ItemStack stack) {
        // Return the sound to play
    }
}

ConsumableListener

消耗品及其完成后应用的效果很实用,但有时还需要通过其他数据组件对外提供效果属性。例如,猫和狼也会吃食物,需要查询营养值;带药水内容的物品则需要查询颜色以便渲染。此时,数据组件会实现 ConsumableListener,提供消耗逻辑。

ConsumableListener 只有 #onConsume 一个方法,接收当前世界、消耗物品的实体、被消耗物品,以及物品上的 Consumable 实例。物品完全消耗时,Item#finishUsingItem 会调用 onConsume。

添加自己的 ConsumableListener,只需注册新的数据组件并实现 ConsumableListener。

public record MyConsumableListener() implements ConsumableListener {

    @Override
    public void onConsume(
        Level level, LivingEntity entity, ItemStack stack, Consumable consumable
    ) {
        // Do things here
    }
}

食物

食物是饥饿系统中的一种 ConsumableListener。食物物品的全部功能已由 Item 类处理,因此只需将 FoodProperties 添加到 DataComponents#FOOD,并搭配消耗品组件。辅助方法 food 接收 FoodProperties 和 Consumable;未指定后者时使用 Consumables#DEFAULT_FOOD。

可直接调用记录类构造函数创建 FoodProperties,也可使用 new FoodProperties.Builder(),最后调用 build:

  • nutrition:恢复的饥饿点数,以半个饥饿图标为单位。例如,Minecraft 的牛排恢复 8 点。
  • saturationModifier:计算进食后恢复的饱和度时使用的系数。计算式为 min(2 * nutrition * saturationModifier, playerNutrition),因此使用 0.5 时,有效饱和度值与营养值相同。
  • alwaysEdible:即使饥饿条已满,是否仍可食用。默认为 false;金苹果等除了填饱肚子还提供额外效果的物品使用 true。
// Assume there is some DeferredRegister.Items ITEMS
public static final DeferredItem<Item> FOOD = ITEMS.registerSimpleItem(
    "food",
    props -> props.food(
        new FoodProperties.Builder()
            // Heals 1.5 hearts
            .nutrition(3)
            // Carrot is 0.3
            // Raw Cod is 0.1
            // Cooked Chicken is 0.6
            // Cooked Beef is 0.8
            // Golden Aple is 1.2
            .saturationModifier(0.3f)
            // When set, the food can alway be eaten even with
            //  a full hunger bar.
            .alwaysEdible()
    )
);

要查看示例或 Minecraft 中的具体取值,可参考 Foods 类。

调用 ItemStack.get(DataComponents.FOOD) 获取物品的 FoodProperties。由于不是所有物品都可食用,结果可能为 null。判断是否可食用时,需要对结果进行空值检查(原文在此另提及 getFoodProperties 调用)。

药水内容

通过 PotionContents 表示的药水内容也是一种 ConsumableListener,会在消耗时应用效果。它包含可选的药水、可选的药水颜色、自定义 MobEffectInstance 列表(与药水效果一起应用),以及获取物品堆叠名称时使用的可选翻译键。如果物品不是 PotionItem 子类,Mod 开发者需要重写 Item#getName。


来源: NeoForged/Documentation(26.1)。中文翻译依据提交 4a680cdd4d15。