Skip to main content

1.7 可穿戴装备

Part A. 实战

和近战武器/工具类似,可穿戴装备也有 “工具等级” 来管理和装备位置无关的数据,这就是 IArmorMaterial 接口类,我们模仿 1.6 中的 ModItemTier 用一个枚举类来自定义:

public enum ObsidianArmorMaterial implements IArmorMaterial {
OBSIDIAN(
"obsidian", 40, new int[]{5,8,10,5}, 20, SoundEvents.ITEM_ARMOR_EQUIP_DIAMOND, 2.0F, 0.0F, () -> {
return Ingredient.fromItems(ItemRegistry.obsidianIngot.get());
}
);
// 下面是 IArmorMaterial 需要拥有的属性:
// 最大耐久数值数组
private static final int[] MAX_DAMAGE_ARRAY = new int[]{13,15,16,11};
// 这个名字是给渲染端用的(也就是客户端),也是找材质用的名字。请确保此处、注册名、材质名三者写的是一个名字
private final String name;
private final int maxDamageFactor; // 最大耐久衰减因数
private final int[] damageReductionAmountArray; // 减伤数组(保护效果)
private final int enchantability; // 附魔能力
private final SoundEvent soundEvent; // 穿着音效
private final float toughness; // 文档没说,可以自己尝试
private final float knockbackResistance; // 击退抵抗数值
// 这个 LazyValue 可以理解为 Forge 包里面内含的一种容器,实现懒加载。泛型参数是懒加载的结果,而它初始化参数是对应泛型的 Supplier
// 这个属性相当于 ItemTier 中的 getRepairMaterial()
private final LazyValue<Ingredient> repairMaterial;

// 构造函数还是就把之前的属性给它们赋值一遍
ObsidianArmorMaterial(
String armorName, int maxDamageF, int[]damageReductionArray,
int enchantAbility, SoundEvent se, float tough, float knockResist,
Supplier<Ingredient> repairMaterialSupplier
) {
this.name = armorName; this.maxDamageFactor = maxDamageF;
this.damageReductionAmountArray = damageReductionArray;
this.enchantability = enchantAbility;
this.soundEvent = se; this.toughness = tough;
this.knockbackResistance = knockResist;
this.repairMaterial = new LazyValue<>(repairMaterialSupplier);
}
// 下面实现 IArmorMaterial 接口,只要给出所有的 get 方法就行。注意名字和参数。
// 注意,getName 因为是为客户端准备的函数,所以加上 annotation @OnlyIn 就行

public int getDurability(EquipmentSlotType slotIn) {
return MAX_DAMAGE_ARRAY[slotIn.getIndex()] * this.maxDamageFactor;
}
public int getDamageReductionAmount(EquipmentSlotType slotIn) {
return this.damageReductionAmountArray[slotIn.getIndex()];
}
public int getEnchantability() {
return this.enchantability;
}
public SoundEvent getSoundEvent() {
return this.soundEvent;
}
public Ingredient getRepairMaterial() {
return this.repairMaterial.getValue();
}
@OnlyIn(Dist.CLIENT)
public String getName() {
return this.name;
}
public float getToughness() {
return this.toughness;
}
public float getKnockbackResistance() {
return this.knockbackResistance;
}

}

接着,和其他 item 一样设置可穿戴装备对应的对象,并且注册。

但是!一般情况我们无需继承不同的类,只需要创建 ArmorItem 类的不同实例,就能获得不同部位的 item 对象。这多半是因为可穿戴装备是盔甲,都设计成一套,例如注册时:

// 回忆一下,item_reg 是之前在 ItemRegistry 类中定义的 DeferredRegister 类的实例
public static final RegistryObject<Item> obsidianHelmet = item_reg.register("obsidian_helmet", () -> new ArmorItem(ModArmorMaterial.OBSIDIAN, EquipmentSlotType.HEAD, (new Item.Properties()).group(ModGroup.itemGroup)));

public static final RegistryObject<Item> obsidianChestplate = item_reg.register("obsidian_chestplate", () -> new ArmorItem(ModArmorMaterial.OBSIDIAN, EquipmentSlotType.CHEST, (new Item.Properties()).group(ModGroup.itemGroup)));

public static final RegistryObject<Item> obsidianLeggings = item_reg.register("obsidian_leggings", () -> new ArmorItem(ModArmorMaterial.OBSIDIAN, EquipmentSlotType.LEGS, (new Item.Properties()).group(ModGroup.itemGroup)));

public static final RegistryObject<Item> obsidianBoots = item_reg.register("obsidian_boots", () -> new ArmorItem(ModArmorMaterial.OBSIDIAN, EquipmentSlotType.FEET, (new Item.Properties()).group(ModGroup.itemGroup)));

现在传给 DeferredRegister<Item>::register() 的第二参数不再是类自带的 new 方法,而是临时写的匿名函数,利用已存在的类 ArmorItem 的构造方法:

// 第一参数是之前定义的 可穿戴装备的“等级枚举类”
// 第二参数是盔甲的部位,会针对性读取材质的某一区域
// 第三参数是 Item 惯用构造属性
public ArmorItem(IArmorMaterial material, EquipmentSlotType type, Item.PropertyItem.Properties builtIn);

现在最后一件事需要注意,就是盔甲的材质添加和其他 item 不一样

首先从前面共用一个类可以看出,这里盔甲的材质也在一个图中。说 “一个图” 不是很准确,准确来说是穿着状态的盔甲全套一组图,物品栏中的盔甲又是另一组图。所以这里需要两组材质图,分布在不同 “layer” 中。因此,如果想自己创作,还需要专门的软件,例如上面提到的 blockbench;

其次,minecraft 自己把盔甲穿着的材质写死到 minecraft:/ 资源作用域下,这意味着,我们不能在原来的地方加材质图,应该新建一个 minecraft 目录;而同时盔甲在物品栏的贴图也要自己设计,和普通 item 一样加 model json 和 扁平材质:

resources/
├── META-INF/
│   └── mods.toml
├── assets/
│   ├── mymod/ # 你的 modid,之前材质存放的地方(作用域为 mymod:/)
│   | ├── models/
│   | │   └── item/ # 盔甲物品栏图标模型 json 存放位置
│   | └── textures/
│   | └── item/ # 盔甲物品栏材质图片存放位置
| |
| └── minecraft/ # 新建的目录,材质作用域是 minecraft:/
| └── textures/
| └── models/
| └── armor/ # 盔甲穿着材质图片存放位置
| └── ...
|
└── pack.mcmeta

注意,盔甲材质图命名格式:<ArmorMaterialName>_layer_<N>(盔甲贴图可能由多层组成,取决于模型导出的情况);

你可以使用诸如 MCreator、BlockBench 这类的编辑工具来绘制盔甲模型和材质。

Part B. 理论:IArmorMaterial & ArmorItem

首先观察 IArmorMaterial 接口,该接口指定了装备套装的耐久、破坏减少值、穿着音效、装备硬度(韧性)、名称(仅客户端)、附魔等级、击退抵抗、修补材料(同 IItemTier):

对于装备硬度(装备韧性),它也属于装备保护的属性之一,官方解释如下:

总结:

  • **护甲值(Armor)**是决定减伤比例区间的属性,每 1 点护甲值使伤害减少 4%、至多被伤害降低至下限 0.8%。单次伤害中的每 0.5 伤害值使护甲值提供的伤害减少比例减少 1%,直到下限。该值上限为 30。

    注:10 颗心 20 点生命值;

  • **盔甲韧性(Armor toughness)**是决定降低护甲值减伤比例需要的伤害的属性,通俗来说,因为盔甲的减伤比例会随着伤害值的增加而被削弱(例如钻石盔甲可以对 3 点伤害减伤 77%,但是对 13 点伤害只能减伤 67%),而盔甲韧性就是能够减缓这个被削弱效果。每 1 点盔甲韧性使减少 1% 减少比例需要的伤害增加 0.0625。该值上限为 20。

注意这几个点:

  • 由于装备是成套的,而且各个装备的原始耐久度各不相同,因此我们采用数组的方式存放原始耐久值

    // 注意到用户装备槽可以通过 EquipmentSlotType.getIndex() 来了解指定位置的信息
    public int getDurability(EquipmentSlotType slotIn) {
    return MAX_DAMAGE_ARRAY[slotIn.getIndex()] * this.maxDamageFactor;
    }

    以及减伤数组:

    public int getDamageReductionAmount(EquipmentSlotType slotIn) {
    return this.damageReductionAmountArray[slotIn.getIndex()];
    }

    注:EquipmentSlotType 就是描述装备槽位置的枚举类型,有内置常量:EquipmentSlotType.HEAD/LEGS 等等,也可以使用 EquipmentSlotType.getIndex() 来获取底层的索引值,上面将索引值用作索引耐久值 / 减伤数组;

  • 实现 IArmorMaterialgetRepairMaterial() 在私有成员中存放的是 LazyValue 类型以 Supplier 进行构造,仅在读属性时才创建对象:

    // definition
    this.repairMaterial = new LazyValue<>(repairMaterialSupplier);

    // usage
    public Ingredient getRepairMaterial() {
    return this.repairMaterial.getValue();
    }

    如果希望定义更多的修复方式,详细信息请参见:Chapter 2-EX 铁砧修补配方

  • 这里声音事件我们直接使用内置的常量,例如 SoundEvents.ITEM_ARMOR_EQUIP_DIAMOND,以后介绍声音事件自定义时再详细叙述;

ArmorItem 相当于是包装好的 IArmorMaterial,我们只需传入事先定义的 IArmorMaterial 的 Tier,并且告诉当前实例的装备位置,以及最后一个参数 Item.Properties 定义 Item 的通用属性,我们就无需定义一个新的类来声明一套状态。


总结一下,其实 装备的 tier 和近战武器/工具的 tier 都继承了 TieredItem,不过各自在这个基础上进行包装,与描述各自 tier 的实例(我们一般用枚举类型的实例)组合了起来:

Item --> TieredItem (组合了 IItemTier 接口实例) --> SwordItem
| └──--> ToolItem -> {PickaxeItem, ShovelItem, AxeItem, HoeItem}
└─-> ArmorItem(组合了 IArmorMaterial 接口实例)