You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Bukkit服务器插件中实现类似马匹菜单的自定义实体GUI?

实现Bukkit服务器自定义Inventory GUI动态渲染实体的方案

完全可行,纯服务端即可实现,无需客户端模组。核心是复刻原版马匹界面的数据包逻辑,通过向客户端发送特定NMS数据包,在自定义GUI内渲染任意实体。

核心逻辑

原版马匹界面并非直接在Inventory里嵌入实体,而是服务端向客户端发送「打开马匹窗口」的数据包,同时在客户端的窗口渲染位置生成一个临时实体。我们可以照搬这个机制,替换成任意目标实体。

具体实现步骤

1. 搭建自定义GUI容器

先用Bukkit API创建基础Inventory框架,比如和原版马匹界面尺寸一致的5行容器:

// 创建5行的Inventory,自定义标题
Inventory customGui = Bukkit.createInventory(null, 45, "劫掠兽信息界面");
// 添加交互元素(比如关闭按钮)
customGui.setItem(40, new ItemStack(Material.BARRIER));

这个Inventory仅作为交互操作的容器,实体渲染依赖后续的数据包发送。

2. 发送窗口打开数据包

当玩家打开GUI时,不直接调用player.openInventory(customGui),而是发送NMS的PacketPlayOutOpenWindow数据包,指定窗口类型为minecraft:horse(对应原版带实体渲染位的窗口模板)。这里用ProtocolLib简化跨版本操作:

ProtocolManager manager = ProtocolLibrary.getProtocolManager();
// 构建打开窗口数据包
PacketContainer openWindowPacket = manager.createPacket(PacketType.Play.Server.OPEN_WINDOW);
openWindowPacket.getIntegers().write(0, player.getOpenInventory().getTopInventory().getViewers().size()); // 窗口ID
openWindowPacket.getWindowTypes().write(0, WindowType.HORSE); // 指定马匹窗口类型
openWindowPacket.getChatComponents().write(0, new ChatComponentText("劫掠兽信息界面"));
// 发送数据包给目标玩家
manager.sendServerPacket(player, openWindowPacket);

客户端收到后会自动加载带实体渲染区域的窗口框架。

3. 发送临时实体渲染数据包

创建目标实体的虚拟实例(不加入服务器世界),然后发送实体生成、元数据数据包到客户端:

// 创建虚拟劫掠兽实体(服务端不加入世界,避免影响游戏逻辑)
EntityRaider ravager = (EntityRaider) EntityTypes.RAVAGER.create(player.getWorld());
ravager.setLocation(player.getLocation().add(0, 100, 0), 0, 0); // 放在玩家视野外,防止世界中显示

// 发送实体生成数据包
PacketContainer spawnPacket = manager.createPacket(PacketType.Play.Server.SPAWN_ENTITY_LIVING);
spawnPacket.getIntegers().write(0, ravager.getId()); // 实体唯一ID
spawnPacket.getEntityTypeModifier().write(0, EntityType.RAVAGER);
spawnPacket.getDoubles().write(0, 0).write(1, 0).write(2, 0); // 客户端内渲染位置,固定为0即可
manager.sendServerPacket(player, spawnPacket);

// 发送实体元数据,同步实体状态(比如生命值、外观)
PacketContainer metadataPacket = manager.createPacket(PacketType.Play.Server.ENTITY_METADATA);
metadataPacket.getIntegers().write(0, ravager.getId());
metadataPacket.getEntityMetadataModifier().write(0, ravager.getDataWatcher());
manager.sendServerPacket(player, metadataPacket);

客户端会自动将这个实体渲染在窗口的中间位置,而非游戏世界中。

4. 清理临时实体

当玩家关闭GUI时,发送实体销毁数据包,避免客户端残留无效实体:

// 监听Inventory关闭事件
@EventHandler
public void onGuiClose(InventoryCloseEvent event) {
    Player player = (Player) event.getPlayer();
    ProtocolManager manager = ProtocolLibrary.getProtocolManager();
    // 发送销毁实体数据包(替换为你的虚拟实体ID)
    PacketContainer destroyPacket = manager.createPacket(PacketType.Play.Server.DESTROY_ENTITIES);
    destroyPacket.getIntLists().write(0, Arrays.asList(ravager.getId()));
    manager.sendServerPacket(player, destroyPacket);
}

同时要在PlayerQuitEvent中做同样的清理,防止玩家断开连接后客户端残留实体。

5. 全生物兼容

只要替换实体类型(比如EntityType.ZOMBIE、EntityType.CREEPER),发送对应类型的Spawn数据包即可,客户端会自动渲染对应实体,无需额外适配逻辑。

关键注意事项

  • 若不使用ProtocolLib,需用反射处理不同MC版本的NMS类名差异(比如1.19的NMS包是net.minecraft.network.protocol.game,1.18是net.minecraft.server.v1_18_R1)。
  • 虚拟实体绝对不能调用world.addEntity()加入服务器世界,否则会影响游戏逻辑。
  • 不同MC版本的窗口类型标识可能有差异,比如1.17+用字符串标识,旧版本用整数ID,需要针对性兼容。

内容的提问来源于stack exchange,提问作者LeyCM

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 13:05:16