交互流程
玩家左键、右键或中键点击对象时,会经过较为复杂的处理流程。本文说明这些流程,并解释各处应使用哪种返回结果及其原因。
HitResult
Minecraft 使用 HitResult 判断玩家当前注视的对象。它类似于其他游戏引擎的射线检测结果,其中尤其重要的方法是 #getLocation。
命中结果有三种类型,由 HitResult.Type 枚举表示:BLOCK、ENTITY 和 MISS。BLOCK 类型可转换为 BlockHitResult,ENTITY 类型可转换为 EntityHitResult;两种子类都提供命中的方块或实体的额外信息。MISS 表示既未命中方块,也未命中实体,不能转换为上述任一子类。
在物理客户端上,Minecraft 类每帧更新当前注视的 HitResult,并将其保存到 hitResult 字段。可通过 Minecraft.getInstance().hitResult 访问。
左键使用物品
- 检查主手
ItemStack所需的全部功能标志是否启用。检查失败则结束流程。 Player#cannotAttackWithItem检查攻击延迟及DataComponents#MINIMUM_ATTACK_CHARGE。原文描述为:该方法返回false时结束流程。- 以鼠标左键和主手触发
InputEvent.InteractionKeyMappingTriggered。事件若被取消,结束流程。 - 根据
Minecraft中的HitResult,按所注视的对象执行不同逻辑:- 如果手持物品具有
DataComponents#PIERCING_WEAPON:- 仅服务端:调用
PiercingWeapon#attack。- 使用
ProjectileUtil#getHitEntitiesAlong获取满足以下条件的全部实体:- 位于攻击者的交互范围及物品攻击范围(
DataComponents#ATTACK_RANGE)内。 - 不处于无敌状态,并且可被弹射物命中。
- 不是同一载具上的乘客。
- 位于攻击者的交互范围及物品攻击范围(
- 对每个实体调用
LivingEntity#stabAttack。目标成功受到伤害、击退或被迫下车时返回true:- 通过
ItemStack#getDamageSource获取伤害来源。 - 调用
Entity#hurtServer。 - 如果刺击武器会造成击退,调用
LivingEntity#causeExtraKnockback。 - 如果武器命中会使目标下车,且目标正乘坐载具,调用
Entity#stopRiding。 - 如果目标是
LivingEntity,调用ItemStack#hurtEnemy。 - 如果成功造成伤害,应用
EnchantmentHelper#doPostAttackEffects。 - 如果目标成功受到伤害、击退或被迫下车:
- 以目标实体调用
LivingEntity#setLastHurtMob。 - 调用
LivingEntity#playAttackSound。
- 以目标实体调用
- 通过
- 使用
- 调用
LivingEntity#onAttack。 - 调用
LivingEntity#lungerForwardMaybe,通过EnchantmentHelper#doLungeEffects应用突进效果。 - 仅服务端:如果至少一个目标的
LivingEntity#stabAttack返回true,调用PiercingWeapon#makeHitSound。 - 仅服务端:调用
PiercingWeapon#makeSound。 - 调用
LivingEntity#swing。
- 仅服务端:调用
- 如果注视的是交互范围内的实体:
- 触发
AttackEntityEvent。若取消事件,结束流程。 - 调用
IItemExtension#onLeftClickEntity。若返回true,结束流程。 - 调用目标的
Entity#isAttackable。若返回false,结束流程。 - 调用目标的
Entity#skipAttackInteraction。若返回true,结束流程。 - 如果目标位于
minecraft:redirectable_projectile标签中(默认包括火球和风弹),并且是Projectile实例,则偏转目标并结束流程。 - 分别以两个浮点数计算实体基础伤害(
minecraft:attack_damage属性的值)和附魔额外伤害。两者均为0时结束流程。- 此处尚未计入主手物品的属性修饰符,它们在检查之后添加。
- 将主手物品的
minecraft:attack_damage属性修饰符加入基础伤害。 - 触发
CriticalHitEvent。若事件的#isCriticalHit返回true,将基础伤害乘以#getDamageMultiplier返回值。满足若干条件时默认倍率为1.5,否则为1.0;事件可修改它。 - 将附魔额外伤害加入基础伤害,得到最终伤害。
- 触发
SweepAttackEvent。若isSweeping返回true,玩家执行横扫攻击。默认检查攻击冷却是否超过 90%、不是暴击、玩家在地面上,且移动速度不超过minecraft:movement_speed属性值。 - 调用
Entity#hurtOrSimulate。若返回false,结束流程。 - 如果目标是
LivingEntity、攻击强度超过 90%、玩家正在疾跑,且经附魔修正后的minecraft:attack_knockback大于0,调用LivingEntity#knockback。- 该方法内会触发
LivingKnockBackEvent。取消事件则不应用击退。
- 该方法内会触发
- 根据
SweepAttackEvent#isSweeping,对附近的LivingEntity执行横扫攻击。- 如果实体位于玩家交互范围内,且
Entity#hurtServer返回true,此方法中会再次调用LivingEntity#knockback,从而再次触发LivingKnockBackEvent。
- 如果实体位于玩家交互范围内,且
- 调用
Item#hurtEnemy,可用于攻击后效果。例如,重锤会在适用情况下于此处将玩家弹回空中。 - 调用
Item#postHurtEnemy,在此扣除耐久。- 如果耐久归零,使堆叠变为
ItemStack#EMPTY,触发PlayerDestroyItemEvent。
- 如果耐久归零,使堆叠变为
- 触发
- 如果注视的是交互范围内的方块:
- 进入方块破坏子流程。
- 其他情况:
- 触发
PlayerInteractEvent.LeftClickEmpty。
- 触发
- 如果手持物品具有
右键使用物品
右键处理流程会调用一系列返回结果的方法,涉及下文介绍的两类结果类型。多数方法在返回明确成功或明确失败时会终止流程。为了便于阅读,下文将“明确成功或明确失败”统称为“确定结果”。
- 以鼠标右键和主手触发
InputEvent.InteractionKeyMappingTriggered。事件若被取消,结束流程。 - 检查若干条件,例如玩家不处于旁观模式,且主手
ItemStack所需的全部功能标志已启用。任一检查失败则结束流程。 - 根据
Minecraft中的HitResult,按注视对象执行不同逻辑:- 如果注视的是交互范围内、且不在世界边界外的实体:
- 触发
PlayerInteractEvent.EntityInteractSpecific。取消事件则结束流程。 - 调用被注视实体的
Entity#interactAt。返回确定结果则结束流程。- 为自己的实体添加行为时重写该方法;为原版实体添加行为时使用事件。
- 如果实体打开了界面(例如村民交易界面或运输矿车界面),结束流程。
- 触发
PlayerInteractEvent.EntityInteract。取消事件则结束流程。 - 调用被注视实体的
Entity#interact。返回确定结果则结束流程。- 为自己的实体添加行为时重写该方法;为原版实体添加行为时使用事件。
- 对于
Mob,重写后的Entity#interact会处理拴绳,以及主手ItemStack是刷怪蛋时生成幼体等逻辑,再将生物特有行为交给Mob#mobInteract。Entity#interact的返回结果规则也适用于此。
- 如果被注视实体是
LivingEntity,对主手ItemStack调用Item#interactLivingEntity。返回确定结果则结束流程。
- 触发
- 如果注视的是交互范围内、且不在世界边界外的方块:
- 触发
PlayerInteractEvent.RightClickBlock。取消事件则结束流程。也可以在事件中仅拒绝方块使用或物品使用。 - 调用
IItemExtension#onItemUseFirst。返回确定结果则结束流程。 - 如果
IItemExtension#doesSneakBypassUse返回false,且事件未拒绝使用方块,触发UseItemOnBlockEvent。事件取消时使用其取消结果;否则调用BlockBehaviour#useItemOn。返回确定结果则结束流程。 - 如果
InteractionResult是TryEmptyHandInteraction的实例(例如TRY_WITH_EMPTY_HAND),且当前执行的手是主手,调用BlockBehaviour#useWithoutItem。返回确定结果则结束流程。 - 如果事件未拒绝使用物品,调用
Item#useOn。返回确定结果则结束流程。
- 触发
- 其他情况:
- 触发
PlayerInteractEvent.RightClickEmpty。
- 触发
- 如果注视的是交互范围内、且不在世界边界外的实体:
- 触发
PlayerInteractEvent.RightClickItem。取消事件则结束流程。 - 调用
Item#use。- 如果
InteractionResult是Success实例(例如SUCCESS),将ItemStack替换为Success#heldItemTransformedTo。
- 如果
- 如果当前堆叠与原始堆叠不同,且新堆叠为空,触发
PlayerDestroyItemEvent。 - 上述流程再执行一次,这次以副手替代主手。
InteractionResult
InteractionResult 是一个密封接口,表示物品或空手与实体、方块等对象交互的结果。接口分为四个记录类,有六种默认状态。
首先是 InteractionResult.Success,表示操作成功并结束流程。成功状态包含两个参数:SwingSource 指定实体是否应在对应逻辑端挥手;InteractionResult.ItemContext 保存交互是否由手持物品引起,以及物品在使用后变成了什么。挥手来源由默认状态决定:InteractionResult#SUCCESS 表示客户端挥手,InteractionResult#SUCCESS_SERVER 表示服务端挥手,InteractionResult#CONSUME 表示不挥手。如果 ItemStack 发生变化,通过 Success#heldItemTransformedTo 设置物品上下文;如果手持物品未与目标发生交互,则使用 withoutItem。默认表示发生了物品交互,但没有物品转换。
// In some method that returns an interaction result
// Item in hand will turn into an apple
return InteractionResult.SUCCESS.heldItemTransformedTo(new ItemStack(Items.APPLE));
提示 通常不要在同一个方法中同时使用
SUCCESS与SUCCESS_SERVER。如果客户端已有足够信息判断何时挥手,应始终使用SUCCESS;如果判断依赖客户端没有的服务端信息,则使用SUCCESS_SERVER。
其次是 InteractionResult.Fail,由 InteractionResult#FAIL 实现,表示操作失败,不再允许后续交互,流程就此结束。它可以用在任何位置,但在 Item#useOn 和 Item#use 以外的地方应谨慎使用。许多情况下使用 InteractionResult#PASS 更合适。
最后是 InteractionResult.Pass 和 InteractionResult.TryWithEmptyHandInteraction,分别由 InteractionResult#PASS 和 InteractionResult#TRY_WITH_EMPTY_HAND 实现,表示操作既不算成功,也不算失败,应继续处理。除 BlockBehaviour#useItemOn 默认返回 TRY_WITH_EMPTY_HAND 外,其他返回 InteractionResult 的方法默认都返回 PASS。具体来说,如果 BlockBehaviour#useItemOn 返回的不是 TRY_WITH_EMPTY_HAND,无论物品是否位于主手,都不会调用 BlockBehaviour#useWithoutItem。
某些方法有特殊行为或要求,以下分别说明。
Item#useOn
如果希望操作视为成功,但不希望挥动手臂或增加 ITEM_USED 统计值,使用 InteractionResult#CONSUME 并调用 #withoutItem。
// In Item#useOn
return InteractionResult.CONSUME.withoutItem();
Item#use
只有这里会使用 Success 变体(SUCCESS、SUCCESS_SERVER、CONSUME)中转换后的 ItemStack。如果 Success#heldItemTransformedTo 设置的结果发生变化,它会替代发起使用时的 ItemStack。
Item#use 的默认实现中:如果物品可消耗(具有 DataComponents#CONSUMABLE),且玩家能够食用(处于饥饿状态,或物品始终可食用),返回 InteractionResult#CONSUME;若物品可消耗但玩家不能食用,则返回 InteractionResult#FAIL。如果物品可装备(具有 DataComponents#EQUIPPABLE),交换装备时返回 InteractionResult#SUCCESS,并通过 heldItemTransformedTo 将手持物品替换为换下的物品;若盔甲附魔具有 EnchantmentEffectComponents#PREVENT_ARMOR_CHANGE 组件,则返回 InteractionResult#FAIL。如果物品能格挡攻击(具有 DataComponents#BLOCKS_ATTACKS),先调用 Item#startUsingItem,再返回 InteractionResult#CONSUME。其他情况返回 InteractionResult#PASS。
在处理主手时返回 InteractionResult#FAIL 会阻止副手行为执行。如果希望继续处理副手(通常如此),改为返回 InteractionResult#PASS。
中键点击
- 如果
Minecraft.getInstance().hitResult中的HitResult为null或类型为MISS,结束流程。 - 原文描述为:以鼠标左键和主手触发
InputEvent.InteractionKeyMappingTriggered。事件若被取消,结束流程。 - 根据
Minecraft.getInstance().hitResult,按注视对象执行不同逻辑:- 如果注视的是交互范围内的实体:
- 如果
Entity#isPickable返回false,结束流程。 - 如果
Player#isWithinEntityInteractionRange返回false,结束流程。 - 调用
Entity#getPickResult。若快捷栏中已有与返回ItemStack匹配的槽位,则激活该槽位;否则,如果玩家处于创造模式,将结果堆叠加入玩家物品栏。- 原文说明默认实现会转交给
Entity#getPickResult,Mod 开发者可重写它。
- 原文说明默认实现会转交给
- 如果
- 如果注视的是交互范围内的方块:
- 如果
Player#isWithinBlockInteractionRange返回false,结束流程。 - 调用
IBlockExtension#getCloneItemStack(默认委托给BlockBehaviour#getCloneItemStack),将结果作为“选取的”ItemStack。- 默认返回
Block对应的Item。
- 默认返回
- 如果按住 Control、玩家处于创造模式,且目标方块具有
BlockEntity:- 通过
BlockEntity#saveCustomOnly获取方块实体数据。- 作为后处理步骤,调用
BlockEntity#removeComponentsFromTag。
- 作为后处理步骤,调用
- 通过
DataComponents#BLOCK_ENTITY_DATA,将方块实体数据加入“选取的”ItemStack。
- 通过
- 若快捷栏中已有与“选取的”
ItemStack匹配的槽位,则激活该槽位;否则,如果玩家处于创造模式,将选取的堆叠加入物品栏。
- 如果
- 如果注视的是交互范围内的实体:
来源: NeoForged/Documentation(26.1)。中文翻译依据提交
4a680cdd4d15。