Skip to main content

1.6 近战武器 和 工具

Part A. 实战

MC 原版 1.16.5 的近战武器可以认为只有剑,如果想要自定义近战物品,就可以按剑的类的做法来做(入门不介绍攻击动画的制作,就复用剑的攻击动画)。

MC 原版 1.16.5 的工具就是耳熟能详几件:稿子、铲子、斧头、锄头

首先,细心的同学可以发现,木头、石头、铁、黄金和钻石种类的武器面板基础数据(除去附魔等加成的效率、耐久、伤害……)是不同的,并且损坏后修复的材料不同。但它们都是 “剑 / 稿 / 斧 / 锄 / 铲” 这个属性,所以这些额外的数据可以抽象为一个单独的类 / 枚举类型来管理,在初始化时再传给 “剑 / 稿 / 斧 / 锄 / 铲” 这个类。在 MC 中,它就是接口类型 IItemTier,即 “工具等级”(Tiered adj. 阶梯式的、分层的)。

官方选择了接口类来设计 IItemTier,而由于 Java 的枚举类型在能够大量定义常量的同时,允许添加方法,因此我们只需写一个枚举类来 implements 它。举个例子:

public enum ModItemTier implements IItemTier {
// 规范来说,enum 内部先定义实例
OBSIDIAN(3, 2000, 10.0F, 4.0F, 30);

private final int harvestLevel; // 精准采集等级
private final int maxUses; // 最大耐久值
private final float efficiency; // 效率值
private final float attackDamage; // 伤害
private final int enchantability; // 附魔能力

// 构造方法就是将属性值全部传进去
ModItemTier(int harvestIn, int maxUsesIn, float effIn, float attIn, int enchantabilityIn) {
this.harvestLevel = harvestIn;
this.maxUses = maxUsesIn;
this.efficiency = effIn;
this.attackDamage = attIn;
this.enchantability = enchantabilityIn;
}

// 下面的部分别看它长,就是把 get 方法简单覆盖一下 -------------------|
@Override
public int getHarvestLevel() { return this.harvestLevel; }
@Override
public int getMaxUses() { return this.maxUses; }
@Override
public float getEfficiency() { return this.efficiency; }
@Override
public float getAttackDamage() { return this.attackDamage; }
@Override
public int getEnchantability() { return this.enchantability; }
// ------------------------------------------------------------|

// 之前提到的,修复材料需要单独写一下
// 但具体的合成表还需要以后写
@Override
public Ingredient getRepairMaterial() {
return Ingredient.fromItems(ItemRegistry.obsidianIngot.get());
}
}

再来看 “剑 / 稿 / 斧 / 锄 / 铲” 类的继承关系:

Item --> TieredItem (有工具等级的 item,组合了 IItemTier 接口实例) --> SwordItem
|
└─--> ToolItem // 这里是下一节要说的 “工具”,也有工具等级
├─----------> PickaxeItem
├─----------> ShovelItem
├─----------> AxeItem
└─----------> HoeItem

这里简单到爆炸,只需要掌握这些类的初始化方法就行,以剑为例:

public SwordItem(ItemTier tierObj, int attackDamage, float attackSpeed, Properties prop);

像这里以黑曜石剑为例:

public class ObsidianSword extends SwordItem {
public ObsidianSword() {
// 这里的 OBSIDIAN 是之前定义 enum ModItemTier 时创建的实例
// ItemGroup.COMBAT 是战斗物品组,对应创造模式物品栏“铁剑”图标一栏
super(ModIterTier.OBSIDIAN, 3, -2.4F, new Properties().group(ItemGroup.COMBAT));
}
}

SwordItem 构造函数的第一参数 ItemTier 后面的两个参数分别是攻击伤害、攻击速度

注:准确来说是属性修饰。原本 Item 默认的 “攻击速度” 是 5.0f。观察源码发现修饰方法是 AttributeModifier.Operation.ADDITION,也就是说相加修饰,因此这里 -2.4f 表示比正常 Item 的攻速慢 2.4 个单位;

攻击伤害也是如此,原本 Item 默认是 5;

之后的模型、材质、物品注册也是完全和普通 item 相同。

镐子、铲子、斧头、锄头也一样。

注意:以上数据面板理论上只要不溢出都可以自行设置。但如果想要写成好的 Mod,这边建议自己试试平衡性,再调整数值。

你问为什么不介绍 “弓” / “弩” 这类远程武器?因为现在的知识还不足我们作出动态 item 模型;

Part B. 理论:TieredItem & ToolItem

1.6.B.1 IItemTier & TieredItem: Sword

注意到,IItemTier 是相当简明的接口:

实现这些接口的类就能称 Tier 类。而我们之前定义的 ObsidianTier 就是简单地实现、初始化以供使用外部类(例如 SwordItem)使用而已。

再看 SwordItem 类,我们的 ObsidianSword 就继承于这个类:

IVanishable 是空接口,也就是一种标记接口(与 Serializable 的道理一样),用于标识是否能附魔 “消失诅咒”;

观察到 SwordItem 的构造函数:

public SwordItem(IItemTier tier, int attackDamage, float attackSpeed, Item.Properties builder);

此外我们发现一些重要的逻辑:

  • SwordItem 类武器的伤害由所属的 IItemTier 本身的 attackDamage,附加上武器自身的伤害(构造函数参数)合并计算;
  • Item.PropertiesItem 公共类型的构造函数参数(就是在第一节,创建 Item 时传入的对象的类型);
  • 除了 attackDamage 外的属性都是由 attributeModifiers 这个 multimap 管理的。这个 attributeModifiers 只有该类武器在主手(main hand)时生效;这也是为什么 attackDamage 除了存在于 modifiers 中,还单独作为一个私有成员;

我们还可以再留意一些 SwordItem 中的方法(实现 / 重写了 Item 父类对应的方法),熟悉一下(一开始不需要掌握这么多!用到再查):

  • canPlayerBreakBlockWhileHolding
  • canHarvestBlock
  • onBlockDestroyed
  • getDestroySpeed
  • hitEntity

再观察另一件重要的事:TieredItem 表示被 IItemTier 描述的 Item 类型,除了需要传入 Item 属性以外,还需要传入、存放所属的 IItemTier,并且根据所属 tier 判断:

  • 装备附魔等级(IIterTier 属性 enchantability);

  • 装备是否可修复(IItemTier 的方法 getRepairMaterial 以及 Item 自身方法 getIsRepairable);

    具体实现在铁砧上自定义修补配方,请参见:Chapter 2-EX 铁砧修补配方

1.6.B.2 ToolItem: Pickaxe, Axe, Shovel, Hoe

SwordItem 直接继承于 TieredItem 不同,由于 pickaxe、axe、shovel、hoe 对方块 / 实体可能具有特殊的效果(例如:斧头削皮、锄头锄地、铲子铲草皮/铲雪等等),因此它们继承于 TieredItem 的子类 ToolItem(中间层),而 ToolItem 则是用于阐述这些工具的独特作用的。

这里可以看到 ToolItemTieredItem 构造函数多传入了一个 effectiveBlocks 的集合类型,表示该工具对哪些方块起特殊效果并且只有遇到起效果的方块,该工具的 destroySpeed 才会取决于 IItemTier 指定的效率

这里,我们看镐子 PickaxeItem 类型继承于 ToolItem

镐子挖掘效果会生效的方块就被以静态常量的方式写在这个类中(上面写了 ACTIVATOR_RAIL 激活铁轨,COAL_ORE 煤矿 等等)。

这里还有两个有趣的点:

  • PickaxeItem 计算是否可以获得方块掉落物时(canHarvestBlock),只有 BlockState.getHarvestTool() 指定的工具类型是 PICKAXE,才会按照 IItemTier 的收获等级计算是否能获得。否则无论这个镐子的收获等级多高,它都只能收获 ROCK(岩石)、IRON(金属)、AVAIL(铁砧);

    后面介绍方块的定义时,可以通过定义 BlockState.getHarvestTool() 来达到一些特殊的效果;

  • PickaxeItem 类型只有破坏 ROCK(岩石)、IRON(金属)、AVAIL(铁砧)3 类物品时才以自身的效率进行挖掘,否则取决于父类 ToolItem 的定义(回忆一下,ToolItemgetDestroySpeed 是判断了当前方块是否在 effectiveBlocks 内,如果不在就是固定的 1.0F 效率,否则是 IItemTier 定义的效率);

    这个效率值和 PickaxeItem 的私有成员 efficiency 是一致的……

可惜 PickaxeItem 并不能代表所有 ToolItem 的实现类,因为它还有一些方法没有实现(因为逻辑上不需要)。为了了解更丰富的实现,我们再看看斧头 AxeItem

这里我们比较感兴趣的有几点:

  • AxeItem 除了有对方块有更高效率的 EFFECTIVE_ON_BLOCK,还有 EFFECTIVE_ON_MATERIALS(作用于材料)和 BLOCK_STRIPPING_MAP(方块削皮前后的映射关系);
  • AxeItem 多实现了 Item 类的 onItemUse 方法,它是 AxeItem 被使用(不是破坏方块,这里的含义就是手持斧头右键削皮)时的回调函数。可以看到,虽然函数中有一些名称仍然是混淆的,但能看出大致的效果是定义了削皮时的音效(World.playSound)、更新方块的特殊状态(不是破坏而是更新),以及处理一些服务端和客户端的通知逻辑;
  • AxeItem 多实现了 getAxeStrippingState 方法,用于根据定义的静态常量获取被斧头削皮后的方块状态;

其实铲子、锄头实现方式如出一辙,它们多出的属性分别是 SHOVEL_LOOKUP(铲子铲草皮的方块映射)、HOE_LOOKUP(锄头耕地的方块映射);

注:查看源码可以知道,铲子能收集雪块是通过重写 canHarvestBlock 来实现的;