物品属性模型

物品属性模型

有些物品会根据堆叠数据改变状态,例如拉弓、鞘翅损坏、时钟处于特定维度等。物品模型可以跟踪某个属性,并根据条件选择模型。属性模型分为范围分派、选择和条件三种,分别类似浮点范围表达式、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。