强曰为道

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

第 9 章:实体 API

第 9 章:实体 API

深入理解实体系统,掌握自定义生物、AI 装备和实体管理。


9.1 实体类型层级

Bukkit 的实体类遵循严格的继承层级:

Entity(最基础)
├── Projectile(弹射物)
│   ├── Arrow
│   ├── Snowball
│   └── Fireball
├── LivingEntity(活体实体)
│   ├── Player
│   ├── Mob(生物)
│   │   ├── Monster(怪物)
│   │   │   ├── Zombie
│   │   │   ├── Skeleton
│   │   │   ├── Creeper
│   │   │   └── Enderman
│   │   ├── Animals(动物)
│   │   │   ├── Cow
│   │   │   ├── Pig
│   │   │   └── Sheep
│   │   └── WaterMob(水生生物)
│   └── ArmorStand(盔甲架)
└── Item(掉落物)

常用实体类型表

类型实体典型用途
EntityType.ZOMBIE僵尸PVE 怪物
EntityType.SKELETON骷髅远程怪物
EntityType.CREEPER苦力怕自爆怪物
EntityType.VILLAGER村民商店 NPC
EntityType.ARMOR_STAND盔甲架展示/装饰
EntityType.ITEM_FRAME物品展示框展示物品
EntityType.MARKER标记器无形体标记
EntityType.TEXT_DISPLAY文本展示浮动文字

9.2 实体属性操作

生命值管理

// 获取实体
LivingEntity entity = ...;

// 获取/设置生命值
double health = entity.getHealth();
entity.setHealth(30.0); // 设置为 15 颗心

// 获取最大生命值
double maxHealth = entity.getMaxHealth();

// 设置最大生命值(通过 Attribute)
entity.getAttribute(Attribute.MAX_HEALTH).setBaseValue(100.0);
entity.setHealth(100.0); // 同步设置当前值

通用属性

Attribute说明默认值
MAX_HEALTH最大生命值20.0
FOLLOW_RANGE跟踪范围32.0
KNOCKBACK_RESISTANCE击退抗性0.0
MOVEMENT_SPEED移动速度0.7
ATTACK_DAMAGE攻击伤害2.0
ARMOR护甲值0.0
ARMOR_TOUGHNESS护甲韧性0.0
ATTACK_KNOCKBACK攻击击退0.0
FLYING_SPEED飞行速度0.4

修改属性

// 修改攻击力
AttributeInstance attackAttr = entity.getAttribute(Attribute.ATTACK_DAMAGE);
if (attackAttr != null) {
    attackAttr.setBaseValue(10.0); // 基础攻击力设为 10
}

// 添加修饰符(Modifier)
attackAttr.addModifier(new AttributeModifier(
    NamespacedKey.minecraft("bonus_damage"),
    5.0,                                    // 增加 5 点
    AttributeModifier.Operation.ADD_NUMBER  // 直接相加
));

// 检查修饰符
if (attackAttr.getModifier(NamespacedKey.minecraft("bonus_damage")) != null) {
    // 已有加成
}

9.3 生物装备系统

设置装备

public class MobEquipment {

    /**
     * 给生物设置装备
     */
    public static void equipMob(LivingEntity mob, Material helmet,
                                 Material chestplate, Material leggings,
                                 Material boots, Material weapon) {
        EntityEquipment equipment = mob.getEquipment();

        if (helmet != null) equipment.setHelmet(new ItemStack(helmet));
        if (chestplate != null) equipment.setChestplate(new ItemStack(chestplate));
        if (leggings != null) equipment.setLeggings(new ItemStack(leggings));
        if (boots != null) equipment.setBoots(new ItemStack(boots));
        if (weapon != null) equipment.setItemInMainHand(new ItemStack(weapon));

        // 设置装备掉落率(0.0 = 不掉落,1.0 = 100%掉落)
        equipment.setHelmetDropChance(0.0f);
        equipment.setChestplateDropChance(0.1f);
        equipment.setItemInMainHandDropChance(0.5f);
    }

    /**
     * 装备的 NBT 自定义
     */
    public static void equipWithCustomWeapon(LivingEntity mob) {
        ItemStack sword = new ItemStack(Material.DIAMOND_SWORD);
        ItemMeta meta = sword.getItemMeta();

        if (meta != null) {
            meta.displayName(Component.text("§c暗黑之刃"));
            meta.addEnchant(Enchantment.SHARPNESS, 5, true);
            meta.lore(List.of(
                Component.text("§7骷髅将军的武器", NamedTextColor.GRAY)
            ));

            // 自定义属性
            meta.addAttributeModifier(Attribute.ATTACK_DAMAGE,
                new AttributeModifier(
                    NamespacedKey.minecraft("attack_damage"),
                    15.0,
                    AttributeModifier.Operation.ADD_NUMBER,
                    EquipmentSlotGroup.MAINHAND
                ));

            sword.setItemMeta(meta);
        }

        mob.getEquipment().setItemInMainHand(sword);
    }
}

装备掉落事件

@EventHandler
public void onEntityDeath(EntityDeathEvent event) {
    LivingEntity entity = event.getEntity();

    if (entity instanceof Zombie zombie) {
        // 自定义掉落逻辑
        event.getDrops().clear(); // 清除默认掉落

        // 自定义掉落
        if (zombie.getEquipment().getHelmet() != null) {
            event.getDrops().add(new ItemStack(Material.DIAMOND, 2));
        }

        // 设置经验
        event.setDroppedExp(50);
    }
}

9.4 生物 AI 系统

Paper 的目标选择器(Goal System)

Paper 1.20+ 提供了原生的生物目标(Goal)API:

import io.papermc.paper.entity.ai.Goal;
import io.papermc.paper.entity.ai.GoalKey;
import io.papermc.paper.entity.ai.GoalType;
import io.papermc.paper.entity.ai.MobGoals;

// 获取 MobGoals API
MobGoals mobGoals = Bukkit.getMobGoals();

// 移除所有 AI 目标
mobGoals.removeAllGoals(zombie);

// 添加自定义目标
Goal<Zombie> patrolGoal = new PatrolGoal(zombie);
mobGoals.addGoal(zombie, 1, patrolGoal); // 优先级 1

自定义巡逻目标

import org.bukkit.entity.Zombie;
import org.jetbrains.annotations.NotNull;
import io.papermc.paper.entity.ai.Goal;
import io.papermc.paper.entity.ai.GoalKey;
import io.papermc.paper.entity.ai.GoalType;

import java.util.EnumSet;
import java.util.List;

public class PatrolGoal implements Goal<Zombie> {

    private final Zombie zombie;
    private final List<Location> patrolPoints;
    private int currentPoint = 0;
    private long lastMoveTime = 0;

    public PatrolGoal(Zombie zombie) {
        this.zombie = zombie;
        this.patrolPoints = getPatrolRoute();
    }

    @Override
    public boolean shouldActivate() {
        return !patrolPoints.isEmpty();
    }

    @Override
    public void tick() {
        long now = System.currentTimeMillis();
        if (now - lastMoveTime < 3000) return; // 每 3 秒移动一次

        Location target = patrolPoints.get(currentPoint);
        zombie.getPathfinder().moveTo(target, 1.0);

        if (zombie.getLocation().distance(target) < 2.0) {
            currentPoint = (currentPoint + 1) % patrolPoints.size();
        }

        lastMoveTime = now;
    }

    @Override
    public @NotNull GoalKey<Zombie> getKey() {
        return GoalKey.of(Zombie.class,
            NamespacedKey.fromString("myplugin:patrol"));
    }

    @Override
    public @NotNull EnumSet<GoalType> getTypes() {
        return EnumSet.of(GoalType.MOVE);
    }
}

禁用特定 AI

// 移除攻击目标
mobGoals.removeGoalOfType(zombie, GoalType.TARGET);

// 让僵尸不攻击玩家
mobGoals.addGoal(zombie, 0, new Goal<Zombie>() {
    @Override
    public boolean shouldActivate() {
        return false; // 永不激活 = 禁用此目标
    }

    @Override
    public void tick() {}

    @Override
    public @NotNull GoalKey<Zombie> getKey() {
        return GoalKey.of(Zombie.class,
            new NamespacedKey("myplugin", "no_target"));
    }

    @Override
    public @NotNull EnumSet<GoalType> getTypes() {
        return EnumSet.of(GoalType.TARGET);
    }
});

9.5 盔甲架(ArmorStand)应用

盔甲架是服务端开发中非常重要的实体,常用于 GUI 展示和装饰。

创建展示 NPC

public class DisplayNPC {

    /**
     * 创建一个展示用的盔甲架 NPC
     */
    public static ArmorStand createNPC(Location loc, String name) {
        ArmorStand stand = loc.getWorld().spawn(loc, ArmorStand.class, as -> {
            as.customName(Component.text(name));
            as.customNameVisible(true);

            // 不可见/不可交互
            as.setVisible(false);
            as.setGravity(false);
            as.setBasePlate(false);

            // 设置为小型
            as.setSmall(false);

            // 设置装备
            as.getEquipment().setHelmet(new ItemStack(Material.ZOMBIE_HEAD));
            as.getEquipment().setChestplate(new ItemStack(Material.DIAMOND_CHESTPLATE));
            as.getEquipment().setItemInMainHand(new ItemStack(Material.DIAMOND_SWORD));
        });

        return stand;
    }

    /**
     * 创建小型展示架
     */
    public static ArmorStand createMiniDisplay(Location loc, ItemStack displayItem) {
        return loc.getWorld().spawn(loc, ArmorStand.class, as -> {
            as.setVisible(false);
            as.setGravity(false);
            as.setSmall(true);
            as.setBasePlate(false);
            as.setMarker(true); // 不可交互

            as.getEquipment().setHelmet(displayItem);
        });
    }
}

9.6 文本展示与展示实体

Paper 1.19.4+ 支持新的展示实体,比盔甲架性能更好:

import org.bukkit.entity.TextDisplay;
import org.bukkit.entity.ItemDisplay;
import org.bukkit.entity.BlockDisplay;

// 文本展示
TextDisplay textDisplay = world.spawn(loc, TextDisplay.class, td -> {
    td.text(Component.text("§6欢迎来到服务器!")
        .append(Component.newline())
        .append(Component.text("§7享受你的冒险吧!")));
    td.setBillboard(Display.Billboard.CENTER); // 始终面向玩家
    td.setLineWidth(200);
    td.setBackgroundColor(Color.fromARGB(128, 0, 0, 0)); // 半透明黑色背景
    td.setShadowed(true);
});

// 物品展示
ItemDisplay itemDisplay = world.spawn(loc, ItemDisplay.class, id -> {
    id.setItemStack(new ItemStack(Material.DIAMOND_SWORD));
    id.setBillboard(Display.Billboard.FIXED);
    id.setItemDisplayTransform(ItemDisplay.ItemDisplayTransform.GUI);
});

// 方块展示
BlockDisplay blockDisplay = world.spawn(loc, BlockDisplay.class, bd -> {
    bd.setBlock(Material.DIAMOND_BLOCK.createBlockData());
    bd.setTransformation(new Transformation(
        new Vector3f(0, 0, 0),    // 平移
        new AxisAngle4f(0, 0, 1, 0), // 旋转
        new Vector3f(2, 2, 2),    // 缩放(放大 2 倍)
        new AxisAngle4f(0, 0, 1, 0)
    ));
});

9.7 村民 NPC 系统

创建自定义村民

public class VillagerNPC {

    /**
     * 创建商店村民
     */
    public static Villager createShopNPC(Location loc, String shopName) {
        return loc.getWorld().spawn(loc, Villager.class, v -> {
            v.customName(Component.text("§6" + shopName));
            v.customNameVisible(true);

            // 设置职业
            v.setProfession(Villager.Profession.WEAPONSMITH);

            // 设置外观
            v.setVillagerType(Villager.PlainsType.PLAINS);

            // 设置等级
            v.setVillagerLevel(5);

            // 不可被攻击
            v.setInvulnerable(true);

            // 不会消失
            v.setRemoveWhenFarAway(false);

            // 禁止 AI(不会乱走)
            v.setAI(false);

            // 设置交易(通过 API)
            List<MerchantRecipe> recipes = new ArrayList<>();
            recipes.add(createRecipe(
                new ItemStack(Material.EMERALD, 5),
                new ItemStack(Material.DIAMOND_SWORD),
                10 // 最大使用次数
            ));
            v.setRecipes(recipes);
        });
    }

    private static MerchantRecipe createRecipe(ItemStack cost, ItemStack result,
                                                int maxUses) {
        MerchantRecipe recipe = new MerchantRecipe(result, maxUses);
        recipe.addIngredient(cost);
        return recipe;
    }
}

村民交易事件

@EventHandler
public void onVillagerTrade(VillagerTradeEvent event) {
    Villager villager = event.getEntity();
    Player player = (Player) event.getPlayer();
    ItemStack result = event.getResult();

    // 记录交易日志
    getLogger().info(player.getName() + " 与 " + villager.getName()
        + " 交易了 " + result.getType());

    // 自定义逻辑:VIP 折扣
    if (player.hasPermission("myplugin.vip")) {
        // 给予额外奖励
        player.sendMessage("§6VIP 折扣!额外获得经验!");
        player.giveExp(10);
    }
}

9.8 实体管理与清理

实体计数与限制

public class EntityManager {

    /**
     * 获取指定区域内的实体数量
     */
    public static int countEntities(World world, int radius, EntityType type) {
        Location center = world.getSpawnLocation();
        return (int) world.getNearbyEntities(center, radius, radius, radius).stream()
            .filter(e -> e.getType() == type)
            .count();
    }

    /**
     * 清理多余实体
     */
    public static int cleanupEntities(World world, EntityType type, int maxCount) {
        List<Entity> entities = world.getEntities().stream()
            .filter(e -> e.getType() == type)
            .filter(e -> !(e instanceof Player))
            .collect(Collectors.toList());

        if (entities.size() <= maxCount) return 0;

        int removed = 0;
        int toRemove = entities.size() - maxCount;

        // 按距离排序,移除距离玩家最远的
        entities.sort(Comparator.comparingDouble(e ->
            e.getNearbyEntities(100, 100, 100).stream()
                .filter(n -> n instanceof Player)
                .mapToDouble(n -> n.getLocation().distanceSquared(e.getLocation()))
                .min().orElse(Double.MAX_VALUE)));

        for (int i = entities.size() - 1; i >= 0 && removed < toRemove; i--) {
            entities.get(i).remove();
            removed++;
        }

        return removed;
    }
}

实体自定义名称与可见性

// 设置自定义名称
entity.customName(Component.text("§c精英僵尸"));
entity.customNameVisible(true); // 始终显示名称

// Paper 的自定义名称样式
entity.customName(Component.text("BOSS")
    .color(NamedTextColor.RED)
    .decorate(TextDecoration.BOLD));

// 设置发光效果
entity.setGlowing(true);

9.9 实体传送与跟随

// 实体传送
entity.teleport(location);

// 让实体面向目标
Location lookAt = target.getLocation();
entity.teleport(entity.getLocation().setDirection(
    lookAt.toVector().subtract(entity.getLocation().toVector())
));

// 实体跟随玩家(使用 Pathfinder)
if (entity instanceof Mob mob) {
    mob.getPathfinder().moveTo(player, 1.5); // 速度 1.5
}

9.10 业务场景:BOSS 系统

public class BossManager {

    private static final String BOSS_NAME = "§4§l暗黑龙王";

    /**
     * 生成 BOSS
     */
    public static EnderDragon spawnBoss(Location loc) {
        return loc.getWorld().spawn(loc, EnderDragon.class, dragon -> {
            dragon.customName(Component.text(BOSS_NAME));
            dragon.customNameVisible(true);

            // 设置属性
            dragon.getAttribute(Attribute.MAX_HEALTH).setBaseValue(200.0);
            dragon.setHealth(200.0);
            dragon.getAttribute(Attribute.ATTACK_DAMAGE).setBaseValue(15.0);

            // 不会自然消失
            dragon.setRemoveWhenFarAway(false);

            // 发光
            dragon.setGlowing(true);
        });
    }

    /**
     * BOSS 血条管理
     */
    public static void updateBossBar(BossBar bossBar, EnderDragon dragon) {
        double progress = dragon.getHealth() / dragon.getMaxHealth();
        bossBar.progress(Math.max(0, Math.min(1, progress)));

        if (progress > 0.6) {
            bossBar.color(BarColor.GREEN);
        } else if (progress > 0.3) {
            bossBar.color(BarColor.YELLOW);
        } else {
            bossBar.color(BarColor.RED);
        }
    }
}

9.11 常见问题排查

问题原因解决方案
实体 AI 不生效setAI(false) 会禁用所有 AI只移除特定目标
实体穿墙碰撞箱问题使用 setCollidable()
盔甲架消失被攻击致死setInvulnerable(true)
实体过多导致卡顿未设置清理机制定时清理 + 限制生成数量
自定义名称不显示customNameVisible 为 false设为 true

9.12 扩展阅读


9.13 本章小结

要点内容
实体层级Entity → LivingEntity → Mob → 具体类型
属性系统Attribute + AttributeModifier 控制战斗属性
装备系统EntityEquipment 设置装备和掉落率
AI 系统Paper MobGoals API,自定义 Goal
展示实体TextDisplay / ItemDisplay 比盔甲架性能更好
实体管理定时清理、限制数量、分批处理

下一章: 第 10 章:计分板 — 学习计分板、侧边栏、标签和 Teams 系统。