交互流程

交互流程

玩家左键、右键或中键点击对象时,会经过较为复杂的处理流程。本文说明这些流程,并解释各处应使用哪种返回结果及其原因。

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。