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
父类对应的方法),熟悉一下(一开始不需要掌握这么多!用到再查):
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
则是用于阐述这些工具的独特作用的。

这里可以看到 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
来实现的;