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

PaperMC 插件开发完全指南 / 第 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);

支持的数据类型

PersistentDataType Java 类型 说明
BYTE Byte 字节
SHORT Short 短整数
INTEGER Integer 整数
LONG Long 长整数
FLOAT Float 单精度浮点
DOUBLE Double 双精度浮点
STRING String 字符串
BYTE_ARRAY byte[] 字节数组
INTEGER_ARRAY int[] 整数数组
LONG_ARRAY long[] 长整数数组
TAG_CONTAINER PersistentDataContainer 嵌套容器

自定义复合数据

// 存储嵌套数据
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 为 null AIR 物品没有 Meta 先检查 item.getType() != AIR
附魔不显示 未设置 glint 添加附魔或使用 meta.addItemFlags
Lore 不更新 物品引用未更新 item.setItemMeta(meta) 后使用新引用
自定义模型不生效 资源包未加载 确保客户端已应用资源包
物品丢失 物品栈被意外覆盖 克隆物品栈 item.clone()

6.11 扩展阅读


6.12 本章小结

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

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