强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 6 章:物品 API

第 6 章:物品 API

掌握 Minecraft 物品系统,学会创建自定义物品、操作 NBT 数据和自定义模型。


6.1 ItemStack 基础

ItemStack 是 Bukkit 中物品的核心类,表示一个物品堆。

创建基础物品

import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;

// 简单创建
ItemStack diamond = new ItemStack(Material.DIAMOND);

// 指定数量
ItemStack diamonds = new ItemStack(Material.DIAMOND, 64);

// 空物品(AIR)
ItemStack empty = ItemStack.empty();

物品不可变性

在 Paper 1.20.5+ 中,ItemStack 引入了不可变(Immutable)概念:

// 可变物品栈
ItemStack item = new ItemStack(Material.DIAMOND_SWORD);

// 不可变视图(防止意外修改)
ItemStack immutable = item.asOne();     // 返回数量为 1 的不可变副本
ItemStack mutable = item.asQuantity(5); // 返回新栈,数量为 5

6.2 ItemMeta:物品元数据

ItemMeta 控制物品的显示名称、Lore(描述)、附魔等。

设置显示名称

ItemStack item = new ItemStack(Material.DIAMOND_SWORD);
ItemMeta meta = item.getItemMeta();

if (meta != null) {
    // 使用 Adventure API 设置名称(Paper 推荐)
    meta.displayName(Component.text("§6传说之剑", NamedTextColor.GOLD));

    // 设置 Lore(描述文本)
    meta.lore(List.of(
        Component.text("§7一把传说中的武器", NamedTextColor.GRAY),
        Component.empty(),
        Component.text("§c⚔ 攻击力: +100", NamedTextColor.RED),
        Component.text("§b✦ 速度: +20%", NamedTextColor.AQUA)
    ));

    item.setItemMeta(meta);
}

不可破坏与自定义模型数据

if (meta != null) {
    // 设置为不可破坏
    meta.setUnbreakable(true);

    // 设置自定义模型数据(用于资源包自定义材质)
    meta.setCustomModelData(1001);

    item.setItemMeta(meta);
}

附魔系统

import org.bukkit.enchantments.Enchantment;

if (meta != null) {
    // 添加附魔
    meta.addEnchant(Enchantment.SHARPNESS, 5, true); // 锋利 V,true 忽略限制
    meta.addEnchant(Enchantment.FIRE_ASPECT, 2, true);

    // 移除附魔
    meta.removeEnchant(Enchantment.FIRE_ASPECT);

    // 检查附魔
    if (meta.hasEnchant(Enchantment.SHARPNESS)) {
        int level = meta.getEnchantLevel(Enchantment.SHARPNESS);
    }

    item.setItemMeta(meta);
}

属性修饰符(Attribute Modifiers)

import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeModifier;
import org.bukkit.inventory.EquipmentSlotGroup;

if (meta != null) {
    // 增加攻击力
    meta.addAttributeModifier(
        Attribute.ATTACK_DAMAGE,
        new AttributeModifier(
            NamespacedKey.minecraft("attack_damage"),
            10.0, // 增加 10 点
            AttributeModifier.Operation.ADD_NUMBER,
            EquipmentSlotGroup.MAINHAND
        )
    );

    // 增加移动速度
    meta.addAttributeModifier(
        Attribute.MOVEMENT_SPEED,
        new AttributeModifier(
            NamespacedKey.minecraft("movement_speed"),
            0.1,
            AttributeModifier.Operation.ADD_SCALAR,
            EquipmentSlotGroup.MAINHAND
        )
    );

    item.setItemMeta(meta);
}

6.3 自定义物品工厂

在实际项目中,通常使用工厂模式创建自定义物品:

package com.example.myplugin.items;

import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.ItemMeta;
import org.bukkit.persistence.PersistentDataType;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;

import java.util.List;

public final class CustomItemFactory {

    private static NamespacedKey ITEM_ID_KEY;

    public static void init(MyPlugin plugin) {
        ITEM_ID_KEY = new NamespacedKey(plugin, "custom_item_id");
    }

    /**
     * 创建魔法苹果
     */
    public static ItemStack createMagicApple() {
        ItemStack item = new ItemStack(Material.GOLDEN_APPLE);
        ItemMeta meta = item.getItemMeta();

        if (meta != null) {
            meta.displayName(Component.text("✦ 魔法苹果", NamedTextColor.LIGHT_PURPLE)
                .decoration(TextDecoration.ITALIC, false));

            meta.lore(List.of(
                Component.text("§7散发着神秘的光芒", NamedTextColor.GRAY)
                    .decoration(TextDecoration.ITALIC, false),
                Component.empty(),
                Component.text("§a❤ 恢复全部生命值", NamedTextColor.GREEN)
                    .decoration(TextDecoration.ITALIC, false),
                Component.text("§b✦ 获得 30 秒生命恢复", NamedTextColor.AQUA)
                    .decoration(TextDecoration.ITALIC, false)
            ));

            meta.setCustomModelData(2001);

            // 存储自定义 ID(用于识别物品)
            meta.getPersistentDataContainer().set(
                ITEM_ID_KEY,
                PersistentDataType.STRING,
                "magic_apple"
            );

            item.setItemMeta(meta);
        }

        return item;
    }

    /**
     * 创建传送卷轴
     */
    public static ItemStack createTeleportScroll() {
        ItemStack item = new ItemStack(Material.PAPER);
        ItemMeta meta = item.getItemMeta();

        if (meta != null) {
            meta.displayName(Component.text("§b传送卷轴"));
            meta.lore(List.of(
                Component.text("§7右键使用,传送到"),
                Component.text("§7设定的地标位置")
            ));
            meta.setCustomModelData(3001);

            meta.getPersistentDataContainer().set(
                ITEM_ID_KEY,
                PersistentDataType.STRING,
                "teleport_scroll"
            );

            item.setItemMeta(meta);
        }

        return item;
    }

    /**
     * 检查物品是否是自定义物品
     */
    public static boolean isCustomItem(ItemStack item, String customId) {
        if (item == null || item.getType() == Material.AIR) return false;

        ItemMeta meta = item.getItemMeta();
        if (meta == null) return false;

        String id = meta.getPersistentDataContainer().get(
            ITEM_ID_KEY, PersistentDataType.STRING
        );

        return customId.equals(id);
    }
}

6.4 NBT 数据操作

Paper 提供了 PersistentDataContainer API 来存储自定义数据,无需依赖 NMS。

基本 NBT 读写

NamespacedKey key = new NamespacedKey(plugin, "my_custom_data");

// 写入数据
meta.getPersistentDataContainer().set(key, PersistentDataType.STRING, "hello");
meta.getPersistentDataContainer().set(key, PersistentDataType.INTEGER, 42);
meta.getPersistentDataContainer().set(key, PersistentDataType.DOUBLE, 3.14);

// 读取数据
String str = meta.getPersistentDataContainer().get(key, PersistentDataType.STRING);
Integer num = meta.getPersistentDataContainer().get(key, PersistentDataType.INTEGER);

// 检查是否存在
boolean hasKey = meta.getPersistentDataContainer().has(key, PersistentDataType.STRING);

// 删除数据
meta.getPersistentDataContainer().remove(key);

支持的数据类型

PersistentDataTypeJava 类型说明
BYTEByte字节
SHORTShort短整数
INTEGERInteger整数
LONGLong长整数
FLOATFloat单精度浮点
DOUBLEDouble双精度浮点
STRINGString字符串
BYTE_ARRAYbyte[]字节数组
INTEGER_ARRAYint[]整数数组
LONG_ARRAYlong[]长整数数组
TAG_CONTAINERPersistentDataContainer嵌套容器

自定义复合数据

// 存储嵌套数据
NamespacedKey rootKey = new NamespacedKey(plugin, "item_data");

PersistentDataContainer container = meta.getPersistentDataContainer()
    .getAdapterContext()
    .newPersistentDataContainer();

container.set(new NamespacedKey(plugin, "damage"), PersistentDataType.DOUBLE, 15.0);
container.set(new NamespacedKey(plugin, "level"), PersistentDataType.INTEGER, 5);

meta.getPersistentDataContainer().set(rootKey, PersistentDataType.TAG_CONTAINER, container);

6.5 Paper 数据组件(Data Components)

Paper 1.20.5+ 引入了新的数据组件系统,替代了传统的部分 ItemMeta 方法:

import io.papermc.paper.datacomponent.DataComponentTypes;
import io.papermc.paper.datacomponent.item.CustomModelData;

// 使用数据组件设置自定义模型数据
item.setData(DataComponentTypes.CUSTOM_MODEL_DATA, CustomModelData.customModelData(1001));

// 读取
CustomModelData cmd = item.getData(DataComponentTypes.CUSTOM_MODEL_DATA);

注意: 数据组件 API 在 Paper 中仍在快速迭代,具体可用的组件类型请参考最新文档。


6.6 烟火物品示例

创建自定义烟火

import org.bukkit.FireworkEffect;
import org.bukkit.inventory.meta.FireworkMeta;

public static ItemStack createPartyFirework() {
    ItemStack item = new ItemStack(Material.FIREWORK_ROCKET);
    FireworkMeta meta = (FireworkMeta) item.getItemMeta();

    if (meta != null) {
        meta.displayName(Component.text("§6派对烟花"));

        meta.addEffect(FireworkEffect.builder()
            .with(FireworkEffect.Type.STAR)
            .withColor(Color.RED, Color.BLUE, Color.GREEN)
            .withFade(Color.YELLOW, Color.PURPLE)
            .trail(true)
            .flicker(true)
            .build());

        meta.setPower(2); // 飞行高度

        item.setItemMeta(meta);
    }

    return item;
}

6.7 物品比较与识别

物品相等性检查

// 不可靠:比较对象引用
if (item1 == item2) { ... }

// 可靠:比较物品内容
if (item1.isSimilar(item2)) { ... } // 材料、元数据都相同
if (item1.equals(item2)) { ... }    // isSimilar + 数量相同

识别自定义物品

// 方法一:PersistentDataContainer(推荐)
public static String getCustomId(ItemStack item) {
    if (item == null || !item.hasItemMeta()) return null;
    return item.getItemMeta().getPersistentDataContainer()
        .get(ITEM_ID_KEY, PersistentDataType.STRING);
}

// 使用
String id = getCustomId(item);
if ("magic_apple".equals(id)) {
    // 是魔法苹果
}

// 方法二:CustomModelData
public static boolean isSpecialItem(ItemStack item, int modelData) {
    if (item == null || !item.hasItemMeta()) return false;
    ItemMeta meta = item.getItemMeta();
    return meta.hasCustomModelData() && meta.getCustomModelData() == modelData;
}

6.8 业务场景:武器升级系统

public class WeaponUpgradeSystem {

    private static final NamespacedKey LEVEL_KEY = new NamespacedKey(plugin, "weapon_level");
    private static final NamespacedKey XP_KEY = new NamespacedKey(plugin, "weapon_xp");

    /**
     * 给武器添加经验
     */
    public static boolean addWeaponXP(ItemStack weapon, int xp) {
        if (!isWeapon(weapon)) return false;

        ItemMeta meta = weapon.getItemMeta();
        if (meta == null) return false;

        PersistentDataContainer pdc = meta.getPersistentDataContainer();
        int currentXP = pdc.getOrDefault(XP_KEY, PersistentDataType.INTEGER, 0);
        int currentLevel = pdc.getOrDefault(LEVEL_KEY, PersistentDataType.INTEGER, 1);

        int newXP = currentXP + xp;
        int xpNeeded = getXPForLevel(currentLevel);

        // 检查是否升级
        if (newXP >= xpNeeded) {
            newXP -= xpNeeded;
            currentLevel++;
            pdc.set(LEVEL_KEY, PersistentDataType.INTEGER, currentLevel);

            // 更新 Lore
            updateWeaponLore(meta, currentLevel, newXP);
            weapon.setItemMeta(meta);
            return true; // 升级了
        }

        pdc.set(XP_KEY, PersistentDataType.INTEGER, newXP);
        updateWeaponLore(meta, currentLevel, newXP);
        weapon.setItemMeta(meta);
        return false;
    }

    private static int getXPForLevel(int level) {
        return 100 + (level * 50); // 等级越高需要越多经验
    }

    private static void updateWeaponLore(ItemMeta meta, int level, int xp) {
        int needed = getXPForLevel(level);
        meta.lore(List.of(
            Component.text("§7等级: §e" + level, NamedTextColor.GRAY),
            Component.text("§7经验: §a" + xp + "§7/§a" + needed, NamedTextColor.GRAY),
            Component.empty(),
            Component.text("§7击杀怪物获得经验", NamedTextColor.GRAY)
        ));
    }
}

6.9 物品序列化与反序列化

Base64 编码存储

import org.bukkit.inventory.ItemStack;
import org.bukkit.util.io.BukkitObjectInputStream;
import org.bukkit.util.io.BukkitObjectOutputStream;

import java.io.*;
import java.util.Base64;

public final class ItemSerializer {

    /**
     * 物品序列化为 Base64
     */
    public static String toBase64(ItemStack item) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream);
        dataOutput.writeObject(item);
        dataOutput.close();
        return Base64.getEncoder().encodeToString(outputStream.toByteArray());
    }

    /**
     * Base64 反序列化为物品
     */
    public static ItemStack fromBase64(String base64) throws IOException, ClassNotFoundException {
        ByteArrayInputStream inputStream = new ByteArrayInputStream(
            Base64.getDecoder().decode(base64)
        );
        BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream);
        ItemStack item = (ItemStack) dataInput.readObject();
        dataInput.close();
        return item;
    }
}

背包序列化

/**
 * 背包序列化为 Base64
 */
public static String inventoryToBase64(PlayerInventory inventory) throws IOException {
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream);

    // 写入物品数组长度
    dataOutput.writeInt(inventory.getSize());

    // 写入每个物品
    for (int i = 0; i < inventory.getSize(); i++) {
        dataOutput.writeObject(inventory.getItem(i));
    }

    dataOutput.close();
    return Base64.getEncoder().encodeToString(outputStream.toByteArray());
}

6.10 常见问题排查

问题原因解决方案
ItemMeta 为 nullAIR 物品没有 Meta先检查 item.getType() != AIR
附魔不显示未设置 glint添加附魔或使用 meta.addItemFlags
Lore 不更新物品引用未更新item.setItemMeta(meta) 后使用新引用
自定义模型不生效资源包未加载确保客户端已应用资源包
物品丢失物品栈被意外覆盖克隆物品栈 item.clone()

6.11 扩展阅读


6.12 本章小结

要点内容
ItemStack物品栈,包含材料类型和数量
ItemMeta物品元数据,控制名称、Lore、附魔等
PersistentDataContainerPaper 推荐的自定义数据存储方式
自定义物品使用工厂模式 + PDC 识别和创建
序列化Base64 编码存储物品数据
Data ComponentsPaper 1.20.5+ 新 API,逐步替代 ItemMeta 部分功能

下一章: 第 7 章:背包与 GUI — 学习创建自定义 GUI 菜单、处理背包交互事件。