消耗品
消耗品是可以持续使用一段时间、并在使用过程中被“消耗”的物品。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。