物品属性模型
有些物品会根据堆叠数据改变状态,例如拉弓、鞘翅损坏、时钟处于特定维度等。物品模型可以跟踪某个属性,并根据条件选择模型。属性模型分为范围分派、选择和条件三种,分别类似浮点范围表达式、switch 分支和布尔判断。
范围分派模型
范围分派模型通过 RangeSelectItemModelProperty 取得浮点值,并据此切换模型。各条目具有阈值,选择不超过属性值、且最接近属性值的阈值对应模型。例如阈值为 3 和 5,属性值为 4 时选择 3 对应的模型,为 6 时选择 5 对应的模型。已有属性可在 RangeSelectItemModelProperties 中查找。
JSON
// For some item 'examplemod:example_item'
// JSON at 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:range_dispatch",
// The `RangeSelectItemModelProperty` to use
"property": "minecraft:count",
// A scalar to multiply to the computed property value
// If count was 0.3 and scale was 0.2, then the threshold checked would be 0.3*0.2=0.06
"scale": 1,
"fallback": {
// The fallback model to use if no threshold matches
// Can be any unbaked model type
"type": "minecraft:model",
// Points to 'assets/examplemod/models/item/example_item.json'
"model": "examplemod:item/example_item"
},
// Properties defined by `Count`
// When true, normalizes the count using its max stack size
"normalize": true,
// Entries with threshold information
"entries": [
{
// When the count is a third of its current max stack size
"threshold": 0.33,
"model": {
// Can be any unbaked model type
"type": "minecraft:model",
// Points to 'assets/examplemod/models/item/example_item_1.json'
"model": "examplemod:item/example_item_1"
}
},
{
// When the count is two thirds of its current max stack size
"threshold": 0.66,
"model": {
// Can be any unbaked model type
"type": "minecraft:model",
// Points to 'assets/examplemod/models/item/example_item_2.json'
"model": "examplemod:item/example_item_2"
}
}
]
}
}
数据生成
// Assume there is some DeferredItem<Item> EXAMPLE_ITEM
// Within an extended ModelProvider
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
itemModels.itemModelOutput.accept(
EXAMPLE_ITEM.get(),
new RangeSelectItemModel.Unbaked(
new Count(
// When true, normalizes the count using its max stack size
true
),
// A scalar to multiply to the computed property value
// If count was 0.3 and scale was 0.2, then the threshold checked would be 0.3*0.2=0.06
1,
// Entries with threshold information
List.of(
new RangeSelectItemModel.Entry(
// When the count is a third of its current max stack size
0.33,
// Can be any unbaked model type
new CuboidItemModelWrapper.Unbaked(
// Points to 'assets/examplemod/models/item/example_item_1.json'
Identifier.fromNamespaceAndPath("examplemod", "item/example_item_1"),
Optional.empty(),
Collections.emptyList()
)
),
new RangeSelectItemModel.Entry(
// When the count is two thirds of its current max stack size
0.66,
// Can be any unbaked model type
new CuboidItemModelWrapper.Unbaked(
// Points to 'assets/examplemod/models/item/example_item_2.json'
Identifier.fromNamespaceAndPath("examplemod", "item/example_item_2"),
Optional.empty(),
Collections.emptyList()
)
)
),
// The fallback model to use if no threshold matches
Optional.of(
new CuboidItemModelWrapper.Unbaked(
// Points to 'assets/examplemod/models/item/example_item.json'
ModelLocationUtils.getModelLocation(EXAMPLE_ITEM.get()),
Optional.empty(),
Collections.emptyList()
)
)
)
);
}
创建自定义 RangeSelectItemModelProperty 时,实现该接口,创建对象的 MapCodec,再通过 Mod 事件总线上的 RegisterRangeSelectItemModelPropertyEvent 注册 codec。接口只有 get 方法,接收当前堆叠、世界、持有实体及种子值,返回供范围分派模型解释的任意浮点值。
public record AppliedEnchantments() implements RangeSelectItemModelProperty {
public static final MapCodec<AppliedEnchantments> MAP_CODEC = MapCodec.unit(new AppliedEnchantments());
@Override
public float get(ItemStack stack, @Nullable ClientLevel level, @Nullable ItemOwner owner, int seed) {
return (float) stack.getTagEnchantments().size();
}
@Override
public MapCodec<AppliedEnchantments> type() {
return MAP_CODEC;
}
}
// In some event handler class
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerRangeProperties(RegisterRangeSelectItemModelPropertyEvent event) {
event.register(
// The name to reference as the type
Identifier.fromNamespaceAndPath("examplemod", "applied_enchantments"),
// The map codec
AppliedEnchantments.MAP_CODEC
)
}
JSON
// For some item 'examplemod:example_item'
// JSON at 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:range_dispatch",
// The `RangeSelectItemModelProperty` to use
"property": "examplemod:applied_enchantments",
// A scalar to multiply to the computed property value
// If count was 0.3 and scale was 0.2, then the threshold checked would be 0.3*0.2=0.06
"scale": 0.5,
"fallback": {
// The fallback model to use if no threshold matches
// Can be any unbaked model type
"type": "minecraft:model",
// Points to 'assets/examplemod/models/item/example_item.json'
"model": "examplemod:item/example_item"
},
// Entries with threshold information
"entries": [
{
// When there is at least one enchantment present
// Since 1 * the scale 0.5 = 0.5
"threshold": 0.5,
"model": {
// Can be any unbaked model type
"type": "minecraft:model",
// Points to 'assets/examplemod/models/item/example_item_1.json'
"model": "examplemod:item/example_item_1"
}
},
{
// When there are at least two enchantments present
// Since 2 * the scale 0.5 = 1
"threshold": 1,
"model": {
// Can be any unbaked model type
"type": "minecraft:model",
// Points to 'assets/examplemod/models/item/example_item_2.json'
"model": "examplemod:item/example_item_2"
}
}
]
}
}
数据生成
// Assume there is some DeferredItem<Item> EXAMPLE_ITEM
// Within an extended ModelProvider
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
itemModels.itemModelOutput.accept(
EXAMPLE_ITEM.get(),
new RangeSelectItemModel.Unbaked(
new AppliedEnchantments(),
// A scalar to multiply to the computed property value
// If count was 0.3 and scale was 0.2, then the threshold checked would be 0.3*0.2=0.06
0.5,
// Entries with threshold information
List.of(
new RangeSelectItemModel.Entry(
// When there is at least one enchantment present
0.5,
// Can be any unbaked model type
new CuboidItemModelWrapper.Unbaked(
// Points to 'assets/examplemod/models/item/example_item_1.json'
Identifier.fromNamespaceAndPath("examplemod", "item/example_item_1"),
Optional.empty(),
Collections.emptyList()
)
),
new RangeSelectItemModel.Entry(
// When there are at least two enchantments present
1,
// Can be any unbaked model type
new CuboidItemModelWrapper.Unbaked(
// Points to 'assets/examplemod/models/item/example_item_2.json'
Identifier.fromNamespaceAndPath("examplemod", "item/example_item_2"),
Optional.empty(),
Collections.emptyList()
)
)
),
// The fallback model to use if no threshold matches
Optional.of(
new CuboidItemModelWrapper.Unbaked(
// Points to 'assets/examplemod/models/item/example_item.json'
ModelLocationUtils.getModelLocation(EXAMPLE_ITEM.get()),
Optional.empty(),
Collections.emptyList()
)
)
)
);
}
选择模型
选择模型与范围分派类似,但依据 SelectItemModelProperty 提供的值切换,类似针对枚举的 switch。只有属性值与分支值完全匹配时才选择对应模型。已有类型见 SelectItemModelProperties。
JSON
// For some item 'examplemod:example_item'
// JSON at 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:select",
// The `SelectItemModelProperty` to use
"property": "minecraft:display_context",
"fallback": {
// The fallback model to use if no case matches
// Can be any unbaked model type
"type": "minecraft:model",
"model": "examplemod:item/example_item"
},
// Switch cases based on Selectable Property
"cases": [
{
// When the display context is `ItemDisplayContext#GUI`
"when": "gui",
"model": {
// Can be any unbaked model type
"type": "minecraft:model",
// Points to 'assets/examplemod/models/item/example_item_1.json'
"model": "examplemod:item/example_item_1"
}
},
{
// When the display context is `ItemDisplayContext#FIRST_PERSON_RIGHT_HAND`
"when": "firstperson_righthand",
"model": {
// Can be any unbaked model type
"type": "minecraft:model",
// Points to 'assets/examplemod/models/item/example_item_2.json'
"model": "examplemod:item/example_item_2"
}
}
]
}
}
数据生成
// Assume there is some DeferredItem<Item> EXAMPLE_ITEM
// Within an extended ModelProvider
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
itemModels.itemModelOutput.accept(
EXAMPLE_ITEM.get(),
new SelectItemModel.Unbaked(
new SelectItemModel.UnbakedSwitch(
// The `SelectItemModelProperty` to use
new DisplayContext(),
// Switch cases based on selectable property
List.of(
new SelectItemModel.SwitchCase(
// The list of cases to match for this model
List.of(ItemDisplayContext.GUI),
// Can be any unbaked model type
new CuboidItemModelWrapper.Unbaked(
// Points to 'assets/examplemod/models/item/example_item_1.json'
Identifier.fromNamespaceAndPath("examplemod", "item/example_item_1"),
Optional.empty(),
Collections.emptyList()
)
),
new SelectItemModel.SwitchCase(
// The list of cases to match for this model
List.of(ItemDisplayContext.FIRST_PERSON_RIGHT_HAND),
// Can be any unbaked model type
new CuboidItemModelWrapper.Unbaked(
// Points to 'assets/examplemod/models/item/example_item_2.json'
Identifier.fromNamespaceAndPath("examplemod", "item/example_item_2"),
Optional.empty(),
Collections.emptyList()
)
)
)
),
// The fallback model to use if no case matches
Optional.of(
new CuboidItemModelWrapper.Unbaked(
// Points to 'assets/examplemod/models/item/example_item.json'
ModelLocationUtils.getModelLocation(EXAMPLE_ITEM.get()),
Optional.empty(),
Collections.emptyList()
)
)
)
);
}
自定义选择属性需要实现 SelectItemModelProperty<T>,创建序列化属性值的 Codec,再创建编码和解码对象的 MapCodec,通过 Mod 事件总线的 RegisterSelectItemModelPropertyEvent 注册。泛型 T 表示用于分支选择的值。接口仅有 get 方法,接收堆叠、世界、持有实体、种子值和物品显示上下文,返回供选择模型使用的 T 值。
// The select property class
public record StackRarity() implements SelectItemModelProperty<Rarity> {
// The object to register that contains the relevant codecs
public static final SelectItemModelProperty.Type<StackRarity, Rarity> TYPE = SelectItemModelProperty.Type.create(
// The map codec for this property
MapCodec.unit(new StackRarity()),
// The codec for the object being selected
// Used to serialize the case entries ("when": <property value>)
Rarity.CODEC
);
@Nullable
@Override
public Rarity get(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity entity, int seed, ItemDisplayContext displayContext) {
// When null, uses the fallback model
return stack.get(DataComponents.RARITY);
}
@Override
public SelectItemModelProperty.Type<StackRarity, Rarity> type() {
return TYPE;
}
}
// In some event handler class
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerSelectProperties(RegisterSelectItemModelPropertyEvent event) {
event.register(
// The name to reference as the type
Identifier.fromNamespaceAndPath("examplemod", "rarity"),
// The property type
StackRarity.TYPE
)
}
JSON
// For some item 'examplemod:example_item'
// JSON at 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:select",
// The `SelectItemModelProperty` to use
"property": "examplemod:rarity",
"fallback": {
// The fallback model to use if no case matches
// Can be any unbaked model type
"type": "minecraft:model",
"model": "examplemod:item/example_item"
},
// Switch cases based on Selectable Property
"cases": [
{
// When the rarity is `Rarity#UNCOMMON`
"when": "uncommon",
"model": {
// Can be any unbaked model type
"type": "minecraft:model",
// Points to 'assets/examplemod/models/item/example_item_1.json'
"model": "examplemod:item/example_item_1"
}
},
{
// When the rarity is `Rarity#RARE`
"when": "rare",
"model": {
// Can be any unbaked model type
"type": "minecraft:model",
// Points to 'assets/examplemod/models/item/example_item_2.json'
"model": "examplemod:item/example_item_2"
}
}
]
}
}
数据生成
// Assume there is some DeferredItem<Item> EXAMPLE_ITEM
// Within an extended ModelProvider
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
itemModels.itemModelOutput.accept(
EXAMPLE_ITEM.get(),
new SelectItemModel.Unbaked(
new SelectItemModel.UnbakedSwitch(
// The `SelectItemModelProperty` to use
new StackRarity(),
// Switch cases based on selectable property
List.of(
new SelectItemModel.SwitchCase(
// The list of cases to match for this model
List.of(Rarity.UNCOMMON),
// Can be any unbaked model type
new CuboidItemModelWrapper.Unbaked(
// Points to 'assets/examplemod/models/item/example_item_1.json'
Identifier.fromNamespaceAndPath("examplemod", "item/example_item_1"),
Optional.empty(),
Collections.emptyList()
)
),
new SelectItemModel.SwitchCase(
// The list of cases to match for this model
List.of(Rarity.RARE),
// Can be any unbaked model type
new CuboidItemModelWrapper.Unbaked(
// Points to 'assets/examplemod/models/item/example_item_2.json'
Identifier.fromNamespaceAndPath("examplemod", "item/example_item_2"),
Optional.empty(),
Collections.emptyList()
)
)
)
),
// The fallback model to use if no case matches
Optional.of(
new CuboidItemModelWrapper.Unbaked(
// Points to 'assets/examplemod/models/item/example_item.json'
ModelLocationUtils.getModelLocation(EXAMPLE_ITEM.get()),
Optional.empty(),
Collections.emptyList()
)
)
)
);
}
条件模型
条件模型是三者中最简单的,通过 ConditionalItemModelProperty 取得布尔值,根据 true 或 false 选择模型。已有类型见 ConditionalItemModelProperties。
JSON
// For some item 'examplemod:example_item'
// JSON at 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:condition",
// The `ConditionalItemModelProperty` to use
"property": "minecraft:damaged",
// What the boolean outcome is
"on_true": {
// Can be any unbaked model type
"type": "minecraft:model",
// Points to 'assets/examplemod/models/item/example_item_1.json'
"model": "examplemod:item/example_item_1"
},
"on_false": {
// Can be any unbaked model type
"type": "minecraft:model",
// Points to 'assets/examplemod/models/item/example_item_2.json'
"model": "examplemod:item/example_item_2"
}
}
}
数据生成
// Assume there is some DeferredItem<Item> EXAMPLE_ITEM
// Within an extended ModelProvider
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
itemModels.itemModelOutput.accept(
EXAMPLE_ITEM.get(),
new ConditionalItemModel.Unbaked(
// The property to check
new Damaged(),
// When the boolean is true
new CuboidItemModelWrapper.Unbaked(
// Points to 'assets/examplemod/models/item/example_item_1.json'
Identifier.fromNamespaceAndPath("examplemod", "item/example_item_1"),
Optional.empty(),
Collections.emptyList()
),
// When the boolean is false
new CuboidItemModelWrapper.Unbaked(
// Points to 'assets/examplemod/models/item/example_item_2.json'
Identifier.fromNamespaceAndPath("examplemod", "item/example_item_2"),
Optional.empty(),
Collections.emptyList()
)
)
);
}
自定义条件属性需要实现 ConditionalItemModelProperty,创建对象的 MapCodec,并通过 Mod 事件总线的 RegisterConditionalItemModelPropertyEvent 注册。接口的 get 方法接收堆叠、世界、持有实体、种子值和显示上下文,返回布尔值,由条件模型选择 on_true 或 on_false。
public record BarVisible() implements ConditionalItemModelProperty {
public static final MapCodec<BarVisible> MAP_CODEC = MapCodec.unit(new BarVisible());
@Override
public boolean get(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity entity, int seed, ItemDisplayContext context) {
return stack.isBarVisible();
}
@Override
public MapCodec<BarVisible> type() {
return MAP_CODEC;
}
}
// In some event handler class
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerConditionalProperties(RegisterConditionalItemModelPropertyEvent event) {
event.register(
// The name to reference as the type
Identifier.fromNamespaceAndPath("examplemod", "bar_visible"),
// The map codec
BarVisible.MAP_CODEC
)
}
JSON
// For some item 'examplemod:example_item'
// JSON at 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:condition",
// The `ConditionalItemModelProperty` to use
"property": "examplemod:bar_visible",
// What the boolean outcome is
"on_true": {
// Can be any unbaked model type
"type": "minecraft:model",
// Points to 'assets/examplemod/models/item/example_item_1.json'
"model": "examplemod:item/example_item_1"
},
"on_false": {
// Can be any unbaked model type
"type": "minecraft:model",
// Points to 'assets/examplemod/models/item/example_item_2.json'
"model": "examplemod:item/example_item_2"
}
}
}
数据生成
// Assume there is some DeferredItem<Item> EXAMPLE_ITEM
// Within an extended ModelProvider
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
itemModels.itemModelOutput.accept(
EXAMPLE_ITEM.get(),
new ConditionalItemModel.Unbaked(
// The property to check
new BarVisible(),
// When the boolean is true
new CuboidItemModelWrapper.Unbaked(
// Points to 'assets/examplemod/models/item/example_item_1.json'
Identifier.fromNamespaceAndPath("examplemod", "item/example_item_1"),
Optional.empty(),
Collections.emptyList()
),
// When the boolean is false
new CuboidItemModelWrapper.Unbaked(
// Points to 'assets/examplemod/models/item/example_item_2.json'
Identifier.fromNamespaceAndPath("examplemod", "item/example_item_2"),
Optional.empty(),
Collections.emptyList()
)
)
);
}
来源: NeoForged/Documentation(26.1,原文第 452–1134 行)。中文翻译依据提交
4a680cdd4d15。