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.Properties是Item公共类型的构造函数参数(就是在第一节,创建Item时传入的对象的类型);- 除了
attackDamage外的属性都是由attributeModifiers这个 multimap 管理的。这个attributeModifiers只有该类武器在主手(main hand)时生效;这也是为什么attackDamage除了存在于 modifiers 中,还单独作为一个私有成员;
我们还可以再留意一些 SwordItem 中的方法(实现 / 重写了 Item 父类对应的方法),熟悉一下(一开始不需要掌握这么多!用到再查):
canPlayerBreakBlockWhileHoldingcanHarvestBlockonBlockDestroyedgetDestroySpeedhitEntity
再观察另一件重要的事: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 则是用于阐述这些工具的独特作用的。
这里可以看到 ToolItem 比 TieredItem 构造函数多传入了一个 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的定义(回忆一下,ToolItem的getDestroySpeed是判断了当前方块是否在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来实现的;